Custom Widgets
Kendo UI provides options for you to create your own widgets by inheriting from the base widget
class.
Getting Started
-
Extend the base Kendo UI widget class in the
kendo.ui
namespace.The following example demonstrates how to create variables to hold values which also help with the minification. The entire process is wrapped in a self-executing anonymous function, so as to protect the global namespace. jQuery is passed in as a reference to make sure
$
is jQuery. The widget itself extends the base widget class so it is given the uppercase name ofMyWidget
—or whatever the name of your widget is for that matter. This is generally considered a best practice when naming classes in JavaScript as opposed to regular objects.(function($) { // Shorten references to variables which is better for uglification. kendo = window.kendo, ui = kendo.ui, Widget = ui.Widget var MyWidget = Widget.extend({ // The initialization code goes here. }); })(jQuery);
-
Provide an
init
method for your widget. This method is called by the framework when the widget is initialized. Thisinit
function takes two parameters. The first one is the element on which you are initializing the widget. The second one is a set of options that you are going to specify shortly. These will be configuration values.var MyWidget = Widget.extend({ init: function(element, options) { // The base call to initialize the widget. Widget.fn.init.call(this, element, options); } });
-
If you are extending a widget, the call to the base is what translates your widget from declarative initialization or the standard imperative initialization and merges all the base options and custom options. Declare those options right under the
init
statement. Anything that you declare in the options object will be available for the user to pass as either a configuration value or as a data attribute.var MyWidget = Widget.extend({ init: function(element, options) { // The base call to initialize the widget. Widget.fn.init.call(this, element, options); }, options: { // The name is what it will appear as the kendo namespace(i.e. kendo.ui.MyWidget). // The jQuery plugin would be jQuery.fn.kendoMyWidget. name: "MyWidget", // Other options go here. ... } });
-
Add the widget to Kendo UI. The following example demonstrates the full boilerplate for creating your own Kendo UI widget and making it available like all other Kendo UI widgets are.
(function($) { // Shorten the 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) { // The base call to the widget initialization. Widget.fn.init.call(this, element, options); }, options: { // The name is what it will appear as the kendo namespace(i.e. kendo.ui.MyWidget). // The jQuery plugin would be jQuery.fn.kendoMyWidget. name: "MyWidget", // Other options go here. .... } }); ui.plugin(MyWidget); })(jQuery);
-
To make this widget DataSource- or MVVM-aware, implement some additional items. The following section discusses the process of creation a DataSource-aware widget. The MVVM part is tackled later on in this article. The widget that is demonstrated is a simple one that just repeats the data in the DataSource and also allows you to specify your own custom template. You can regard it as an extremely dumbed-down ListView which, for an easier handling, is named the Repeater.
To make your widget aware of a Data Source, use the created convenience method on the Data Source base object. The code snippet offers flexibility in the way you initialize the DataSource for your widget. If you actually create a new DataSource either outside your widget initialization or inline, that DataSource is returned.
that.dataSource = kendo.data.DataSource.create(that.options.dataSource);
-
Create a new DataSource to bind the widget. This step is not a must because you can set the DataSource to an array as demonstrated in the following example. If you pass this array, the
kendo.data.DataSource.create
method will create a new DataSource based on the data in this array and returns it tothat.dataSource
.$("#div").kendoRepeater({ dataSource: ["Item 1", "Item 2", "Item 3"] });
-
Create a DataSource by specifying its configuration values inline as demonstrated in the following example. The example specifies a DataSource configuration but does not actually create a DataSource instance. The
kendo.data.DataSource.create(that.options.dataSource)
takes this configuration object and returns a new DataSource instance with the specified configuration.To replicate the Kendo UI MultiSelect data-binding behavior, explicitly assign the
kendo.data.binders.widget.multiSelectCustom = kendo.data.binders.widget.multiselect;
binding.$("#div").kendoRepeater({ dataSource: { transport: { read: { url: "http://mydomain/customers" } } } });
Handling Events
-
Bind to your DataSource a
change
event and handle it. This is where you mutate your DOM based on the data read from the DataSource. Typically, this is done in arefresh
method. Make it public, so that you or someone else is able to call it 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 way the widget code now looks is demonstrated in the following example. Note that when you bind to the
change
event on the DataSource, you actually bind to the string value of"change"
. As a best practice, assign these as constants at the top of the widget and then refer to the constant. The entire DataSource configuration is also moved into its own method. This is becausethat
signifies the widget since it is the calling object. You can reference all widget properties off of thethat
object after assigningthat
tothis
.(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() { var that = this; // returns the datasource OR creates one if using an array or a 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);
<!--_-->
-
Fetch from the DataSource (if necessary) by checking for the
autoBind
configuration value ofthat.options
. Then, callthat.dataSource.fetch()
. Note thatfetch
is different fromread
because it only populates the DataSource if the DataSource is not yet read from. If aread
is previously called on the DataSource before the widget is initialized, you are not to cause 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 read on the dataSource if one has not happened yet. if (that.options.autoBind) { that.dataSource.fetch(); } }
-
Add the
autoBind
configuration to theoptions
object on the widget and give it a default value oftrue
. All data-bound widgets in Kendo UI doautoBind
by default.options: { name: "Repeater", autoBind: true }
Rendering Widgets with Templates
-
The HTML that is output by widgets is rendered over the Kendo UI templates. They allow you to pre-compile HTML and inject data or expressions, which are evaluated, into the HTML 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, first add the template to the
options
object and set its value to an empty string. Contrary to other configuration settings, do not set its default value here.options: { name: "Repeater", autoBind: true, template: "" }
-
Set the default value by adding a line directly under the call to the base widget initialization. This pre-compiles the template passed in by the user, or uses a default template. In the case of this Repeater, write out
strong
tags wrapped in a paragraphs and then reference thedata
object, which will be a string if we pass an array of strings. If you pass objects to the template, the default template renders[object Object]
.that.template = kendo.template(that.options.template || "
#= data #
")
Implementing Refresh Functions
-
Since you bound to the
change
method, you need to implement therefresh
public function that will be called when the DataSource changes or when it is called directly. Inside therefresh
method is where you are going to mutate the DOM. First, callthat.dataSource.view()
, which gives you the data from the DataSource. Next, usekendoRender
and pass in a template along with the DataSource data—a.k.a.view
. This is how Kendo UI widgets mutate the DOM. Therender
method applies the data to the DataSource and returns the HTML string.refresh: function() { var that = this, view = that.dataSource.view(), html = kendo.render(that.template, view); }
-
Set the HTML of
that.element
—the element on which you are initializing your widget. If you are handling initialization on aninput
and you want to translate or wrap thatinput
with a container, add that logic here before setting its HTML. Thethat.element
is a jQuery wrapped element, so you can simply call thehtml
method directly off of it. The final refresh method looks like the one demonstrated in the example below.refresh: function() { var that = this, view = that.dataSource.view(), html = kendo.render(that.template, view); that.element.html(html); }
-
Having added the final touches from the previous step, now you have a fully data-bound widget. The following example demonstrates 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);
The following example uses two widgets that are initialized. The first one takes a simple array as a DataSource. The second uses a remote endpoint, a template, and declarative initialization.
<div id="repeater"></div> <div id="container"> <div data-role="repeater" data-source="dataSource" data-template="template"></div> </div> <script type="text/x-kendo-template" id="template"> <div style="float: left; color: salmon; margin-right: 10px"><h1>#= data.ProductName #</h1></div> </script> <script> (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); var dataSource = new kendo.data.DataSource({ type: "odata", transport: { read: "https://demos.telerik.com/kendo-ui/service/Northwind.svc/Products" } }); kendo.bind($("#container")); $("#repeater").kendoRepeater({ dataSource: [ "item1", "item2", "item3" ] }); </script>
Using MVVM
-
To make this widget MVVM-aware, you need to define some events. Specifically, expose the
dataBinding
event and thedataBound
event. ThedataBinding
event is what you are going to call before you mutate the DOM with your widget. This gives MVVM a chance to traverse the fragment that you are about to mutate, and unbind anything that is currently bound. The second event is thedataBound
event, which allows MVVM to go back through the fragment, and re-bind what is necessary. These events are exposed via theevents
object on the widget. These events are strings, so define them as constants in the head of the widget as part of the pattern Kendo UI uses when developing Kendo UI widgets.By exposing these as events for MVVM to listen to, you have loose coupling between your widget and the MVVM core engine. This means that if you do not expose these events, MVVM will not be aware of the lifecycle of the widget. This is a very good architecture as it ensures that your widget will not break other MVVM bindings of which it is not aware.
var DATABINDING = "dataBinding", DATABOUND = "dataBound", CHANGE = "change" var Repeater = kendo.ui.Widget.extend({ init: function(element, options) { ... }, options{ ... }, // The events are used by other widgets or developers - API for other purposes. // These events support MVVM bound items in the template for loose coupling with MVVM. events: [ // Call before mutating DOM. // MVVM will traverse DOM, unbind any bound elements or widgets. DATABINDING, // Call after mutating DOM. // Traverses DOM and binds ALL THE THINGS. DATABOUND ] });
-
MVVM expects you to expose the DOM fragments from your widget, which represents each row or each repeated data element. You must return the outermost element for MVVM to work with. While it varies, this is typically just
this.element.children
. Since each template item rendered is a DOM fragment, attached to the bound element, this is all you need. Expose it for MVVM by making it available off of the items object.var DATABINDING = "dataBinding", DATABOUND = "dataBound", CHANGE = "change" var Repeater = kendo.ui.Widget.extend({ init: function(element, options) { ... }, options{ ... }, // The events are used by other widgets or developers - API for other purposes. // These events support MVVM bound items in the template. for loose coupling with MVVM. events: [ // Call before mutating DOM. // MVVM will traverse DOM, unbind any bound elements or widgets. DATABINDING, // Call after mutating DOM. // Traverses DOM and binds ALL THE THINGS. DATABOUND ], // MVVM expects an array of DOM elements that represent each item of the datasource. // Has to be the children of the outermost element. items: function() { return this.element.children(); } });
-
Since it is possible to change the DataSource by using MVVM, you need to implement the
setDataSource
function. MVVM calls this when the DataSource is set inside a ViewModel. Set your internal DataSource reference equal to the one passed in by MVVM and then rebuild the DataSource using the already defineddataSource()
function.// For supporting changing the datasource over MVVM. setDataSource: function(dataSource) { // Set the internal datasource equal to the one passed in by MVVM. this.options.dataSource = dataSource; // Rebuild the datasource if necessary or just reassign. this._dataSource(); }
-
You need to make some small tweaks to your method which assigns or builds the DataSource. The
_dataSource
method that you call ininit
does 3 things:- Assigns the DataSource, or builds on from an array or configuration object.
- Reads from the DataSource, if
autoBind
is enabled, and the DataSource is not yet read from. - Binds the
change
event on the DataSource to an internalrefresh
method that you handle manually.
Since you have already bound the change event on the DataSource possibly once, make sure you unbind it if necessary. If this is not done, the widget retains a list of all bindings and executes the
refresh
function numerous times. Also, MVVM will be listening to the internal_refreshHandler
function which is not yet defined. You need to point the internal_refreshHandler
to your publicly exposedrefresh
method. First though, check and see if there is an existing connection between the publicrefresh
, which is bound to the change event on the DataSource, and the internal_refreshHandler
. If there is, remove just the binding to thechange
event. If there is no connection between your internal_refreshHandler
and the publicrefresh
function, you need to create it. This is done by the$.proxy
jQuery method, which calls the publicrefresh
with the correct context, which is the widget itself. Finally, rebind to thechange
event of the DataSource.The following can be a bit confusing if you have not used the
proxy
jQuery function before, but all it is doing is saying that when the_refreshHandler
is called, it should execute the publicrefresh
widget function and inside thatrefresh
function, this will be a reference to the widget itself, and not something else, such as window. Due to the fact that the the value of the keywordthis
is always changing in JavaScript, this is a good way to ensure that the scope is correct when therefresh
function executes._dataSource: function() { var that = this; // If the DataSource is defined and the _refreshHandler is wired up, unbind because // you need to rebuild the DataSource. if ( that.dataSource && that._refreshHandler ) { that.dataSource.unbind(CHANGE, that._refreshHandler); } else { that._refreshHandler = $.proxy(that.refresh, that); } // 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, that._refreshHandler ); if (that.options.autoBind) { that.dataSource.fetch(); } }
-
Trigger the
dataBinding
anddataBound
events in the publicrefresh
. Note thatdataBinding
happens before you mutate the DOM anddataBound
happens directly after that.refresh: function() { var that = this, view = that.dataSource.view(), html = kendo.render(that.template, view); // Trigger the dataBinding event. that.trigger(DATABINDING); // Mutate the DOM (AKA build the widget UI). that.element.html(html); // Trigger the dataBound event. that.trigger(DATABOUND); }
Now, you have a fully enabled MVVM in your widget. Define the widget as demonstrated in the example below.
<div data-role="repeater" data-bind="source: dataSource"></div> <script> var viewModel = kendo.observable({ dataSource: new kendo.data.DataSource({ transport: { read: "Customers/Orders", dataType: "json" } }) }); kendo.bind(document.body.children, viewModel); </script>
Notice that the widget is now bound to the
dataSource
variable inside of the ViewModel viadata-bind
. This means that if we add an item client-side to the DataSource, your widget will reflect the change immediately, without you having to re-render anything.In the following complete example, note that when you add an item to the DataSource, it is immediately reflected in the Repeater widget.
```dojo <label for="newItem">Enter A New Item</label> <input id="newItem" data-bind="value: newItem" class="k-input" /> <button class="k-button" data-bind="click: add">Add Item</button> <div data-role="repeater" data-bind="source: items" data-template="template"></div> <script type="text/x-kendo-template" id="template"> <div style="color: salmon; margin-right: 10px'"><h1>#= data #</h1></div> </script> <script> var viewModel = kendo.observable({ items: ["item1", "item2", "item3"], newItem: null, add: function(e) { if (this.get("newItem")) { this.get("items").push(this.get("newItem")); } } }); kendo.bind(document.body, viewModel); </script> ```
Working with Value-Bound Widgets
In order for a widget to support value
binding, you need to:
- Add a
value
method to the widget, which sets the current widget value and returns the current value if no arguments are passed. - Trigger the widget change event when the widget value is changed.
The following examples demonstrate how to create a simple input widget that selects the value on focus.
-
Create the widget and implement the functionality that you are looking for.
(function ($) { var kendo = window.kendo; var SelectedTextBox = kendo.ui.Widget.extend({ init: function (element, options) { kendo.ui.Widget.fn.init.call(this, element, options); this.element.on("focus", this._focus); }, options: { name: "SelectedTextBox" }, _focus: function () { this.select(); }, destroy: function () { this.element.off("focus", this._focus); } }); kendo.ui.plugin(SelectedTextBox); })(jQuery);
-
Add a
value
method.var SelectedTextBox = kendo.ui.Widget.extend({ ... value: function (value) { if (value !== undefined) { this.element.val(value); } else { return this.element.val(); } } });
-
Trigger the
change
event.var SelectedTextBox = kendo.ui.Widget.extend({ init: function (element, options) { ... this._changeHandler = $.proxy(this._change, this); this.element.on("change", this._changeHandler); }, ... _change: function () { this.trigger("change"); }, destroy: function () { this.element.off("change", this._changeHandler); this.element.off("focus", this._focus); } });
The following example combines the snippets and exhibits the full code.
<script>
(function ($) {
var kendo = window.kendo;
var SelectedTextBox = kendo.ui.Widget.extend({
init: function (element, options) {
kendo.ui.Widget.fn.init.call(this, element, options);
this._changeHandler = $.proxy(this._change, this);
this.element.on("change", this._changeHandler);
this.element.on("focus", this._focus);
},
options: {
name: "SelectedTextBox"
},
_change: function () {
this._value = this.element.val();
this.trigger("change");
},
_focus: function () {
this.select();
},
value: function (value) {
if (value !== undefined) {
this.element.val(value);
} else {
return this.element.val();
}
},
destroy: function () {
this.element.off("change", this._changeHandler);
this.element.off("focus", this._focus);
}
});
kendo.ui.plugin(SelectedTextBox);
})(jQuery);
</script>
<input type="text" data-role="selectedtextbox" data-bind="value:foo" />
<script>
var viewModel = kendo.observable({
foo: "bar"
});
kendo.bind(document.body, viewModel);
</script>
Technical Support
Custom widgets that inherit from Kendo UI widgets are not subject to technical support service, unless the question or issue can be discussed in the context of the originating widget that is provided in the Kendo UI installer.