This is a migrated thread and some comments may be shown as answers.

Creating custom MVVM widgets - Part 1 - Options

6 Answers 294 Views
MVVM
This is a migrated thread and some comments may be shown as answers.
Scott
Top achievements
Rank 1
Scott asked on 13 Jan 2014, 05:41 PM
I have seen a variety of questions centered around creating custom MVVM widgets on this forum, and have not yet seen a good answer on how exactly to create a custom widget that is MVVM aware.  Mr. Holland did a nice job with a data-source aware widget that also binds to MVVM, but it only covers a datasource aware widget and doesn't extend into other bindings that you might want to support in your own custom widgets.  I wanted to post a series of tips I've discovered with creating custom widgets that are fully MVVM aware and that function in the same manner as the Kendo widgets themselves.

In this first post, I will show how to create widget that uses simple options that are defined either from data-someoption attributes, or within the { someoption: 'myvalue'} javascript declarations.   We'll create a widget that uses markup like:
<input data-role="SimpleWidget" data-someoption="myoption" />
In order to accomplish this, let's start with the boiler plate code for a simple widget extension.
(function($) {
var kendo = window.kendo,
   ui = kendo.ui,
    
   Widget = ui.Widget;
 
   var SimpleWidget= Widget.extend({
      // custom widget stuff
   });
})(jQuery);
With the boiler plate code, we can start adding in the functionality we'll need to support this widget within the KendoUI framework.  First, we should create an init function.  Kendo will call init during the creation of the widget.
// Kendo calls this method when a new widget is created
init: function (element, options) {
    var that = this;
    Widget.fn.init.call(this, element, options);
}
The init function is the entry point for KendoUI framework to initialize our widget.  We'll need to use this skeleton of a function and add in our DOM elements creation, event bindings to handle events within the widgets, and setup of any settings or options that we need to handle during creation.  The options variable is supplied to us by kendo and contains all of the options that are set as data-option value pairs or passed in via the options attribute in a javascript declaration.  If we had an option in markup such as data-someoption, we could reference it here as options.someoption.  This value could also be passed during javascript initialization as { someoption: 'somevalue' }.  In order for options to be read and available in the options collection, we have to declare them in a property called options within our widget.
//List of all options supported and default values
options: {
    name: "SimpleWidget",
    value: null,
    width: "150",
    someoption: ""
}
By exposing this options collection, now have access to these options in the options collection of our widget.  Options will automatically be set by the framework within our options collection without any action on our part.  Each option that is declared using a data-someoption="somevalue" attribute or in the javascript as an options: { someoption: "myvalue" } that also has a corresponding options: {} declaration in our widget will be available in the options collection in the init method, and the provided value will also automatically be updated within this.options in our widget.

We can show the use of the options with a little bit of simple jQuery to set the source elements value to equal the option's value.
init: function(element, options) {
  ...
      //Options are accessible via the options
      $(that.element).val(options.someoption);
      //Our internal widget's options property has also been updated for us
      $(that.element).val(that.options.someoption);
  ...
}
We can see that both the options parameter as well as our widget's options property have been set to the value specified in the markup's data-someoption attribute.  We would also see this value from a javascript options attribute if our widget was declared from code.

This fiddle provides a working example of widget options: http://jsfiddle.net/cn172/EuNLh/

The full source code for a very basic widget with simple options support:
(function ($) {
    var kendo = window.kendo,
        ui = kendo.ui,
        Widget = ui.Widget;
 
    var SimpleWidget = Widget.extend({
        // Kendo calls this method when a new widget is created
        init: function (element, options) {
            var that = this;
            Widget.fn.init.call(this, element, options);
            //Our internal widget's options property has been updated for us
            $(that.element).val(that.options.someoption);
        },
        //List of all options supported and default values
        options: {
            name: "SimpleWidget",
            value: null,
            width: "150",
            someoption: "",
        },
    });
    ui.plugin(SimpleWidget);
})(jQuery);

In the next post, I'll cover a basic MVVM binding, the value binding.  We'll see how MVVM sets the value of widgets with a data-bind="value: someViewModelProperty" markup and how to reflect that value on our UI.

6 Answers, 1 is accepted

