One of the questions that I see with some frequency on the Twitters, Emails and general InterWebs is:

“Hey yo! How can I extend Kendo UI widgets or create custom Kendo UI plugins?”.

Of course the fabulous thing about JavaScript frameworks is that you can in fact extend them or create your own plugins for them.  jQuery’s ability to add plugins so easily is one of the major contributing factors in its raging success and rampant implementation. There was an absolute explosion of available plugins online that did everything from the very useful Masked Input, to the completely ridiculous yet awesome UnicornBlast.

Kendo UI is built on a plugin architecture and exposes that same interface for you to either extend existing widgets, create your own plugins that are completely new widgets, or composite of exsting Kendo UI controls.

In this post, I’ll show you how to create a custom Kendo UI plugin that uses the AutoComplete, ListView, Pager and Kendo UI DataSource to harness the power of Google Suggest and the YouTube search API.

Disclaimer

While Kendo UI provides awesome ways for building custom widgets, remember that the Kendo UI team can only support features that ship out of the box! If you want improvements to existing Kendo UI widgets, it's much better to share the idea on UserVoice and/or submit a feature request and/or ask for the improvement in the forums. If the improvement makes sense, we're very likely to implement it in an upcoming release (we have 3 major releases + service packs every year), and then you get support for it! Same goes for new widgets. If you want it, ask for it!  

Of course, we'd love to see your work and help you out if you run into any issues, so you can always hit us up on Twitter or by posting a question on StackOverflow.

Kendo YouTube Search Widget

You can grab all the code used in this project from the kendo-plugins GitHub repo.  First, lets have a look at the finished product.  You create the YouTube search widget in the normal way that you would initialize any Kendo UI widget.

Imperative Initialization

<div id="youtube"></div> 
<script>
    $("#youtube").kendoYouTube();
</script>

Declarative Initialization

<div class="container"> 
  <div id="youtube" data-role="kendoYouTube"></div> 
</div> 

<script> 
    
    // bind all the widgets
    kendo.bind($(".container"));

</script>

And here is what you get…

Of course you can dive right in and start looking at the source if that’s how your boat is best floated.  I’m going to dissect it a bit here and go over some of the practices that we use when building Widgets that you can use as well.  First let me go over the basics of creating a new widget and provide you with some boilerplate code.

The Boilerplate

Extending Kendo UI is pretty darn easy and straightforward.

Step 1 is to extend the base Widget class in the kendo.ui namespace.  I also create some variables to hold values which helps with minification down the road.

Extend The Base Widget Class

(function($) {
    
  // shorten references to variables. this is better for uglification 
  var kendo = window.kendo,
      ui = kendo.ui,
      Widget = ui.Widget;

  var YouTube = Widget.extend({

    // awesome code lies herein.

  });

})(jQuery);
Notice a couple of things here.

1. The entire thing is wrapped in a self executing anonymous function so as to protect the global namespace as if it were the rebel base.  jQuery is passed in as a reference to make sure $ is jQuery.

2. The widget itself extends the base Widget class so its given the Upper Case name YouTube because this is how Crockford wants it so that’s how you WILL DO IT.  This is generally considered best practice when naming classes in JavaScript as opposed to regular objects.

Number 2. Engage. Or Init.

Obscure Star Trek TNG reference out the way, we need to provide the init method.  This method will be called by the framework when the Widget is initialized.  This init function takes 2 parameters.  The first one is the element on which you are initializing the widget.  The second is a set of options that we are going to specify shortly.  These will be configuration values.

Override The Init Method

var YouTube = Widget.extend({

  init: function(element, options) {
      
    // base call to initialize widget
    Widget.fn.init.call(this, element, options);
  
  }

});

The call to the base is what translates your widget in from declarative initialization or jQuery Initialization; and merges all the base options (if you are extending a widget) and custom options.

Speaking of options, we’re going to need to declare those right under the init.  Anything that you declare in the options object will be available for the user to pass as either a configuration value, or a data attribute.

Specifying Default Options

var YouTube = Widget.extend({

  init: function(element, options) {
      
    // base call to initialize widget
    Widget.fn.init.call(this, element, options);
  
  },
  
  options: {
    
    // the name is what it will appear as off the kendo namespace(i.e. kendo.ui.YouTube). 
    // The jQuery plugin would be jQuery.fn.kendoYouTube.
    name: "YouTube",
    
    // other options go here
    ...
  }

});

Sweet.  Lastly but not leastly we add the widget to Kendo UI.  Here is the full boilerplate for creating your own Kendo UI widget and making it available like all other Kendo UI widgets are.

Kendo UI Widget Boilerplate

