Databound custom widget

9 posts, 1 answers
  1. Lance
    Lance avatar
    25 posts
    Member since:
    Apr 2013

    Posted 22 Jun 2014 Link to this post

    Hi,

    I've successfully created a custom widget and can use it as the editor in a kendo grid but I can't figure out how to databind it.  I want the widget to have a value property that is bound to the datasource of the grid so that when the grid is loaded, this widget will be filled with the correct value for that record and when the value is changed in the widget it will update the value of the bound field in that record of the datasource.  Can you please let me know how to achieve this?  Is there a simple example?

    Please help.

    Thanks
  2. Alexander Valchev
    Admin
    Alexander Valchev avatar
    2877 posts

    Posted 24 Jun 2014 Link to this post

    Hi Lance,

    Please note that providing assistance related to development of custom widgets is out of the scope of our support service. The standard support service - the one included in the support package that goes with the controls, cover only the built-in functionality and features of the framework.
    In case you would like to create your own dataSource aware custom widget please check this tutorial.

    Regards,
    Alexander Valchev
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  3. Kendo UI is VS 2017 Ready
  4. Lance
    Lance avatar
    25 posts
    Member since:
    Apr 2013

    Posted 10 Jul 2014 in reply to Alexander Valchev Link to this post

    Hi Alexander,

    This seems a bit mean-spirited, especially as I'm a paying customer.  I've got a custom widget that works.  The value of the widget is updated with the value of the field in the external datasource that is bound to it which is good, but I can't get the bound field in this datasource to be updated with the new value in the widget when there is a change.  I feel I'm close but have spend a frustrating lot of time to get this far.  Can you please help me finish this last piece of the puzzle?

    Below is my widget code:
    define(
        [
          'kendo',
          'text!plugins/usersearch/userselection.html',
          'text!plugins/usersearch/usersearch.html',
          'models/userinformation'
        ], function (kendo, userSelectionTemplate, userSearchTemplate, userinformation)
        {
            //turn off autocomplete (remembering previous values) for all textboxes
            $("input:text,form").attr("autocomplete", "off");

            //Select all text on focus for all textboxes
            $('input:text,textarea, form').on('focus', function ()
            {
                //use .one to only stop the first mouseup event
                $(this).one('mouseup', function (event)
                {
                    event.preventDefault();
                }).select();
            });

            var wnd;
            var wind;
            var kendo = window.kendo,
                ui = kendo.ui,
                Widget = ui.Widget;

            var CHANGE = "change";
            var mainLabelElement;
            var mainDivElement;

            var gridDataSource = new kendo.data.DataSource({
                schema: {
                    data: function (data)
                    { //specify the array that contains the data
                        return data || [];
                    },
                    model: userinformation
                },
                transport: {
                    read: {
                        url: CommonInformationWcfServiceUrl.value + "/rest/SearchUsers",
                        dataType: "jsonp",
                        type: "GET"
                    },
                    parameterMap: function (data, operation)
                    {
                        if (operation == "read")
                        {
                            data["searchText"] = $("#searchText").val();

                            return data;
                        }
                    }
                },
                error: function (e)
                {
                    this.set("validSearch", true);

                    var xhr = e.xhr;
                    var statusCode = e.status;
                    var errorThrown = e.errorThrown;
                    var msg = xhr.responseText;

                    JL().error(errorThrown + ' ' + msg);
                },
                change: function (data)
                {

                }
            });

            var viewModel = kendo.observable({
                title: 'Select User',
                gridSource: gridDataSource,
                validSearch: false,
                searchTextChange: function (e)
                {
                    this.set("validSearch", $("#searchText").val().length > 2);
                },
                userSearchOpen: function (e)
                {

                },
                searchUser: function (e)
                {
                    this.set("validSearch", false);

                    $('#grid').data('kendoGrid').dataSource.read();
                },
                rowSelect: function (e)
                {
                    var gview = e.sender.element.getKendoGrid();
                    var selectedItem = gview.dataItem(gview.select());

                    this.set("selectedUser", selectedItem.FullName);

                    //UserSearch.fn.value(selectedItem.BemsId);

                    if (mainLabelElement)
                    {
                        mainLabelElement.text(selectedItem.FullName);
                    }

                    gview.dataSource.data([]);

                    wind.close();

                    UserSearch.fn._change(selectedItem.BemsId);
                },
                selectedUser: "",
                selectUser: function (e)
                {
                    e.preventDefault();

                    wnd = $(document.createElement('div'));

                    // Apply template to the placeholder element, and bind the viewmodel.
                    wnd.html(kendo.template(userSearchTemplate)(viewModel));
                    kendo.bind(wnd, viewModel);

                    // Add window placeholder to the body.
                    $('body').append(wnd);

                    // Turn placeholder into a Window widget.
                    wnd.kendoWindow({
                        width: 500,
                        title: "Find User",
                        resizable: false,
                        iframe: false,
                        close: function ()
                        {
                            // When the window is closed, remove the element from the document.
                            wnd.parents(".k-window").remove();

                            wind.destroy();
                            wnd = null;
                            wind = null;
                        },
                        open: viewModel.userSearchOpen
                    });

                    // Centre and show the Window.
                    wind = wnd.data("kendoWindow").center().open();
                }
            });

            var UserSearch = Widget.extend({
                init: function (element, options)
                {
                    var that = this;

                    Widget.fn.init.call(this, element, options);

                    that._create();

                    if (options)
                    {
                        if (options.value)
                        {
                            that.value(options.value);
                        }
                    }
                },
                options: {
                    name: "UserSearch",
                    value: null
                },
                events: [CHANGE],
                _templates: {
                    label: '<label id="selectedUser" class="k-label" style="padding-right: 10px; min-width: 100px">#: value #</label>',
                    button: '<button class="k-button k-button-icontext" style="padding: 2px; margin: 2px; line-height:1em">...</button>',
                    div: '<div id="divUserSelect" data-bind="events: { keyup: searchTextChange }"></div>'
                },
                _create: function ()
                {
                    var that = this;

                    var template = kendo.template(that._templates.label);
                    that.label = $(template(that.options));

                    template = kendo.template(that._templates.button);
                    that.button = $(template(that.options));

                    template = kendo.template(that._templates.div);
                    that.div = $(template(that.options));

                    that.div.append(that.label)
                            .append(that.button);

                    that.button.click(viewModel.selectUser);

                    that.element.append(that.div);

                    mainLabelElement = that.label;
                    mainDivElement = that.div;

                    //kendo.data.binders.value = kendo.data.Binder.extend({
                    //    refresh: function ()
                    //    {
                    //        // get the value of the view model field to which the element is bound 
                    //        var value = this.bindings["value"].get();

                    //        $(this.element).value(value);
                    //    }
                    //});

                    ////listen for the change event of the element
                    //$(mainDivElement).on(CHANGE, function ()
                    //{
                    //    that._change(); //call the change function
                    //});

                    //bind widget to viewmodel
                    kendo.bind(that.element, viewModel);
                },
                //set: function (value)
                //{
                //    var that = this;

                //    if (that._old != value)
                //    {
                //        that._update(value);
                //        that._old = value;
                //        that.trigger(CHANGE);
                //    }
                //},
                value: function (value)
                {
                    var that = this;

                    if (value === undefined)
                    {
                        return that._value;
                    }

                    that._update(value);
                    //that._change(value);
                    that._old = that._value;
                },
                _update: function (value)
                {
                    if (value)
                    {
                        //Update the internals of 'value'
                        var that = this;

                        that._value = value;
                        that.options.value = value;

                        //Determine if the value is different than it was before
                        if (that._old != value)
                        {
                            //set value
                            $.ajax({
                                contentType: 'application/json; charset=utf-8',
                                type: "GET",
                                dataType: "jsonp",
                                url: CommonInformationWcfServiceUrl.value + "/rest/SearchUsers?searchText=" + value,
                                success: function (data)
                                {
                                    if (data.length > 0)
                                    {
                                        var name;

                                        that.options.value = data[0].BemsId;
                                        name = data[0].FullName;

                                        viewModel.set("selectedUser", name);

                                        if (mainLabelElement)
                                        {
                                            mainLabelElement.text(name);
                                        }
                                    }
                                },
                                error: function (e, xhr, opt)
                                {
                                    JL().error(e.status + " " + e.statusText);
                                }
                            });
                        }
                    }
                },
                _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, { field: "value" });
                        //that.trigger(CHANGE);
                    }
                }
            });

            ui.plugin(UserSearch);
        });
  5. Answer
    Alexander Valchev
    Admin
    Alexander Valchev avatar
    2877 posts

    Posted 14 Jul 2014 Link to this post

    Hello Lance,

    First of all let me apologize for the late reply.

    In order value binding to work the widget should:

    1. Have a value method that sets or gets the current widget value.
    2. Change event that is fired when the user changes the widget's value. Value method should not fire the change event as this will lead to a never ending loop. Change event should be fired when the user modified widget's value through the UI. The MVVM value binding listens for the change event of the widget.

    I created a very basic example that demonstrates a working value binding for custom widget. Please see it and pay attention to the comments:
    <input id="foo" type="text" data-role="customwidget" data-bind="value: value" />
    <br />
    ViewModel value: <span data-bind="text: value"></span>
     
    <script>
        var CustomWidget = kendo.ui.Widget.extend({
            init: function (element, options) {
                var that = this;
     
                kendo.ui.Widget.fn.init.call(that, element, options); //call the base widget constructor
     
                that.element.addClass("k-textbox"); //enchange the widget
     
                //trigger the change event of the widget on blur
                that.element.on("blur", function() {
                    that.trigger("change");
                });
            },
            options: {
                name: "CustomWidget"
            },
            events: ["change"],
            value: function(value) { //change event that gets or sets the value
                if (value === undefined) {
                    return this.element.val();
                }
     
                this.element.val(value);
            }
        });
     
        kendo.ui.plugin(CustomWidget); //export the widget as plug-in
     
        var viewModel = kendo.observable({
            value: "foo"
        });
     
        kendo.bind($(document.body), viewModel); //bind the UI
    </script>

    trykendoui.telerik.com link

    Note that the widget is not being bound from within one of its internal methods. Calling kendo.bind from within the internal _create method is not recommended, calling kendo.bind from within one of the ViewModel methods is not recommended either.

    Also in your case the change event is fired from the _change method of the widget which as far as I saw is not called when some event (blur, click, focus) occurs.

    Please use the sample code above as a starting point for creating a custom widget that fits in your requirements.

    Regards,
    Alexander Valchev
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  6. Lance
    Lance avatar
    25 posts
    Member since:
    Apr 2013

    Posted 15 Jul 2014 in reply to Alexander Valchev Link to this post


    Hi Alex,

    Thanks very much for your answer.  The key in my case was storing the value in .Val() of an element in the widget.  I was storing it in a local private variable and the value was being lost so the datasource wasn't updated with the new value.

    You said that it is not recommended to call kendo bind from within the internal _create method or from within one of the ViewModel methods but you didn't explain why.  I've moved one call to kendo bind as you suggested but I couldn't see an alternative to having it in the method of my viewModel as I need to bind my dynamically created Window to it.  I am doing this to communate values and events between the widget and the dynamic window. 

    Can you please explain why this isn't recommended and what would be the recommended way of achieving this?

    Thanks
  7. Alexander Valchev
    Admin
    Alexander Valchev avatar
    2877 posts

    Posted 17 Jul 2014 Link to this post

    Hi Lance,

    Kendo widgets does not depended on the ViewModel. In other words the widgets can exist with or without MVVM.
    In case you insist to call kendo.bind from within the widget methods, please be sure that container you will bind is not already bound. 

    Regards,
    Alexander Valchev
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  8. Lance
    Lance avatar
    25 posts
    Member since:
    Apr 2013

    Posted 31 Jul 2014 in reply to Alexander Valchev Link to this post

    Hi Alex,

    Thanks for your reply.  How do I ensure that the container is not already bound?  Do I call kendo.unbind against it first?
  9. Vladimir Iliev
    Admin
    Vladimir Iliev avatar
    2172 posts

    Posted 04 Aug 2014 Link to this post

    Hi Lance,

    Basically you can call the "unbind" method first, however this can break existing logic on the page - that why Alexander already recommended to not call the "bind" method internally.


    Regards,
    Vladimir Iliev
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  10. Gal
    Gal avatar
    48 posts
    Member since:
    Apr 2009

    Posted 19 Sep 2015 in reply to Alexander Valchev Link to this post

    This is a critical piece of information regarding the creation of Custom widgets!
    Without this, the widget doesnt work in a grid! 

    This was real handy!

    10x Lance for insisting on a proper reply!
    10x Alexander ​for the reply!

    With this information I was able to extend the kendo date/dateTime picker to include a mask. 

    (function ($) {
        var kendo = window.kendo,
            ui = kendo.ui,
            Widget = ui.Widget;
            CHANGE = "change",
            BLUR = "blur",
     
        [ { type: 'DateTime', defaultMask: '00/00/0000 00:00', container: '.k-datetimepicker' },
          { type: 'Date',     defaultMask: '00/00/0000',       container: '.k-datepicker'     }]
        .forEach(function(x) {
     
            var baseName = 'kendo' + x.type + 'Picker';
            var newName = "Masked" + x.type + "Picker";
            var _basePlugin = $.fn[baseName];
            var maskedPicker = Widget.extend({
                init: function (element, options) {
                    var that = this;
                    Widget.fn.init.call(this, element, options);
     
                    $(element).kendoMaskedTextBox({ mask: options.mask || x.defaultMask });
                    _basePlugin.call($(element), options)
                        .closest(x.container)
                        .add(element)
                        .removeClass("k-textbox");
     
                    that.element.on("blur", function () {
                        that.trigger("change");
                    });
                },
                options: {
                    name: newName,
                    dateOptions: {}
                },
                destroy: function () {
                    var that = this;
                    Widget.fn.destroy.call(that);
     
                    kendo.destroy(that.element);
                },
                value: function (value) {
                    var datepicker = this.element.data(baseName);
     
                    if (value === undefined) {
                        return datepicker.value();
                    }
                    datepicker.value(value);
                },
                //Export the events the control can fire
                events: [CHANGE]
            });
            ui.plugin(maskedPicker);
            $.fn[baseName] = $.fn['kendo' + newName];
        });
     
    })(jQuery);

    Enjoy ! 

Back to Top
Kendo UI is VS 2017 Ready