Building Windows 8 blog
Windows Store for developers blog
Visual Studio blog
The Windows blog
Inside SkyDrive blog
Download Windows 8 Release Preview
Windows Dev Center
Follow us @WinDevs
The //build/ conference
Developer forums
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.
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:
<script src="//Microsoft.WinJS.1.0/js/base.js"></script><script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<script src="js/hello-world-control.js"></script>
<div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}"></div>
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.
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.
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.
(function () {…})();
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:
To add support for options, we made these changes to the code:
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.
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:
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.
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();
We’ve just worked through the most common aspects of a control that you’ll want to implement:
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