Custom Windows 8 HTML Controls

Oct 12 2012 Published by under Windows 8

Send to Kindle

Building native Windows 8 apps in HTML and JavaScript is – dare I say it? – fun. I’ve been enjoying learning and building things with JavaScript over the last couple of years, but the cross-platform/cross-browser/cross-version issues can become a pain. And the 10 million competing libraries and micro-frameworks can be overwhelming. Everyone has their favorite and is happy to shove it down your throat with the slightest provocation.

But developing apps for Windows 8 has a kind of purity. There’s a single system and a single “browser”. The framework/library code is built in. Yeah, I’m sure you could probably pull jQuery or Backbone or whatever else in to help create your app, but there’s far less need.

There’s also the fact that you have various OS hooks, of which I’ve only scratched the surface so far.

Anyway, if you’ve been doing web dev in JS, you just might find doing Windows 8 apps a breath of fresh air. You’re using all the same tools – HTML, JS, CSS. You can use just about every standard HTML ui element, and with the default CSS files, they are rendered like standard Windows 8 controls.

But one of the more powerful features is custom controls. These are created by creating a div and giving it a data-win-control attribute of the name of a JavaScript “class”. When your app launches, you call args.setPromise(WinJS.UI.processAll()). This looks through the HTML for any divs with the data-win-control attribute and instantiates the class referenced there. This class builds up the custom element’s view and logic inside that div. There are a bunch of built-in custom controls in the WinJS.UI package, such as the AppBar, DatePicker, Lists, Rating control, Flyouts, Menus, Tooltips, etc. In addition, you can set a data-win-options attribute with a stringified object containing option name/values. These help to customize the new control.

So if you wanted to create a Date Picker, you just put this in your HTML:

<div id="date" data-win-control="WinJS.UI.DatePicker">

And if you wanted to set the current date in the picker, you can pass that as an option:

<div data-win-control="WinJS.UI.DatePicker" data-win-options="{current:'10/12/2012'}">

But even more powerful is the ability to create your own custom controls. Let’s do it. Create a new blank Windows 8 JavaScript project in VS2012. Create a new JavaScript file in the js folder. Name it DummyControl.js and fill it with this code:

WinJS.Namespace.define("bit101", {
    DummyControl: WinJS.Class.define(
        function (element, options) {
            // constructor
            console.log(element);
            console.log(options);
        }, {
            // instance members
        }, {
            // static members
        })
    }
);

Here we are defining a namespace called bit101. It will consist of an object with a single property called DummyControl. And bit101.DummyControl will be a pseudoclass defined with WinJS.Class.define. I know, I know. But that’s how it’s done and it’s pretty straightforward. The define method takes 3 arguments. The first is a function which becomes the class’s constructor. The next is an object containing instance members and then an object containing any static members. You can see that the constructor function gets passed the element that the control is instantiated in, and the options that are passed in in the data-win-options attribute.

You can now add the script reference and an instance of this control to your default.html file and run the app.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>CustomControls</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <!-- CustomControls references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
    <script src="/js/DummyControl.js"></script>
</head>
<body>
    <div id="dummy" data-win-control="bit101.DummyControl" data-win-options="{}"></div>
</body>
</html>

I passed in an empty object to options just so we could see something traced out. You should see this in your output:

[object HTMLDivElement][object Object]

Now we can start building out the control to actually do something. Instead of just logging them, we’ll save the parameters and call an init function. The init function will create a new h1 tag and append it to the control’s element:

WinJS.Namespace.define("bit101", {
    DummyControl: WinJS.Class.define(
        function (element, options) {
            // constructor
            this.element = element;
            this.options = options;
            this.init();
        }, {
            // instance members
            init: function () {
                this.h1 = document.createElement("h1");
                this.h1.innerText = this.options.message || "Hello world";
                this.element.appendChild(this.h1);
            }
        }, {
            // static members
        })
    }
);

The h1 tag’s inner text is set to the message property of the options, if it exists, otherwise it defaults to “Hello World”. Running this now should result in a large “Hello world” being displayed in the app.

We can then set the message to whatever we want via the options:

<div id="dummy" data-win-control="bit101.DummyControl" data-win-options="{message:'Windows 8 Rocks'}"></div>

Additionally, you can create methods that can be called on the control itself from external scripts. Let’s make a new function that allows us to change the message:

WinJS.Namespace.define("bit101", {
    DummyControl: WinJS.Class.define(
        function (element, options) {
            // constructor
            this.element = element;
            this.options = options;
            this.init();
        }, {
            // instance members
            init: function () {
                this.h1 = document.createElement("h1");
                this.h1.innerText = this.options.message || "Hello world";
                this.element.appendChild(this.h1);
            },

            setMessage: function (message) {
                this.h1.innerText = message;
            }
        }, {
            // static members
        })
    }
);

But how do we access that? Well first, notice that I gave the div containing the control an id of “dummy”, so it’s easy enough to find the div by saying document.getElementById(“dummy”). But that just gives us the div that contains the control, not the control itself. For that we access the winControl property of that div, like so:

var dummyControl = document.getElementById("dummy").winControl;

