One of the questions that I see with some frequency on the Twitters, Emails and general InterWebs is:
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.
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.
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.
<div id="youtube"></div> <script> $("#youtube").kendoYouTube(); </script>
<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.
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.
(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);
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.
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.
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.
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.
(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);
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.
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:
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.
// 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.
(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); }());
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 a web developer living in Nashville, TN and was the Director of Developer Relations at Progress. He enjoys working with and meeting developers who are building mobile apps with jQuery / HTML5 and loves to hack on social API's. Burke worked for Progress as a Developer Advocate focusing on Kendo UI.