Sort by
0
Scott
Top achievements
Rank 1
answered on 13 Jan 2014, 07:42 PM
In my previous post, I covered a basic widget creation with setting and use of options.  To start with MVVM bindings, I will begin with a basic value binding.  We want to be able to markup our widget as:
<input data-role="simplewidget" data-bind="value: simpleValue" />
To accomplish this, three things need to occur.  First, we need the value to be set in our widget by the MVVM framework.  This is actually very straightforward.  We simply provide a value function and MVVM will call it with the current  value of the property bound by the binding.  In the markup shown above, the value function will be called with the value of the item in our viewmodel called simpleValue.  This can be a simple value or a dependent method.  In either case, the underlying MVVM framework will call the value function anytime the value needs to be updated from the viewmodel.  Our function looks like this:
//MVVM framework calls 'value' when the viewmodel 'value' binding changes
value: function(value) {
    var that = this;
 
    if (value === undefined) {
        return that._value;
    }
    that._update(value);
    that._old = that._value;
},
//Update the internals of 'value'
_update: function (value) {
    var that = this;
    that._value = value;
    that.element.val(value);
}
I have split the work into two methods, the value function is exposed for MVVM, while the _update function by convention (underscored prefix) is intended for internal use.  When MVVM calls value, it provides a simple value in the value parameter.  If undefined is passed, we refrain from updating and return out of the function.  Otherwise, we update our internal _value var to contain the current value of the widget and also update our DOM element's value so that UI is updated with the current value from MVVM.  We also capture the current value in a var called _old for future use, we'll need it when we get to notifying MVVM of changes.  It's really fairly simple and it's all we need to be notified anytime the value binding is updated by the MVVM framework.

The next step is to alert the MVVM framework when the widget's value is changed by the user.  The user will be able to modify the value on the screen and MVVM is going to need to know about the new value.  We need to capture an event to know when the user has updated the value.  Let's declare a 'blur' event handler on our element during the init process so we know when the user has been focused and then 'blurred' out of our element.
init: function (element, options) {
    var that = this;
    ...
    //Create a blur event handler.
    element = that.element.on("blur.kendoSimpleWidget", $.proxy(that._blur, that));
    ...
}
The .on method will create an event handler on our widget.  The function to execute that._blur is wrapped in a proxy to set the scope that will be available when the event handler is called.  This allows us to ensure that the 'this' var is set to our widget at the time that the _blur function is called.  Without this proxy, the this var would not include a reference to our widget, but rather a reference to the element itself.  Now that we've attached an event handler to the element we need to define the callback function that is the handler.
//blur event handler - primary UI change detection entry point
 _blur: function () {          
    var that = this;           
    that._change(that.element.val());
},
The _blur function is passing the event off to a _change function that will handle the rest of the job.  This chaining of events keeps the code clean with convention so we don't bind "blur" to a function called "_change".  It would also allow us to chain other event handlers that may need to update the value. We use jQuery's .val() function to get the current value of the DOM element.
_change: function (value) {
    var that = this;
    //Determine if the value is different than it was before
    if (that._old != value) {
        //It is different, update the value
        that._update(value);
        //Capture the new value for future change detection
        that._old = value;
        // trigger the external change
        that.trigger("change");
    }
The _change function does the real work.  First we determine if the new value that is supplied is any different than the previous value.  We don't want to notify MVVM that a change occured if the user only focused out of the element without making any changes.  The user could also have re-entered the same value a second time.  If the value did change, we capture it in _old for future change detection and then use jQuery to trigger a change event.

The final piece that we need is to let the MVVM framework know that our widget can fire the 'change' event.  Without this declaration the MVVM framework won't know to listen for change events and our trigger won't accomplish anything.
//Export the events the control can fire
events: ["change"]
With the event exported, our widget is now fully MVVM aware on the value binding. We have two-way communications between the MVVM framework and our widget.  In the next part, I'll show how to mutate the DOM to change the widget's appearance and begin to explore other types of bindings that we can support.

A working sample is at: http://jsfiddle.net/cn172/vyuP2/

The full widget code:

(function ($) {
    var kendo = window.kendo,
        ui = kendo.ui,
        Widget = ui.Widget,
        CHANGE = "change",
        BLUR = "blur",
        ns = ".kendoSimpleWidget";
 
    var SimpleWidget = Widget.extend({
        // Kendo calls this method when a new widget is created
        init: function (element, options) {
            var that = this;
            Widget.fn.init.call(this, element, options);
            //Create a blur event handler.
            element = that.element
                           .on(BLUR + ns, $.proxy(that._blur, that));
            //Set the value from the options.value setting
            if (options.value) {
                that.value(options.value);
            }
        },
        //List of all options supported and default values
        options: {
            name: "SimpleWidget",
            value: null,
            width: "150"
        },
        //MVVM framework calls 'value' when the viewmodel 'value' binding changes
        value: function(value) {
            var that = this;
 
            if (value === undefined) {
                return that._value;
            }
            that._update(value);
            that._old = that._value;
        },
        //Export the events the control can fire
        events: [CHANGE],
        // this function creates each of the UI elements and appends them to the element
        //blur event handler - primary UI change detection entry point
        _blur: function () {          
            var that = this;           
            that._change(that.element.val());
        },
        //Update the internals of 'value'
        _update: function (value) {
            var that = this;
            that._value = value;
            that.element.val(value);
        },         
        _change: function (value) {
            var that = this;
            //Determine if the value is different than it was before
            if (that._old != value) {
                //It is different, update the value
                that._update(value);
                //Capture the new value for future change detection
                that._old = value;
                // trigger the external change
                that.trigger(CHANGE);
            }
        }
    });
    ui.plugin(SimpleWidget);
})(jQuery);
0
Alexander Valchev
Telerik team
answered on 15 Jan 2014, 02:50 PM
Hello Scott,

Thank you for the contribution. As a small sign of our appreciation for your effort I updated your Telerik points.

Regards,
Alexander Valchev
Telerik
Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
0
Scott
Top achievements
Rank 1
answered on 15 Jan 2014, 03:54 PM
The next step in creating our custom MVVM widget is to mutate the DOM to represent the visual representation for our simple widget. I'm shamelessly copying an example I found in another post, but I can't remember the source.  We'll add a search icon to a text box.  To do this, we'll want to add a _create internal method that will use jQuery to modify the DOM for our representation.  _create will be called from the init function so that it will be executed when the framework initializes our widget.
// 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;
 
    // setup the icon
    var template = kendo.template(that._templates.icon);
    that.icon = $(template(that.options));
 
    // setup the textbox
    template = kendo.template(that._templates.textbox);
    that.textbox = $(template(that.options));
     
    // append all elements to the DOM
    that.element.attr("name", that.options.name);
    that.element.addClass("k-input");
    that.element.css("width", "100%");
    that.element.wrap(that.textbox);
     
    that.element.after(that.icon);
},
//HTML for the templates that comprise the widget
_templates: {
    textbox: "<span style='width: #: width #px;' class='k-widget k-datepicker k-header tb'><span class='k-picker-wrap k-state-default'></span></span>",
    icon: "<span unselectable='on' class='k-select' role='button'><span unselectable='on' class='k-icon #: iconclass #'>select</span></span>"        },