(function($) {
    
  // shorten references to variables. this is better for uglification 
  var kendo = window.kendo,
      ui = kendo.ui,
      Widget = ui.Widget

  var YouTube = Widget.extend({

    init: function(element, options) {

      // base call to widget initialization
      Widget.fn.init.call(this, element, options);

    },

    options: {       
      // the name is what it will appear as off the kendo namespace(i.e. kendo.ui.YouTube). 
      // The jQuery plugin would be jQuery.fn.kendoYouTube.
      name: "YouTube",
      // other options go here
      ....
    }

  });

  ui.plugin(YouTube);

})(jQuery);

Some Best Practices

It’s nice to adhere to some conventions when creating a plugin for Kendo UI and we kindly ask that you do a few of these small things when creating plugins so that your plugin looks just like our plugins under the covers.

  1. Class names are caps.
  2. If you are going to be using this < 3 times, stick with this. Otherwise, make it var that = this. This is for maximum minification.
  3. private variables and methods being with _.  This is typical and you can find it everywhere in jQuery.  This just denotes a method or property that you should not touch because it’s not meant to be manipulated outside of the Widget.
  4. Try to get all methods out of the init.  It’s tempting to put all your code there, but we try and keep those methods as skinny as possible.

A Widget's Widget

Now that we have gone over the basics, lets look at this Kendo UI YouTube widget.

This is a composite widget.  That simply means that it’s a widget that contains other widgets.  Or a widget inside of a widget. Queue the “Yo Dawg” meme.

This widget contains the AutoComplete which is wired up to Google Suggest API via the Kendo UI DataSource.  When an item is selected in that AutoComplete, a ListView which is bound to the YouTube Search API is refreshed and a pager is attached to the same DataSource.  In the event that a no text is entered in the search box, the ListView and Pager are not displayed.

The widget additionally takes in some configuration attributes:

  • placeholder(string, default: “Search YouTube”) – The watermark text to display when the AutoComplete is empty.
  • template(string, default: internal template) – The template to be used to display the results in the ListView. If not template is provided, a default internal one will be used instead.

We could add more, but this is what it takes for the time being.  In the example above, I am specifying a custom template that returns a video in a Kendo UI Window whenever an item thumbnail is clicked.

In the init method of my widget, I am creating the elements that make up the widget and adding them to the DOM.  I also create the DataSources that are used to connect to Google Suggest and YouTube JSONP API’s.

All methods are moved outside of the init to keep that method as lean as possible.  In my case, its already much larger than what you would find in the standard Kendo UI Widget.

One of the more interesting things about this widget, is that is uses the lesser known parse method on the DataSource to manipulate the results into more valid JSON before the DataSource actually consumes and internally maps them.  For instance, the Google Suggest API returns results as an array of arrays instead of objects.  Which is less than desirable.  Using the parse method on the DataSource, I can enumerate through the array and compose an object of arrays then return that new object as the data.  This is quite a powerful piece of the Kendo UI DataSource as it allows you to have complete control over the structure of the returning data.

Use DataSource Parse Method To Parse Response

// parse the google suggest results before the dataSource gets them
_suggestParse: function(data) {
  return $.map(data[1], function(item) {
    return { value: item[0] }
  });
},

 

The other thing I use on the DataSource is the parameterMap on the transport which allows me to add parameters to the outgoing request before it gets sent.  This is how I can grab the value off of the AutoComplete before I head off to Google Suggest for some, well, suggestions.

The neat thing about the Google Suggest API, is that is auto corrects you as well.  Try searching for “Taylro Swift”, and notice that it fixes it for you in the returned result.  Pretty nifty.  Here is the widget with a custom template and a Kendo UI Window to boot for showcasing the video.

And here is the complete code for the entire widget.

Complete YouTube Widget Code

