How to create a custom widget using TypeScript

21 posts, 0 answers
  1. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 17 Mar 2016 Link to this post

    Hello,

    I'm trying to implement a custom widget using TypeScript. I will not inherit from another Widget.

    Is there any sample or documentation? 

    This is my code:

    module mycompany.MyWidgets {
      "use strict";
     
      export class SampleWidget extends kendo.ui.Widget {
        constructor(element: Element, options?: MyInterfaces.ISampleOptions) {
          super(element, options);
        }
      }
       
      SampleWidget.fn = Sample.prototype;
      $.extend(true, {}, SampleWidget.options);
      (<ISampleOptions>SampleWidget.options).name = "SampleWidget";
      kendo.ui.plugin(SampleWidget);
    }
    interface JQuery {
      kendoSampleWidget(options?: ISampleOptions): JQuery
    }

     

    But the widget will not be used. Not using data-role nor jQuery selector $("#mydiv").kendoMyWidget();

    Samples:

    <div id='widgetcontainer' data-role='samplewidget'></div>

    The widget constructor not will be called.

    And:

    <div id='widgetcontainer'></div>

    window.onload = () => {
        $('#eidgetcontainer').kendoSampleWidget();
    };

    A script error will be shown (kendoSampleWidget is not a function)

    What's the correct way?

    Regards

    Ralf

     

  2. T. Tsonev
    Admin
    T. Tsonev avatar
    2830 posts

    Posted 21 Mar 2016 Link to this post

    Hello,

    We have a sample as part of the documentation.

    I didn't run your code snippet, but the $.extend call doesn't seem right. The return value is not assigned anywhere.
    See the snippet in the documentation for reference.

    Regards,
    T. Tsonev
    Telerik
     
    Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
     
  3. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 24 Apr in reply to T. Tsonev Link to this post

    Do you have a sample that enables MVVM? I tried to use this sample, but kendo.notify will not accept my widget because it says it does not implement fn,extend,prototype.
  4. Aleksandar
    Admin
    Aleksandar avatar
    136 posts

    Posted 28 Apr Link to this post

    Hello Joshua,

    An example of using TypeScript and MVVM is available at the end of the TypeScript documentation section. You can also check the article on Custom Widget Binding with MVVM and TypeScript for an additional example.

    Regards,
    Aleksandar
    Progress Telerik

    Progress is here for your business, like always. Read more about the measures we are taking to ensure business continuity and help fight the COVID-19 pandemic.
    Our thoughts here at Progress are with those affected by the outbreak.
  5. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 28 Apr in reply to Aleksandar Link to this post

    Thanks for the reply. I have read through both of those samples several times. They still don't cover how to create an MVVM enabled custom widget in TypeScript. Currently, I am not finding a way to call kendo.notify in the init, so I am assuming I am missing some vital step.
  6. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 29 Apr in reply to Joshua Link to this post

    Hi,

    here is a complete sample. Hope it will be usefull.

    This sample uses custom binders to handle mvvm binding. You can use build in binders like value too. Check get and set functions and also the custom binders to know how it works.

    Best regards,

    Ralf

     

    const DATABINDING = "dataBinding",
        DATABOUND = "dataBound",
        CHANGE = "change"
     
    export interface HierarchicalDataSourceOptions {
        siteUrl: string;
    }
     
    export interface Site {
        id: string;
        title: string;
    }
     
    export interface Web {
        id: string;
        title: string;
    }
     
    export interface List {
        id: string;
        title: string;
    }
     
    export interface ContentType {
        id: string;
        title: string;
    }
     
    /**
     * Interface for the definition of the options of the control
     */
    export interface SharePointObjectSelectorOptions {
        name: string;
        site: any;
        web: any;
        list: any;
        selectorOptions: HierarchicalDataSourceOptions;
        valuePrimitive: boolean;
        change?(e: SharePointObjectSelectorChangeEvent): void;
        select?(e: SharePointObjectSelectorSelectEvent): void;
    }
     
    export interface SharePointObjectSelectorEvent {
        sender: SharePointObjectSelector;
        preventDefault: Function;
        isDefaultPrevented(): boolean;
    }
     
    export interface SharePointObjectSelectorChangeEvent extends SharePointObjectSelectorEvent {
        field: string;
    }
     
    interface SharePointObjectSelectorSelectEvent extends SharePointObjectSelectorEvent {
        field: string;
        value: any;
    }
     
    /**
     * Implementation of the kendo widget with MVVM connection
     */
    export class SharePointObjectSelector extends kendo.ui.Widget {
        private _site: Site;
        private _web: Web;
        private _list: any;
        private _dataSourceOptions: HierarchicalDataSourceOptions;
        private _options: SharePointObjectSelectorOptions;
        private _siteInput: any;
        private _webInput: any;
        private _listInput: any;
        private _controlId: string;
     
        /**
         * The constructor is called  in the classic and instantiation via MVVM
         * @param element
         * The DOM element, which is the placeholder of the widget.
         * @param options
         * The options of the widget. These are only used for classic instantiation.
         * No options are set for instantiation via MVVM.
         */
        constructor(element: Element, options?: SharePointObjectSelectorOptions) {
            super(element, options);
            this._controlId = kendo.guid();
            if (!options.site) {
                options = <SharePointObjectSelectorOptions>{
                    name: "SharePointObjectSelector",
                    site: null,
                    web: null,
                    list: null,
                    selectorOptions: null
                };
            }
     
            this._site = this.options.site;
            this._web = this.options.web;
            this._list = this.options.list;
     
            this.initalize();
        }
     
        /**
         * The events which can be bound from external.
         */
        public events: Array<string> = [
            CHANGE,
            DATABINDING,
            DATABOUND
        ];
     
        /**
         * Create the UI elements of the widget.
         * During creation, the events of the UI elements are also linked to handlers,
         * to be able to react to it in the control.
         */
        private initalize(): void {
            let wrapper = this.element.append($("<div></div>"));
            wrapper.append($('<select />', { id: "siteSelector_" + this._controlId, style: 'width: 500px;' }));       
            this._siteInput = $("#siteSelector_" + this._controlId).kendoDropDownList({
                value: this._site ? this._site.id : null,
                dataTextField: "title",
                dataValueField: "id",
                change: this._onSiteSelectionChange.bind(this),
                dataSource: new kendo.data.DataSource({
                    data: [{id: "123456", title: "Develoment Site"}, { id: "7890", title: "Production Portal"}]
                })
            }).data("kendoDropDownList");
             
     
            
            wrapper = this.element.append($("<div></div>"));
            wrapper.append($('<select />', { id: "webSelector_" + this._controlId, style: 'width: 500px;' }));
            this._webInput = $("#webSelector_" + this._controlId).kendoDropDownList({
                value: this._web ? this._web.id : null,
                dataTextField: "title",
                dataValueField: "id",
                change: this._onWebSelectionChange.bind(this),
                dataSource: new kendo.data.DataSource({
                    data: [{id: "123456", title: "RootWeb"}, { id: "7890", title: "Archiv"}]
                })
            }).data("kendoDropDownList");
             
            wrapper = this.element.append($("<div></div>"));
            wrapper.append($('<input />', { id: "listSelector_" + this._controlId, style: 'width: 500px;', class: 'k-textbox' }));
            this._listInput = $("#listSelector_" + this._controlId);
            this._listInput.bind("blur", this._onListInputBlur.bind(this));
            this.refresh();
        }
     
        /**
         * This function updates the UI of the control.
         */
        public refresh(): void {
            var that = this;
            that.trigger(DATABINDING);
     
            if (this._site) {
                this._siteInput.value(this._site.id);
            }
            if (this._web) {
                this._webInput.value(this._web.id);
            }
            this._listInput.val(this._list);
     
            that.trigger(DATABOUND);
        }
     
        /**
         * This function is called by a custom binder.
         * It updates the UI with the value bound by MVVM from the ViewModel.
         * @param value
         * The value bound by MVVM.
         */
        public setSite(value: any): void {
            if (this._site !== value) {
                this._site = value;
                this.refresh();
            }
        }
     
        /**
         * This function is called by a custom binder.
         * It updates the value bound by MVVM in the ViewModel.
         */
        public getSite(): any {
            return this._site;
        }
     
        /* This function is called by a custom binder.
         * It updates the UI with the value bound by MVVM from the ViewModel.
         * @param value
         * The value committed by MVVM.
        public setWeb(value: any): void {
            if (value !== this._web) {
                this._web = value;
                this.refresh();
            }
        }
     
        /**
         * This function is called by a custom binder.
         * It updates the value bound by MVVM in the ViewModel
         */
        public getWeb(): any {
            return this._web;
        }
     
        /**
         * This function is called by a custom binder.
         * It updates the UI with the value bound by MVVM from the ViewModel.
         * @param value
         * The value bound by MVVM.
         */
        public setList(value: any): void {
            if (this._list != value) {
                this._list = value;
                this.refresh();
            }
        }
     
        /**
         * This function is called by a custom binder.
         * It updates the value bound by MVVM in the ViewModel.
         */
        public getList(): any {
            return this._list;
        }
     
        /**
         * This function is called by a custom binder.
         * It updates the UI with the value bound by MVVM from the ViewModel.
         * @param value
         * The value bound by MVVM.
         */
        public setDataSourceOptions(value: HierarchicalDataSourceOptions): void {
            this._dataSourceOptions = value;
        }
     
        /**
         * This function is called by a custom binder.
         * It updates the value bound by MVVM in the ViewModel.
         */
        public getDataSourceOptions(): HierarchicalDataSourceOptions {
            return this._dataSourceOptions;
        }
     
        /**
         * This function is called when the control is cleared.
         * Here all internally used objects and bindings should be removed.
         */
        public destroy(): void {
            $(this.element).empty();
            super.destroy();
        }
     
        /**
         * This function reacts to the select event of a DropDownList control,
         * which belongs to Control internal UI to manage internal variables of the control.
         * In addition, the change event of the control is triggered to inform the custom binder,
         * that the bound value has changed and the ViewModel must be updated.
         * Also, if set, the functions 'select and 'change' from the options (SharePointObjectSelectorOptions)
         * to be able to retrieve values in the classic instantiation.
         * @param e
         * The Eventargs.
         */
        private _onSiteSelectionChange(e: kendo.ui.DropDownListChangeEvent): void {
            this._site = e.sender.dataItem(e.sender.select());
            let value = (<SharePointObjectSelectorOptions>this.options).valuePrimitive ? this._site.id : this._site
            if ((<SharePointObjectSelectorOptions>this.options).select !== undefined) {
                (<SharePointObjectSelectorOptions>this.options).select(<SharePointObjectSelectorSelectEvent>{ field: "site", value: value })
            }
            if ((<SharePointObjectSelectorOptions>this.options).change !== undefined) {
                (<SharePointObjectSelectorOptions>this.options).change(<SharePointObjectSelectorChangeEvent>{ field: "site" });
            }
            this.trigger(CHANGE, <SharePointObjectSelectorChangeEvent>{ field: "site" });
        }
     
        /**
         * This function reacts to the select event of a DropDownList control,
         * which belongs to Control internal UI to manage internal variables of the control.
         * In addition, the change event of the control is triggered to inform the custom binder,
         * that the bound value has changed and the ViewModel must be updated.
         * Also, if set, the functions 'select and 'change' from the options (SharePointObjectSelectorOptions)
         * to be able to retrieve values in the classic instantiation.
         * @param e
         * The Eventargs.
         */
        private _onWebSelectionChange(e: kendo.ui.DropDownListChangeEvent): void {
            let dataItem = e.sender.dataItem(e.sender.select());
            this._web = dataItem;
            if ((<SharePointObjectSelectorOptions>this.options).select !== undefined) {
                (<SharePointObjectSelectorOptions>this.options).select(<SharePointObjectSelectorSelectEvent>{ field: "web", value: this._site })
            }
            if ((<SharePointObjectSelectorOptions>this.options).change !== undefined) {
                (<SharePointObjectSelectorOptions>this.options).change(<SharePointObjectSelectorChangeEvent>{ field: "web" });
            }
            this.trigger(CHANGE, <SharePointObjectSelectorChangeEvent>{ field: "web" });
        }
     
        /**
         * This function reacts to the select event of a DropDownList control,
         * which belongs to Control internal UI to manage internal variables of the control.
         * In addition, the change event of the control is triggered to inform the custom binder,
         * that the bound value has changed and the ViewModel must be updated.
         * Also, if set, the functions 'select and 'change' from the options (SharePointObjectSelectorOptions)
         * to be able to retrieve values in the classic instantiation.
         * @param e
         * The Eventargs.
         */
        private _onListInputBlur(e): void {
            this._list = this._listInput.val();
            if ((<SharePointObjectSelectorOptions>this.options).select !== undefined) {
                (<SharePointObjectSelectorOptions>this.options).select(<SharePointObjectSelectorSelectEvent>{ field: "list", value: this._site })
            }
            if ((<SharePointObjectSelectorOptions>this.options).change !== undefined) {
                (<SharePointObjectSelectorOptions>this.options).change(<SharePointObjectSelectorChangeEvent>{ field: "list" });
            }
            this.trigger(CHANGE, <SharePointObjectSelectorChangeEvent>{ field: "list" });
        }
    }
     
    /**
     * Prepare the control to be used as kendo plug-in.
     */
    SharePointObjectSelector.fn = SharePointObjectSelector.prototype;
    (<any>SharePointObjectSelector.fn.options).name = "SharePointObjectSelector";
    kendo.ui.plugin(SharePointObjectSelector);
     
    /**
     * jQuery declaration to instantiate the control classically.
     */
    declare interface JQuery {
        kendoSharePointObjectSelector(options: SharePointObjectSelectorOptions): JQuery;
    }
     
    /**
     * Implementation of a custom binder for use via MVVM.
     */
    (<any>kendo.data).binders.widget.site = kendo.data.Binder.extend({
        /**
         * This function is called during binding. The event 'change' of the control is bound here.
         * The handling method updates when the event is fired using the 'change' function of the bind
         * the ViewModel. This way a two-way binding is realized.
         */
        init: function (element, bindings, options) {
            kendo.data.Binder.fn.init.call(this, element, bindings, options);
     
            var that = this;
            that.element.bind("change", function (e: SharePointObjectSelectorChangeEvent) {
                if (e.field === "site") {
                    that.change();
                }
            });
        },
        /**
         * This function is called when the bound ViewModel property (vm.set('change', { field: 'site' }) is changed.
         * It calls the relevant Custom Control function, which updates the value in the Control UI.
         */
        refresh: function () {
            var that = this,
                value = that.bindings.site.get();
     
            that.element.setSite(value);
        },
        /**
         * This function is called internally by the binder to update the corresponding value in the ViewModel.
         * Compare the function 'ini' of the binder.
         */
        change: function () {
            var value = this.element.getSite();
            this.bindings.site.set(value);
        }
    });
     
    /**
     * Implementation of a custom binder for use via MVVM.
     */
    (<any>kendo.data).binders.widget.web = kendo.data.Binder.extend({
        init: function (element, bindings, options) {
            kendo.data.Binder.fn.init.call(this, element, bindings, options);
     
            var that = this;
            that.element.bind("change", function (e: SharePointObjectSelectorChangeEvent) {
                if (e.field === "web") {
                    that.change();
                }
            });
        },
        refresh: function () {
            var that = this,
                value = that.bindings.web.get();
     
            that.element.setWeb(value);
        },
        change: function () {
            var value = this.element.getWeb();
            this.bindings.web.set(value);
        }
    });
     
    /**
     * Implementation of a custom binder for use via MVVM.
     */
    (<any>kendo.data).binders.widget.list = kendo.data.Binder.extend({
        init: function (element, bindings, options) {
            kendo.data.Binder.fn.init.call(this, element, bindings, options);
     
            var that = this;
            that.element.bind("change", function (e: SharePointObjectSelectorChangeEvent) {
                if (e.field === "list") {
                    that.change();
                }
            });
        },
        refresh: function () {
            var that = this,
                value = that.bindings.list.get();
     
            that.element.setList(value);
        },
        change: function () {
            var value = this.element.getList();
            this.bindings.list.set(value);
        }
    });
     
    /**
     * Implementierung eines Custom Binders für die Verwendung per MVVM.
     */
    (<any>kendo.data).binders.widget.dataSourceOptions = kendo.data.Binder.extend({
        init: function (element, bindings, options) {
            kendo.data.Binder.fn.init.call(this, element, bindings, options);
     
            var that = this;
            that.element.bind("change", function (e: SharePointObjectSelectorChangeEvent) {
                if (e.field === "dataSourceOptions") {
                    that.change();
                }
            });
        },
        refresh: function () {
            var that = this,
                value = that.bindings.dataSourceOptions.get();
     
            that.element.setDataSourceOptions(value);
        },
        change: function () {
            var value = this.element.getDataSourceOptions();
            this.bindings.dataSourceOptions.set(value);
        }
    });
  7. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 29 Apr in reply to Ralf Link to this post

    Forgotten to describe how to set binding in html. 

    Sample: 

    <div data-role="sharepointobjectselector" data-bind="site: selectedSite, web: selectedWeb, list: selectedList, dataSourceOptions: selectorOptions"></div>
  8. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 29 Apr in reply to Ralf Link to this post

    Ralf,

    What a fantasticly detailed example! Any reason you didn't call kendo.notify for binding? It is in all of their documentation and example, but it isn't really described anywhere.

  9. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 29 Apr in reply to Joshua Link to this post

    Hi Johsua,

    the notify function is only used to handle data- attribute binding for widget options such as data-text-field or data-is-selectable. So you can bind widget options via MVVM.

    See this explanation: https://www.telerik.com/forums/kendo-notify()

    Example:

    <select data-role='kendodropdown' data-text-field='text' data-value-field='id' />

     

    So you need to implment notify function only if data- attributes are needed and should bound from a viewmodel.

    If, for example, the widget should be handle enabled/disabled, you can add a property 'enabled' to the widget options and bind true/false by attribute data-enabled.

    Hope this will help.

     

    Kind regards,

    Ralf

  10. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 29 Apr Link to this post

    That seems right, all of my widgets do use data- attributes. My problem is that I can't figure out how to call notifiy via typescript, because it tells me the widget is not implemented properly.
  11. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 29 Apr Link to this post

    Can you post the error message?

    The widget script was loaded after kendo and jquery?

  12. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 29 Apr in reply to Ralf Link to this post

    I have not tried it with the pattern that your presented and I am doing that now, but the error I am getting is at design time and it is because of fn,prototype, extend are not present.

     

     

  13. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 29 Apr in reply to Joshua Link to this post

    What happens if you not use const that = this as RadoloTags; Instead use const that = this;

    Or do not use javascript implementation const widget = kendo.widget.extend. Its better to implement as I have done in my example.

    class MyWidget extends keno ui widget.

    Do not mix javascript and TypeScript. 

    And notify is not relevant for data-bind attribute I guess.

  14. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 29 Apr in reply to Ralf Link to this post

    As found out, you don't have to call kendo.notify directly. It's an internal function of mvvm functionallity.
  15. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 29 Apr in reply to Ralf Link to this post

    I am in the process of converting my widget to your typescript patter. I like it much better! Thank you for that.

    I have attached a screencap from where I used your sample to call notify with the same results.

     

    When I looked at the other thread your referenced, I noticed I am on that thread already... asking about what notify is used for...

    kendo.Notify has always been a real wierd black box.

  16. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 29 Apr in reply to Joshua Link to this post

    Just remove kendo.notify. It will be used inside mvvm functions. You can add an event listener for notify event in custom binder. But this is not needed for implementing a widget. If you take a look inside binder source code, you will see that notify function will be called inside bind function. The bind function will be called automatically when html will be rendered and kendo need to create the widget. Then kendo checking html and the node attributes. Based on this kendo will check for binders (build in and custom). If binders are found kendo will handle binding to viewmodel. On binder init the viewmodel value will be past through widget set- function (setSite for example). And if widget change event will be fired, the binder function change will be called. This function will call the widget get- function (getSite for example), and updates the viewmodel. Thats all. 
    Telerik should update the documentation to explain detailed how widgets (specially custom widgets) and mvvm works. Its a little bit difficult to understand. 

    Some Telerik blog articles how to create custom widgets:

    https://www.telerik.com/blogs/creating-a-kendo-ui-mvvm-widget

    https://www.telerik.com/blogs/creating-a-datasource-aware-kendo-ui-widget

    https://www.telerik.com/blogs/creating-custom-kendo-ui-plugins

    Hope this will help.

     

    Kind regards,

    Ralf

  17. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 29 Apr in reply to Ralf Link to this post

    Ralf, You have spent a lot of time putting this stuff together. I appreciate it very much. Thank you.

    I am very familiar with those blogs they have been around soooo long and could probably use a refreshing. I will start removing notify from my widgets and see what sort of behavior they start displaying.

  18. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 30 Apr in reply to Joshua Link to this post

    Don't mention it. I have spent alot of time to find out how to create custom widgets too. :-)

    I have checked, and found kendo.notify was added to the libarary after release 20017.x or typings of this version was not complete. So I have updated my sample project and added 'kendo.notify(this as any);' in the constructor after 'this.initialize()'.
    This works and function will be called using the right object (widget). My widget is working with and also without keondo.notify. 

    I only cannot figure out how options will be applied from mvvm declaration (data-enabled for example).

    Maybe telerik professionals can describe it? 

     

    Best regards,

    Ralf

  19. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 30 Apr in reply to Ralf Link to this post

    In order to get the options working they need to be present when the widget binds.

     

    At the bottom of your code you set the name property of options.

    (<ISampleOptions>SampleWidget.options).name="SampleWidget";

     

    You need to set all of the properties you intend to bind with their defaults.

     

    (<ISampleOptions>SampleWidget.options) = {name:"SampleWidget",Property1:"some Vue",enabled:true};

     

    Then the options will be parsed in the constructor via super(element, options);

  20. Ralf
    Ralf avatar
    86 posts
    Member since:
    Jun 2012

    Posted 30 Apr in reply to Joshua Link to this post

    Hello Joshua,

    thank you for information. Now I can maintain our apps to make handling better.

     

    Best regards,

    Ralf

  21. Joshua
    Joshua avatar
    82 posts
    Member since:
    May 2012

    Posted 30 Apr in reply to Ralf Link to this post

    Glad to help.

     

    One of the other things I do to make the options be type safe are override with the actual options class.

     

    /**
       * Composite Widget that encapsulates Tag functionality
       */
       class RadoloTags extends kendo.ui.Widget implements IRadoloTags {
     
           /**
           * The events which can be bound from external.
           */
           public events: Array<string> = [
               CHANGE
           ];
     
           public options: RadoloTagsOptions;
     }
Back to Top