Last week I wrote an article showing you how to create your own custom Kendo UI plugin. I gave you some boilerplate code and went over the basics of creating a simple plugin. I also provided a sample plugin for you that was a composite of the AutoComplete, ListView and Pager.
This week I'm in Sofia with the Kendo UI team, and I've sat down and taken a much closer look at the anatomy and lifecycle of a widget. While the code that I gave you last time gives you enough to create your own Widget, I wanted to follow that up with a post about how to create a widget that is DataSource aware. One that is closer to how an actual Kendo UI widget is built and will give you a solid understanding of what you need to do to create your own DataSource aware widgets, as well as more best practices so your code is maintainable, scalable and pretty. Well, the pretty part is up to you but hopefully this will put you on the right track.
As a quick recap of the last post, I gave you some boilerplate code that is the least amount of code that you would need to write a widget. For the sake of a good starting point, here is that boilerplate code again.
(function($) { // shorten references to variables. this is better for uglification var kendo = window.kendo, ui = kendo.ui, Widget = ui.Widget var MyWidget = 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.MyWidget). name: "MyWidget", // other options go here .... } }); ui.plugin(MyWidget); })(jQuery);
That gives you a basic widget that you can initialize either declaratively, or directly.
Now what if you wanted to make this Widget DataSource or MVVM aware? There are some additional items that you need to implement. Instead of looking at all that here, I will go over how to create a DataSource aware widget, and I'll tackle the MVVM part next time by building on what I show you here. As usual, I'll give you a finished widget. It's going to be a very simple widget that just repeats the data in the DataSource and also allows you to specify your own custom template. You can think of this as an extremely dumbed down ListView. I'm going to call it the Repeater for this post.
We'll build of the boilerplate code from the last post. This boilerplate simply initializes the widget and adds it the the kendo namespace.
To make our widget aware of a DataSource, the first thing that we need to do is to use the create convenience method on the DataSource base object.
that.dataSource = kendo.data.DataSource.create(that.options.dataSource);
What this line does is offer you quite a bit of flexibility in the way that you initialize the DataSource for your widget. Should you actually create a new DataSource either outside your widget initialization or inline, that DataSource will be returned. But you don't have to create a new DataSource to bind a widget. You could simply set it's DataSource to an array. Something like this.
$("#div").kendoRepeater({ dataSource: ["Item 1", "Item 2", "Item 3"] });
If you pass this simple array, the kendo.data.DataSource.create method will create a new DataSource for you based upon the data in this array and return it to that.dataSource. But you can also create a DataSource just by specifying it's configuration values inline.
$("#div").kendoRepeater({ dataSource: { transport: { read: { url: "http://mydomain/customers" } } } });
Here I am specifying a DataSource configuration, but not actually creating an instance of one. The kendo.data.DataSource.create(that.options.dataSource) will take this configuration object and return you a new DataSource instance with the specified configuration.
Now you have provided the same flexibility that we do in our own widgets as far as the variety of ways that you can specify the dataSource for this repeater.
The next thing that you need to do is to bind to your DataSource change event and handle it. This is where you will mutate your DOM based on the Data read from the DataSource. We typically do this in a refresh method. We usually make the refresh method public, because there is a high probability that you or someone else may want to call that method on the widget at some point after initialization.
// bind to the change event to refresh the widget that.dataSource.bind("change", function() { that.refresh(); });
The boilerplate code now looks like this.
(function($) { var kendo = window.kendo, ui = kendo.ui, Widget = ui.Widget, CHANGE = "change"; var Repeater = kendo.ui.Widget.extend({ init: function(element, options) { var that = this; kendo.ui.Widget.fn.init.call(that, element, options); // initialize or create dataSource that._dataSource(); }, options: { name: "Repeater" }, _dataSource: function() { // returns the datasource OR creates one if using array or configuration that.dataSource = kendo.data.DataSource.create(that.options.dataSource); // bind to the change event to refresh the widget that.dataSource.bind(CHANGE, function() { that.refresh(); }); } }); kendo.ui.plugin(Repeater); })(jQuery);
Notice that when you bind to the change event on the DataSource, you are really binding to the string value of "change". As a best practice, we assign these as constants at the top of the widget and then refer to the constant. I also moved the entire DataSource configuration into it's own method which I just execute. This is because that will be the widget since it is the calling object. I can reference all of the widget properties off of the that object after assigning that to this.
We need to add one more thing to the _dataSource method, and this will be to fetch from the DataSource if it's necessary. We do that by checking for the autoBind configuration value off of that.options. Then we call that.dataSource.fetch(). It's important to note that a fetch is different from a read in that it will only populate the DataSource if the DataSource has not yet been read from. If a read has previously been called on the DataSource before the widget was initialized, we will not be causing the DataSource to read again.
_dataSource: function() { var that = this; // returns the datasource OR creates one if using array or configuration that.dataSource = kendo.data.DataSource.create(that.options.dataSource); // bind to the change event to refresh the widget that.dataSource.bind(CHANGE, function() { that.refresh(); }); // trigger a read on the dataSource if one hasn't happened yet if (that.options.autoBind) { that.dataSource.fetch(); } }
The autoBind configuration option doesn't exist yet, so lets add it to the options object on the widget and give it a default value of true. All DataBound widgets in Kendo UI do autoBind by default.
options: { name: "Repeater", autoBind: true }
HTML output by widgets is rendered using Kendo UI Templates. Kendo UI Templates allow you to precompile HTML and inject data or expressions into the HTML which are evaluated and a DOM fragment is returned as an HTML string. Nearly all widgets in Kendo UI allow you to specify some kind of template in addition to the default template that a widget uses. To do this, we need to first add the template to the options object and set it's value to an empty string. Contrary to other configuration settings, we won't set its default value here.
options: { name: "Repeater", autoBind: true, template: "" }
To set the default value, add a line directly under the call to the base widget initialization. This will precompile the template passed in by the user, or use a default template. In the case of this simple repeater, I'm just going to write out strong tags wrapped in a paragraphs and then reference the data object, which will be a string if we pass an array of strings. If I pass objects to the DataSource, the default template will render [object Object].
that.template = kendo.template(that.options.template || "<p><strong>#= data #</strong></p>")
Since we bound to the change method, we need to implement the refresh public function that will be called when the DataSource changes or when it's called directly. Inside the refresh method is where I am going to mutate the DOM. The first thing to do is to call that.dataSource.view() which gives us the data from the DataSource. Next, we use kendoRender and pass in a template along with the DataSource data (AKA view). This is how Kendo UI widgets mutate the DOM. The render method applies the data to the template and returns the html string.
refresh: function() { var that = this, view = that.dataSource.view(), html = kendo.render(that.template, view); }
Lastly we simply set the HTML of that.element, which is the element on which we are initializing our widget. In the case that you are handling initialization on an input and you want to translate or wrap that input with a container, you would need to add that logic here before setting it's html. The that.element is a jQuery wrapped element, so we can simply call the html method directly off of it. The final refresh method looks like so:
refresh: function() { var that = this, view = that.dataSource.view(), html = kendo.render(that.template, view); that.element.html(html); }
And with that final touch, we officially have a fully DataBound widget. Here is the complete code for the Repeater Widget.
(function() { var kendo = window.kendo, ui = kendo.ui, Widget = ui.Widget, CHANGE = "change"; var Repeater = Widget.extend({ init: function(element, options) { var that = this; kendo.ui.Widget.fn.init.call(that, element, options); that.template = kendo.template(that.options.template || "<p><strong>#= data #</strong></p>"); that._dataSource(); }, options: { name: "Repeater", autoBind: true, template: "" }, refresh: function() { var that = this, view = that.dataSource.view(), html = kendo.render(that.template, view); that.element.html(html); }, _dataSource: function() { var that = this; // returns the datasource OR creates one if using array or configuration object that.dataSource = kendo.data.DataSource.create(that.options.dataSource); // bind to the change event to refresh the widget that.dataSource.bind(CHANGE, function() { that.refresh(); }); if (that.options.autoBind) { that.dataSource.fetch(); } } }); ui.plugin(Repeater); })(jQuery);
Here is demonstration of it in action. There are two widgets initialized here. The first one takes uses a simple array as a DataSource. The second uses a remote endpoint, a template, and declarative initialization.
Download Kendo UI and you can begin building your own custom DataBound Widgets. As usual, you can find this code and examples on the Kendo UI Labs GitHub repo.
Next time I'll expand upon this widget and make the repeater MVVM aware.
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.