Condividi tramite


Building a custom control using the Windows Library for JavaScript (WinJS)

If you have developed Windows Store apps using JavaScript, you most likely have encountered the Windows Library for JavaScript (WinJS). This library provides you with a set of CSS styles, JavaScript controls and utilities to help you quickly build apps that meet the UX guidelines for the Windows Store. Among the utilities provided by WinJS are a set of functions you can use to create custom controls in your app.

You can write JavaScript controls using any patterns or libraries you like; the library functions provided in WinJS are just one option. The main benefit of using WinJS to build your controls is that it allows you to create your own controls that work consistently with the other controls in the library. The patterns for developing and working with your control are the same as any control in the WinJS.UI namespace.

In this post, I’ll show you how to build your own controls, with support for configurable options, events, and public methods. For those of you interested in the same story for XAML control development, look for a post on that soon.

Including a JavaScript-based control on an HTML page

First, let’s revisit how you include a WinJS control in a page. There are two different ways to do this: imperatively (using JavaScript alone in an un-obtrusive manner) or declaratively (including controls in your HTML page by using additional attributes on HTML elements). The latter enables tools to provide a design-time experience, such as dragging controls from a toolbox. Take a look at the MSDN quick start for adding WinJS controls and styles for more info.

In this article, I’ll show you how to generate a JavaScript control that can benefit from the declarative processing model in WinJS. To include a control declaratively in your page, complete this series of steps:

  1. Include WinJS references in your HTML page, because your control will use APIs from these files.

     <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
    
  2. After the script tag references above, include a reference to a script file containing your control.

     <script src="js/hello-world-control.js"></script>
    
  3. Call WinJS.UI.processAll() in your app’s JavaScript code;. This function parses your HTML and instantiates any declarative controls it finds. If you’re using the app templates in Visual Studio, WinJS.UI.processAll() is called for you in default.js.

  4. Include the control in your page, declaratively.

     <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}"></div>
    

A simple JavaScript control

Now, we’ll build a very simple control: the Hello World of controls. Here’s the JavaScript used to define the control. Create a new file in your project named hello-world-control.js with the following code:

 function HelloWorld(element) {
    if (element) {
        element.textContent = "Hello, World!";
    }
};

WinJS.Utilities.markSupportedForProcessing(HelloWorld);

Then, in the body of your page, include the control using the following markup:

 <div data-win-control="HelloWorld"></div>

When you run your app, you’ll see that the control has been loaded and it displays the text “Hello, World!” in the body of your page.

The only piece of this code specific to WinJS is the call to WinJS.Utilities.markSupportedForProcessing, which marks code as compatible for use with declarative processing. This is your way to tell WinJS that you trust this code to inject content into your page. For more info about this, see the MSDN documentation for the WinJS.Utilities.markSupportedForProcessing function.

Why use the WinJS utilities, or any library, to create a control?

I just showed you how you can create a declarative control without really using WinJS. Now, look at the following code snippet, which still isn’t using WinJS for the bulk of its implementation. This is a more complex control with events, configurable options, and public methods:

 (function (Contoso) {
    Contoso.UI = Contoso.UI || {};

    Contoso.UI.HelloWorld = function (element, options) {
        this.element = element;
        this.element.winControl = this;

        this.blink = (options && options.blink) ? true : false;
        this._onblink = null;
        this._blinking = 0;

        element.textContent = "Hello, World!";
    };

    var proto = Contoso.UI.HelloWorld.prototype;

    proto.doBlink = function () {
        var customEvent = document.createEvent("Event");
        customEvent.initEvent("blink", false, false);

        if (this.element.style.display === "none") {
            this.element.style.display = "block";
        } else {
            this.element.style.display = "none";
        }

        this.element.dispatchEvent(customEvent);
    };

    proto.addEventListener = function (type, listener, useCapture) {
        this.element.addEventListener(type, listener, useCapture);
    };

    proto.removeEventListener = function (type, listener, useCapture) {
        this.element.removeEventListener(type, listener, useCapture);
    };

    Object.defineProperties(proto, {
        blink: {
            get: function () {
                return this._blink;
            },

            set: function (value) {
                if (this._blinking) {
                    clearInterval(this._blinking);
                    this._blinking = 0;
                }
                this._blink = value;
                if (this._blink) {
                    this._blinking = setInterval(this.doBlink.bind(this), 500);
                }
            },
            enumerable: true,
            configurable: true
        },

        onblink: {
            get: function () {
                return this._onblink;
            },
            set: function (eventHandler) {
                if (this._onblink) {
                    this.removeEventListener("blink", this._onblink);
                    this._onblink = null;
                }
                this._onblink = eventHandler;
                this.addEventListener("blink", this._onblink);
            }
        }
    });

    WinJS.Utilities.markSupportedForProcessing(Contoso.UI.HelloWorld);
})(window.Contoso = window.Contoso || {}); 