The _create method uses templating to create the widget's UI and uses kendo styles to achieve the desired effect.  I'll not cover the specifics of the jQuery work here, but will note that the Kendo template system is very easy to use and quite powerful.  In this example, you can see that the widget's width is captured from the width setting.  The icon's class  is also added from options, so the caller can send in their own class via the options collection when they create the markup for the widget.  The widget's options collection is passed into the template call so that the variables in the options are available in the template.  The remaining task is to add a call to _create from our init method.
// Kendo calls this method when a new widget is created
init: function (element, options) {
    ....
    that._create();
    ....
}
The fiddle at: http://jsfiddle.net/cn172/6EWbW/ has a working example.

I've attached a JPG of the widget's look.  Don't see a way to put images inline in these posts.

Full widget source code:
(function ($) {
    var kendo = window.kendo,
        ui = kendo.ui,
        Widget = ui.Widget,
        CHANGE = "change",
        BLUR = "blur",
        ns = ".kendoSimpleWidget";
 
    var SimpleWidget = Widget.extend({
        // Kendo calls this method when a new widget is created
        init: function (element, options) {
            var that = this;
            Widget.fn.init.call(this, element, options);
            //Create a blur event handler.
            element = that.element
                           .on(BLUR + ns, $.proxy(that._blur, that));
            //Create the DOM elements to build the widget
            that._create();
            //Set the value from the options.value setting, if it was called with a static value
            if (options.value) {
                that.value(options.value);
            }
        },
        //List of all options supported and default values
        options: {
            name: "SimpleWidget",
            value: null,
            width: "150px;",
            iconclass: "k-i-search",
        },
        //Convenience method to set the value of the control externally
        //Useful for event handlers in dependent methods to be able to
        //set the control's value and have it propogate to the MVVM subscribers
        set: function (value) {
            var that = this;
            if (that._old != value) {
                //It is different, update the value
                that._update(value);
                //Capture the new value for future change detection
                that._old = value;
                // trigger the external change event to notify subscribers
                that.trigger(CHANGE);
            }
        },
        //MVVM framework calls 'value' when the viewmodel 'value' binding changes
        value: function(value) {
            var that = this;
 
            if (value === undefined) {
                return that._value;
            }
            that._update(value);
            that._old = that._value;
        },
        //Export the events the control can fire
        events: [CHANGE],
        // 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;
 
            // setup the icon
            var template = kendo.template(that._templates.icon);
            that.icon = $(template(that.options));
 
            // setup the textbox
            template = kendo.template(that._templates.textbox);
            that.textbox = $(template(that.options));
             
            // append all elements to the DOM
            that.element.attr("name", that.options.name);
            that.element.addClass("k-input");
            that.element.css("width", "100%");
            that.element.wrap(that.textbox);
             
            that.element.after(that.icon);
        },
        //HTML for the templates that comprise the widget
        _templates: {
            textbox: "<span style='width: #: width #px;' class='k-widget k-datepicker k-header tb'><span class='k-picker-wrap k-state-default'></span></span>",
            icon: "<span unselectable='on' class='k-select' role='button'><span unselectable='on' class='k-icon #: iconclass #'>select</span></span>"        },
        //blur event handler - primary UI change detection entry point
        _blur: function () {          
            var that = this;           
            that._change(that.element.val());
        },
        //Update the internals of 'value'
        _update: function (value) {
            var that = this;
            that._value = value;
            that.element.val(value);
        },         
        _change: function (value) {
            var that = this;
            //Determine if the value is different than it was before
            if (that._old != value) {
                //It is different, update the value
                that._update(value);
                //Capture the new value for future change detection
                that._old = value;
                // trigger the external change
                that.trigger(CHANGE);
            }
        }
    });
    ui.plugin(SimpleWidget);
})(jQuery);

