I had the privilege this week of attending the DevReach conference in Sofia, Bulgaria. For those of you who are not already familiar, Telerik (the company which makes Kendo UI) originated in Bulgaria. There are offices here where you will find engineers, product managers, marketing, sales, R&D and virtually any other function you can think of.
Sofia is a beautiful city and I always enjoy coming here - especially for DevReach. I was able to talk to quite a few folks during the time of the conference and I always get some great feedback on Kendo UI. One developer that I talked to asked me how to write "controls" using Kendo UI that have features that can be expanded on or used as is. This would be what is commonly known as having a base class where you can define some default properties, methods, etc. and then override those as desired.
I've written several articles about creating composite widgets with Kendo UI, A DataSource aware Kendo UI widget and MVVM aware Kendo UI Widgets.
The process for creating your own widget with encapsulated logic is the same as the first post that I wrote, but I had a fun time putting together a proof of concept for this blog post, so lets go over how to do this and create a unique widget that contains some pre-defined logic that you can then either use, or override as you see fit.
Everyone loves bootstraps these days, including myself. I love code that gets me up and running. I've listed this before, but lets go over again the fundamentals of what it takes to create a basic Widget that will be available off of the Kendo UI namespace.
(function($) { // shorten references to variables. this is better for uglification var kendo = window.kendo, ui = kendo.ui, Widget = ui.Widget var CustomWidget = Widget.extend({ init: function(element, options) { // assign that to this var that = this; // call the base function to create the 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.CustomWidget). // The jQuery plugin would be jQuery.fn.CustomWidget. name: "CustomWidget", // other options go here ... } }); ui.plugin(CustomWidget); })(jQuery);
The widget that I create here will be VERY loosely based on what the developer who I spoke to was asking me about.
It's a textbox, a label and a checkbox. The textbox has a default change event and so does the checkbox. These might get used over and over and you wouldn't want to have to specify the base functionality for these events every time you used one. In this tutorial, we will specify some base events and then expose the ability to expand on those features by inheriting from this widget. Here is what the basic widget looks like. Whatever you type in the textbox becomes the text of the label. Clicking the checkbox will toggle the background color.
Not terribly impressive, but we need a starting point. Here is the code that makes the complete widget. I'll be going through what all of this does.
The first step to recreating this is to build the UI. To do that, I specify a _templates object which contains the templates needed to create the 3 HTML components that make up this control.
_templates: { textbox: "<input class='k-textbox' placeholder='#: placeholder #' type='text' id='#: inputId #' value='#: labelId #'>", label: "<label for='#: inputId #' id='#: labelId #' style='display: inline-block; width: 200px; margin-left: 20px;'>#: labelText # </label>", checkbox: "<input type='checkbox' # if (data.checked) { # checked='checked' # } #>" }
You might have noticed that the templates are assuming we are going to pass some data in. This will ultimately be the options object. Lets add the necessary properties to the options object that our template will be expecting. Even if we aren't going to use it, we still need to specify it in the options object and at least give it a default value.
options: { name: "CustomInput", labelText: "Please Enter Your Name...", inputText: "", checked: false, placeholder: "Enter Name", toggleColor: "#9BF49D" },
Here's the breakdown of what they are all going to tell us.
The next thing that we need to do is to turn all of these templates into actual HTML UI components by passing the objects object through them. I extract all of this logic into a private _create function. We try to keep the init functions as skinny as possible, so extract all of your logic into private methods that are prefixed with an underscore.
We are also going to add default change events for the textbox and the checkbox.
// this function creates each of the UI elements and appends them to the element // that was selected out of the DOM for this widget _create: function() { // cache a reference to this var that = this; // set the initial toggled state that.toggle = true; // setup the label var template = kendo.template(that._templates.label); that.label = $(template(that.options)); // setup the textbox template = kendo.template(that._templates.textbox); that.textbox = $(template(that.options)); // setup the textbox change event. wrap it in a closure so that // "this" will be equal to the widget and not the HTML element that // the change function passes. that.textbox.change(function(e) { that._inputChange(); }); // setup the checkbox template = kendo.template(that._templates.checkbox); that.checkbox = $(template(that.options)); // setup the checkbox change event. wrap it in a closure to preserve // the context of "this" that.checkbox.change(function(e) { that._checkChange(); }); // append all elements to the DOM that.element.append(that.textbox) .append(that.label) .append(that.checkbox); }
Lexical scoping means that a function remembers its values even after it executes. In JavaScript, you can reset a variable's value outside of a function and the value of the variable will be changed inside the function as well. This can be really frustrating. The same laws apply to the this object. There are certain tricks that developers have come up with to make sure that the variable values are preserved. One of these ways is to use a closure.
Notice that each of the change events is wrapped in a function (or closure). This is so that I can pass that as the context of this in the function. There are several tricks for setting the context of this. Using closures is one way, but be sure to check out the jQuery $.proxy method and the JavaScript call and apply functions.
Now that this much is done, we can create the widget by calling the _create method in the init function.
init: function(element, options) { // assign that to this to reduce scope confusion var that = this; // base widget initialization call Widget.fn.init.call(this, element, options); // creates the UI and appends it to the element selected from the DOM that._create(); }
Add default a event for the CheckBox which will change the background color. This is a private method, so prefix it with an _.
// the event that fires when the checkbox changes _checkChange: function() { var that = this; // check the toggle value if (that.toggle) { // change the background color to the specified one on the // options object that.element.css("background", that.options.toggleColor); } else { // toggle it back to white that.element.css("background", "#fff"); } // flip the toggle that.toggle = !that.toggle; }
Now the control will toggle the background color when you select and de-select the checkbox. Add a method for the textbox (or input) to change the label to whatever value is in the textbox.
// the event that fires when the textbox changes _inputChange: function() { var that = this; that.label.text(that.textbox.val()); }
At this point, you would have a basic widget that you could initialize by selecting a DOM element and calling kendoCustomInput
<div id="customInput"></div> <script> $("#customInput").kendoCustomInput(); </script>
When I initially wrote this widget, I was using options objects to pass in overriden events. In talking with the team, this is a bad idea. You should instead do one of two things.
We might want to toggle the background color AND disable the textbox when the checkbox changes. To do this, we need to create a new widget that extends the first one.
Create the new widget just like the first, but this time extend the CustomInput class. You will see that in order to call the base _checkChange method, you need to reference it off the prototype - also known as the base object.
// create a new widget which extends the first custom widget var CustomInputDisable = CustomInput.extend({ // every widget has an init function init: function (element, options) { // cache this var that = this; // initialize the widget by calling init on the extended class CustomInput.fn.init.call(that, element, options); }, // handle the _checkChange event. This is overriding the base event. _checkChange: function() { // cache the value of this var that = this; // disable the textbox first if (!that.textbox.is(":disabled")) { that.textbox.attr("disabled", "disabled"); } else { that.textbox.removeAttr("disabled"); } // this calls the base method from the CustomInput widget. If you didn't want // to call this, you would omit this line. then the textbox would be disabled, but // the background color would not change. CustomInput.prototype._checkChange.call(this); // call the base method }, // all options are inherited from the CustomInput widget. This just sets a new name // for this widget options: { name: "CustomInputDisable" } }); // add this new widget to the UI namespace. ui.plugin(CustomInputDisable);
You are probably going to want to take this further and look at using DataSources and MVVM. For that, I suggest reading my prior blog posts on how to make your widget DataSource Aware and MVVM Aware.
You can build your own widgets using Kendo UI, or simply encapsulate settings for a single widget. You may have a grid with some very extensive settings. Simply extend the grid base widget and set the default options. Now you have your own grid with your settings that you can re-use over and over without having to reconfigure.
Checkout the code from today's tutorial in the Plugins GitHub Repo.
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.