Many developers build controls this way (using anonymous functions, constructor functions, properties, custom events) and if it’s something your team is comfortable with, go for it! However, for many developers this code might be a bit confusing. Many web developers aren’t familiar with the techniques used. This is where libraries come in really handy – they help remove some of the confusion around writing this code.

Beyond improving readability, WinJS and other libraries take care of many subtle issues so you don’t have to think about them (efficiently using prototypes, properties, custom events). They optimize for memory utilization and help you avoid common mistakes. WinJS is one such example, but the choice is yours. For a concrete example of how a library can help you, I’d suggest reviewing the code in this section again, after you finish the article, and compare the previous implementation with the same control implemented at the end of the article using the WinJS utilities.

A base pattern for JavaScript controls in WinJS

Below is a minimal best practice pattern for creating a JavaScript control with WinJS.

 (function () {
    "use strict";

    var controlClass = WinJS.Class.define(
            function Control_ctor(element) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                this.element.textContent = "Hello, World!"
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });
})();

And in the page you include the control declaratively:

 <div data-win-control="Contoso.UI.HelloWorld"></div>

Some of this might not be familiar to you, especially if this is your first exposure to WinJS, so let’s walk through what’s going on.

  1. We’re wrapping the code in this example with a common pattern in JavaScript known as an immediately-executed function.

     (function () {
    …
    })();
    

    This is done to ensure that our code is self-contained and doesn’t leave behind any unintentional variables/global assignments. It’s worth noting that this is a general best practice and is a change we’d likely want to make to the original sour.

  2. ECMAScript 5 strict mode is enabled with the use of the "use strict" statement at the start of our function. We do this as a best practice throughout the Windows Store app templates to improve error checking and compatibility with future versions of JavaScript. Again, this is a general best practice and something we’d want to do with the original source as well.

  3. Now, we get into some code that’s specific to WinJS. WinJS.Class.define() is called to create a class for the control which, among other things, will handle the call to markSupportedForProcessing() for us and will also ease the future creation of properties on the control. It’s really just a simple helper around the standard Object.defineProperties function.

  4. A constructor, named Control_ctor, is defined. When WinJS.UI.processAll() is called from default.js, it scans the markup in the page for any controls referenced using the data-win-control attribute, finds our control, and calls this constructor.

  5. Within the constructor, a reference to the element on the page is stored with the control object and a reference to the control object is stored with the element.

    • If you’re wondering what the element || document.createElement("div") piece is about, this is used to support the imperative model. This allows a user to attach a control to an element on the page later.
    • It’s a good idea to maintain a reference to the element on the page this way, as well as maintain a reference on the element to the control object by setting element.winControl. When adding in functionality like events, this allows some library functions to just work. Don’t worry about memory leaks resulting from a circular object/DOM element reference, Internet Explorer 10 will take care of this for you.
    • The constructor modifies the text content of the control to set the “Hello, World!” text you see on the screen.
  6. Finally, WinJS.Namespace.define() is used to publish the control class and expose the control publically to be accessed by any code in our app. Without this, we would have to work out another solution for exposing our control using the global namespace to code outside the inline function in which we’re working.