0
Scott
Top achievements
Rank 1
answered on 15 Jan 2014, 04:27 PM
In my previous post, I added a search icon to the widget and styled it to appear integrated with a text box. This allows our user to type something into the text box, or click the search icon if they want to lookup a value from some sort of modal search screen or perform any action related to clicking on the widget's internal icon.  It's clear from our previous examples how to raise an event like "change" to mvvm and bind to it, but how do we bind a custom event?
In this example, we want the MVVM binding to be able to bind to the click of our search icon only, not when the rest of the widget is clicked.  In short we want a markup like this:
<input data-role="simplewidget" data-bind="value: simpleValue, events: { buttonclick: onSomeButtonClick" />
In our markup, we retain the value binding, but add a new 'buttonclick' binding. The buttonclick event should be raised when the user clicks the 'search icon'.  To support this, the first step is to declare our intention to be firing a new event type.  We'll need to modify the declaration of the 'events' attribute of our widget.
//Export the events the control can fire
events: ["change", "buttonclick"],
With this definition, we allow the MVVM framework to see the events we may fire and be able to setup bindings to these events.  Our binding above would now be valid.  To enable the event, we'll need to handle the actual click event on the icon internally to our widget.  We'll setup a click event handler using jQuery in our widgets _create function.
_create: function () {
    // cache a reference to this
    var that = this;
 
    // setup the icon
    var template = kendo.template(that._templates.icon);
    that.icon = $(template(that.options));
    ....
    that.icon.on("click", $.proxy(that._buttonclick, that));
    ....
}
With our icon selector, we can add our event handler.  We also use the jQuery proxy function to wrap our event handler and provide it with a specific scope.  the var 'that' is provided as scope which will pass our widget as scope to the call to the event handler '_buttonclick' function.  If we don't use the proxy wrapping method, the scope of our event handler won't have convenient access to our widget and won't be able to access any of the properties or methods of the widget.

Now we need to add the event handler function _buttonclick.
//Fire the external event: buttonclick
_buttonclick: function (element) {
    var that = this;
    that.trigger("buttonclick", { element: element });
    return that;
},
_buttonclick is called every time our user clicks the icon. The scope (this) is set to our widget thanks to our proxy, so we have access to the widgets properties and methods if we need them.  We'll call trigger off the scope and trigger the event we called 'buttonclick' and provide the element that was clicked on to be passed in the click event.  It is important to note that the trigger must be fired from the scope of the widget.  If it is fired from a different scope, say we did element.trigger("buttonclick"); MVVM would not be listening for events originating from the element's scope and would not fire our bound event handler that we setup in the markup.  Our markup should now be functioning and we can create any form of widget we like with custom event bindings.