(function($, kendo) {

  // shorten references to variables. this is better for uglification
  var ui = kendo.ui,
      Widget = ui.Widget;

  var YouTube = Widget.extend({

    // method called when a new kendoYouTube widget is created
    init: function(element, options) {
      var that = this,
        id,
        _autoComplete,
        _listView,
        _pager,
        plugin;

      // base call to initialize widget
      Widget.fn.init.call(that, element, options);

      // append the element that will be the auto complete
      _autoComplete = $("<input style='width: 100%; font-size: 1.5em;' />");
      that.element.append(_autoComplete);

      // append the element that will be the pager
      _pager = $("<div class='k-pager-wrap' style='display: none'><div class='k-pager'></div></div>");
      that.element.append(_pager);

      // append the element that will be the list view
      _listView = $("<div></div>");
      that.element.append(_listView);

      // the google suggest datasource
      that.suggest = new kendo.data.DataSource({
        transport: {
          read: {
            url: "http://clients1.google.com/complete/search",
            dataType: "jsonp"
          },
          parameterMap: function() {
            return that._suggestParameterMap.call(that);
          }
        },
        schema: {
          parse: function(data) {
            return that._suggestParse.call(that, data);
          }
        },
        serverFiltering: true
      });

      // create the auto complete
      that.autoComplete = _autoComplete.kendoAutoComplete({
        dataSource: that.suggest,
        placeholder: that.options.placeholder,
        suggest: true,
        minLength: 3,
        dataTextField: "value",
        template: "<span>#= data.value #</span>",
        change: function(e) {
          that._search(that, e);
        }
      }).data("kendoAutoComplete");

      // youtube datasource
      that.youtube = new kendo.data.DataSource({
        transport: {
          read: {
            url: "http://gdata.youtube.com/feeds/api/videos?max-results=10&v=2&alt=jsonc",
            dataType: "jsonp"
          },
          parameterMap: function(options) {
            return that._ytParameterMap.call(that, options);
          }
        },
        schema: {
          data: "data",
          parse: function(data) {
            return that._ytParse.call(that, data);
          },
          total: function(data) {
            return that._ytTotal.call(that, data);
          }
        },
        pageSize: 10,
        serverPaging: true
      });

      // results listview
      that.listView = _listView.kendoListView({
        autoBind: false,
        dataSource: that.youtube,
        template: that.options.template
      }).data("kendoListView");

      // remove the border from the listview
      _listView.css("border-width", "0");

      // pager widget
      that.pager = _pager.kendoPager({
        dataSource: that.youtube
      }).data("kendoPager");
    },

    // options that are avaiable to the user when initializing the widget
    options: {
      name: "YouTube",
      template: "<div style='padding: 10px;'>" + "<div style='float: left;'>" + "<a href='${player.default}' target='_blank'>" + "<img height='90' width='120' src='${thumbnail.sqDefault}' alt='thumbnail' />" + "</a>" + "</div>" + "<div style='margin-left: 130px; height: 90px;'>" + "<h4 style='margin: 2px'>" + "<a href='${player.default}' target='_blank'>${title}</a>" + "</h4>" + "<div style='font-size: .8em'>" + "<p>${description}</p>" + "</div>" + "</div>" + "</div>",
      placeholder: "Search YouTube"
    },

    // parse the return JSON from YouTube before the dataSource processes it
    _ytParse: function(data) {
      var result = {
        count: data.data.totalItems,
        data: []
      };
      result.data = $.map(data.data.items, function(item) {
        item.description = item.description || "";
        item.description = item.description.length > 100 ? (item.description.substring(0, 100) + "...") : item.description;
        return item;
      });
      return result;
    },

    // map parameters on the requests
    _ytParameterMap: function(data) {

      var that = this;

      return {
        // the q is set to the current value of the autoComplete
        q: that.autoComplete.value(),
        // the start index dictates paging
        "start-index": data.skip === 0 ? 1 : data.skip
      };
    },

    // get the total number of records off of the response for the pager
    _ytTotal: function(data) {

      var that = this;

      data.count = data.count || 0;
      if (data.count > 0) {
        that.pager.element.show();
        return data.count;
      } else {
        // if there are no records, hide the pager and listview
        that.pager.element.hide();
        that.listView.element.hide();
      }
    },

    // parse the google suggest results before the dataSource gets them
    _suggestParse: function(data) {
      return $.map(data[1], function(item) {
        return {
          value: item[0]
        };
      });
    },

    // map parameters for the google suggest request
    _suggestParameterMap: function() {

      var that = this;

      return {
        // the q value is the autocomplete current value
        q: that.autoComplete.value(),
        // get suggest results for youtube only
        client: "youtube",
        nolabels: 't'
      };
    },

    _search: function(e) {

      var that = this;

      if (that.autoComplete.value().length > 0) {
        // read the remote source
        that.listView.dataSource.read();
        // show the pager and listview if they are hidden
        that.pager.element.show();
        that.listView.element.show();
      } else {
        that.listView.element.hide();
        that.pager.element.hide();
      }
    }
  });

  ui.plugin(YouTube);

}()); 

What Will You Build?

I’m very interested to see what sort of plugins and composites people will build with Kendo UI.  This YouTube Widget can be placed on any site running Kendo UI and you will have embedded YouTube search right on your site.  In fact, I added it to my own site earlier this week.

Furthermore, you can use your custom widgets with declarative initialization or the standard jQuery initialization and you don’t have to do any extra work.  It’s all handled for you.

Kendo UI is designed to be not only a complete end-to-end HTML5 framework, but one that you can easily extend and composite should you find the need to.  If you haven’t already, download Kendo UI today and get started building custom plugins and widgets.  Maybe you will build the next Raptorize


Burke Holland is the Director of Developer Relations at Telerik
About the Author

Burke Holland

Burke Holland is a web developer living in Nashville, TN and the Director of Developer Relations at Telerik. He enjoys working with and meeting developers who are building mobile apps with jQuery / HTML5 and loves to hack on social API's. Burke works for Telerik as a Developer Advocate focusing on Kendo UI.

Related Posts

Comments

Comments are disabled in preview mode.