Defining control options

To make our example a bit more interesting, let’s add support for configurable options to our control. In this case, we’ll add an option to allow the user to blink the content.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

This time, when including the control on your page, you can configure the blink option using the data-win-options attribute:

 <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}">
</div>

To add support for options, we made these changes to the code:

  1. Options are passed to the control via a parameter (called options) on the constructor function.
  2. Default settings are configured using private properties on the class.
  3. WinJS.UI.setOptions() is called, passing in your control object. This call overrides default values for configurable options on the control.
  4. A public property (called blink) is added for the new option.
  5. We added functionality by blinking the text on screen (in practice, you’d be better off not hard-coding the styles in here, but instead toggling a CSS class)l

The part doing the heavy lifting in this example is the call to WinJS.UI.setOptions(). A utility function, setOptions , cycles through each field in the options object and assigns its value to a field of the same name on the target object, which is the first parameter to setOptions.

In our example, we configure the options object via the data-win-options argument for our win-control, passing in a value of true for the field “blink.” The call to setOptions() in our constructor function will then see the field named “blink” and copy its value into a field of the same name on our control object. We’ve defined a property named blink and it provides a setter function; our setter function is what is called by setOptions() and this sets the _blink member of our control.

Adding support for events

With the oh-so-useful blink option now implemented, let’s add in event support so that we can respond whenever a blink occurs:

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

Include the control in the page, as before. Notice that we’ve added an id to the element so that we can retrieve the element later:

 <div id="hello-world-with-events"
    data-win-control="Contoso.UI.HelloWorld"
    data-win-options="{blink: true}"></div>

With these changes in place, we can now attach an event listener to listen for the “blink” event (Note: I’ve aliased $ to document.getElementById in this example):

 $("hello-world-with-events").addEventListener("blink",
        function (event) {
            console.log("blinked element this many times: " + event.count);
        });

When you run this code you’ll see a message written out every 500 milliseconds to the JS Console window in Visual Studio.

There were 3 changes made to the control to support this behavior:

  1. A call is made to WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.Utilities.createEventProperties("blink")); which creates an “onblink” property that users can set programmatically or to which users can bind declaratively in the HTML page.
  2. A call to WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.UI.DOMEventMixin) adds addEventListener, removeEventListener, and dispatchEvent functions to the control.
  3. The blink event is fired by calling that.dispatchEvent("blink", {element: that.element}); and a custom event object is created with an element field.
  4. An event handler is attached to listen for the blink event; in response, it accesses the element field of the custom event object.

I’ll point out here that calls to dispatchEvent()only work if you have set this.element in your control’s constructor; the internals of the event mix-in require it to access the element in the DOM. This is one of those cases I mentioned earlier, in which an element member is required on the control object. This allows events to bubble up to parent elements in the page in a DOM Level 3 event pattern.

Exposing public methods

As a final change to our control, let’s add a public doBlink() function that can be called at any time to force a blink.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this.doBlink.bind(this), 500);
                        }
                    }
                },

                doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

It’s merely a convention change – we can change the name of our _doBlink function to doBlink.

To call the doBlink() function via JavaScript, you need a reference to the object for your control. If you create your control imperatively, you might already have a reference;. If you use declarative processing, you can access the control object using a winControl property on the HTML element for your control. For example, assuming the same markup as before, you can access the control object via:

$("hello-world-with-events").winControl.doBlink();

Bringing it all together

We’ve just worked through the most common aspects of a control that you’ll want to implement:

  1. Getting your control onto a page.
  2. Passing in configuration options.
  3. Dispatching and responding to events.
  4. Exposing functionality via public methods.

I hope you’ve found this tutorial useful for showing you how to build a simple JavaScript-based custom control! If you have questions while working on your own controls, head over to the Windows Dev Center and ask questions in the forums. Also, if you’re a XAML developer, look for a post coming soon that walks you through this same story for XAML control development.

Jordan Matthiesen 
Program Manager, Microsoft Visual Studio