Working example at: http://jsfiddle.net/cn172/w2Mct/

Full source code:

(function ($) {
    var kendo = window.kendo,
        ui = kendo.ui,
        Widget = ui.Widget,
        CHANGE = "change",
        BUTTONCLICK = "buttonclick",
        BLUR = "blur",
        ns = ".kendoSimpleWidget";
 
    var SimpleWidget = Widget.extend({
        // Kendo calls this method when a new widget is created
        init: function (element, options) {
            var that = this;
            Widget.fn.init.call(this, element, options);
            //Create a blur event handler.
            element = that.element
                           .on(BLUR + ns, $.proxy(that._blur, that));
            //Create the DOM elements to build the widget
            that._create();
            //Set the value from the options.value setting, if it was called with a static value
            if (options.value) {
                that.value(options.value);
            }
        },
        //List of all options supported and default values
        options: {
            name: "SimpleWidget",
            value: null,
            width: "150px;",
            iconclass: "k-i-search",
        },
        //Convenience method to set the value of the control externally
        //Useful for event handlers in dependent methods to be able to
        //set the control's value and have it propogate to the MVVM subscribers
        set: function (value) {
            var that = this;
            if (that._old != value) {
                //It is different, update the value
                that._update(value);
                //Capture the new value for future change detection
                that._old = value;
                // trigger the external change event to notify subscribers
                that.trigger(CHANGE);
            }
        },
        //MVVM framework calls 'value' when the viewmodel 'value' binding changes
        value: function(value) {
            var that = this;
 
            if (value === undefined) {
                return that._value;
            }
            that._update(value);
            that._old = that._value;
        },
        //Export the events the control can fire
        events: [CHANGE, BUTTONCLICK],
        // 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;
 
            // setup the icon
            var template = kendo.template(that._templates.icon);
            that.icon = $(template(that.options));
 
            // setup the textbox
            template = kendo.template(that._templates.textbox);
            that.textbox = $(template(that.options));
 
             
            that.icon.on("click", $.proxy(that._buttonclick, that));
             
            // append all elements to the DOM
            that.element.attr("name", that.options.name);
            that.element.addClass("k-input");
            that.element.css("width", "100%");
            that.element.wrap(that.textbox);
             
            that.element.after(that.icon);
        },
        //Fire the external event: buttonclick
        _buttonclick: function (element) {
            var that = this;
            that.trigger(BUTTONCLICK, { element: element });
            return that;
        },
        //HTML for the templates that comprise the widget
        _templates: {
            textbox: "<span style='width: #: width #px;' class='k-widget k-datepicker k-header tb'><span class='k-picker-wrap k-state-default'></span></span>",
            icon: "<span unselectable='on' class='k-select' role='button'><span unselectable='on' class='k-icon #: iconclass #'>select</span></span>"        },
        //blur event handler - primary UI change detection entry point
        _blur: function () {          
            var that = this;           
            that._change(that.element.val());
        },
        //Update the internals of 'value'
        _update: function (value) {
            var that = this;
            that._value = value;
            that.element.val(value);
        },         
        _change: function (value) {
            var that = this;
            //Determine if the value is different than it was before
            if (that._old != value) {
                //It is different, update the value
                that._update(value);
                //Capture the new value for future change detection
                that._old = value;
                // trigger the external change
                that.trigger(CHANGE);
            }
        }
    });
    ui.plugin(SimpleWidget);
})(jQuery);

0
Jacques
Top achievements
Rank 2
answered on 31 Jan 2014, 05:50 AM
Hi Scott, 

Out of interest, in the Oct 2012 post you make use of a variable that._value but I can't see where this is declared. Are we just assuming JavaScripts ability to randomly add properties/methods to objects? 

Regards,
Jacques
0
Scott
Top achievements
Rank 1
answered on 31 Jan 2014, 03:53 PM
In the second post, i use that._value without any declaration of such.  that._value could be declared in the widgets list of properties with no functional difference.  Without the declaration _value is added to the SimpleWidget function at the point that it is encountered by the interpreter.  If it was a listed property within the SimpleWidget object it would be created when the function was initilialized, in either case it will evaluate to undefined until it is assigned a value.  The widget was coded this way to follow the same style I've seen in Kendo's existing widgets.
Tags
MVVM
Asked by
Scott
Top achievements
Rank 1
Answers by
Scott
Top achievements
Rank 1
Alexander Valchev
Telerik team
Jacques
Top achievements
Rank 2
Share this question
or