But, a very important thing to remember here: Earlier in the article I mentioned that calling WinJS.UI.processAll finds all the data-win-control divs and instantiates their controls. So you need to A. call this function, and B. wait for it to complete. The function uses the promises pattern, so all we have to do is something like this:

            args.setPromise(WinJS.UI.processAll().then(function () {
                var dummyControl = document.getElementById("dummy").winControl;
                dummyControl.setMessage("It works!");
            }));

Of course, this is a rather simple example created for demonstration purposes. In a real control, you can add all the elements, event handlers, styles, and behavior you want in there and create some pretty complex components. As an example, I’ve built a pretty full featured control called FPSDisplay. This displays the current frame rate of a running app based on requestAnimationFrame and a running bar graph of how the frame rate has changed over the recent past. All you have to do is include the fpsDisplay.js file in your HTML and create an instance like so:

<div id="fps" data-win-control="bit101.FPSDisplay" ></div>

Options include color, bgColor and position, which can be “tl”, “tr”, “bl”, “br” (top left, top right, etc.). Methods include start and stop which do the obvious.

Here’s the code in full for you to peruse though. I’m not going to comment on it though. Save it out and add it to your project and see how it goes.

WinJS.Namespace.define("bit101", {
    FPSDisplay: WinJS.Class.define(function (element, options) {
        if (!element) {
            throw "element must be provided";
        }
        this.options = options || { position: "tl" };
        this.element = element;
        this.init();
    }, {
        init: function () {
            this.child = document.createElement("div");
            this.child.style.position = "absolute";
            this.setPosition(this.options.position);
            this.element.appendChild(this.child);

            var canvas = document.createElement("canvas");
            canvas.width = 100;
            canvas.height = 50;
            
            this.context = canvas.getContext("2d");
            this.context.fillStyle = "white";
            this.context.fillRect(0, 0, 100, 50);
            this.context.font = "10px Arial";

            this.child.appendChild(canvas);

            this.text = document.createElement("div");
            this.text.style.font = "10px Arial";
            this.text.style.color = this.options.color || "black";
            this.text.style.backgroundColor = this.options.bgColor || "white";
            this.text.style.position = "absolute";
            this.text.style.top = "1px";
            this.text.style.left = "2px";
            this.child.appendChild(this.text);

            this.onVisibilityChanged = this.onVisibilityChanged.bind(this);
            document.addEventListener("visibilitychange", this.onVisibilityChanged, false);
            this.history = [];
            this.update = this.update.bind(this);
            this.start();
        },

        onVisibilityChanged: function() {
            if (document.visibilityState === "hidden") {
                this.lastTime = 0;
                this.frames = 0;
                this.elapsed = 0;
            }
        },

        start: function () {
            this.frames = 0;
            this.isRunning = true;
            this.elapsed = 0;
            this.lastTime = 0;
            this.animRequest = window.requestAnimationFrame(this.update);
        },

        stop: function () {
            this.isRunning = false;
            cancelAnimationFrame(this.animRequest);
        },

        update: function (time) {
            if (!this.lastTime) {
                this.lastTime = time;
                this.animRequest = window.requestAnimationFrame(this.update);
                return;
            }

            this.elapsed += time - this.lastTime;
            this.frames += 1;
            this.lastTime = time;
            this.animRequest = window.requestAnimationFrame(this.update);
            if (this.elapsed > 1000) {
                this.frames = Math.round(this.frames * 1000 / this.elapsed);
                this.elapsed = 0;
                this.draw();
            }
        },

        draw: function () {
            var min = 1000;
            var max = 0;
            for (var i = Math.max(this.history.length - 10, 0); i < this.history.length; i += 1) {
                min = Math.min(min, this.history[i] - 2);
                max = Math.max(max, this.history[i] + 2);
            }
            var range = max - min;
            this.history.push(this.frames);
            if (this.history.length > 50) {
                this.history.shift();
            }
            this.context.fillStyle = this.options.bgColor || "white";
            this.context.fillRect(0, 0, 100, 50);
            this.context.fillStyle = this.options.color || "black";
            for (var i = 0; i < this.history.length; i += 1) {
                var value = this.history[i];
                var h = (value - min) / range * 50;
                this.context.fillRect(i * 2, 50 - h, 1, h);
            }
            this.text.innerHTML = this.frames + " fps";
            this.frames = 0;
        },

        setPosition: function (pos) {
            // default tl
            this.child.style.left = "0px";
            this.child.style.top = "0px";

            if(pos.indexOf("r") != -1) {
                this.child.style.left = (window.innerWidth - 100) + "px";
            }
            if(pos.indexOf("b") != -1) {
                this.child.style.top = (window.innerHeight - 50) + "px";
            }
        }

    }
)
});
Send to Kindle

3 responses so far. Comments will be closed after post is one year old.

  • MarkR says:

    on the FPS code line: this.options = options || { positon: “tl” };

    You are missing an “i” for the word “position”.

    So it should be

    this.options = options || { position: “tl” };

  • sigutis says:

    Useful article, but do you know a way to force your control styling without specifying every single style and setting as !important?. I’ve found that the styling of the control still can be altered from the outside otherwise.