Friday, March 29, 2024

Create a Windows 8 app in HTML5, CSS3 and JavaScript

The goal of these articles is to provide a pragmatic recipe to create a complete Windows 8 application from scratch.

The application I use as sample is called UrzaGatherer and it is used to help Magic The Gathering collectors to handle their cards collection.

 

UrzaGatherer was originally developed using WPF 4.0 (http://urzagatherer.codeplex.com/) but I decided to use HTML5, CSS3 and JavaScript for developing the Windows 8 version.

You will need the following to start creating the application:

The complete solution can be find there: http://www.catuhe.com/msdn/urza/day0.zip 

Creating the project

First of all, you have to create a blank project (You can obviously create a more prepared project like the ‘Grid application’ but the goal here is to understand all things are tied up) using the File/New Project menu:

The project is created with only required files:

Creating required assets

The package.appxmanifest is the file that describes your application to Windows 8. It contains especially the description of the application alongside with the associated logos:

I love adding logos and colors to my application because it is a simple way to polish it.

The splash screen for example is really important because it is the first thing a user see of your application (and you should know that the very first contact is really significant):

This is sometimes the harder part of the development because developers are not often designers. 

Structuring the project

This part is really dependent on your way of thinking. Personally, I chose to create this project layout:

  • A folder for my assets (/images)
  • A folder for my JavaScript code which is not related to pages (/js)
  • A folder for the pages (/pages)
  • A folder for every page (/pages/xxx) where I create css, js and html files (home.js, home.html, home.css)
  • A root page called default.html (with an associated .css and .js)

 

Connecting to data

Once the assets and the project structure are done, you can add a data.js file in the js folder to handle all that is related to data.

For UrzaGatherer, the data is composed of:

  • a all.json file which describes all the supported cards
  • a list of cards pictures
  • A list of logo for each expansion (cards belong to an expansion which in turn belong to a block)
  • A list of logo for each block

So starting with you empty data.js file, you have to created  an automatic anonymous function:

(function () { })();

Inside this function you can connect to your data. For UrzaGatherer, the data is stored in a json file which is too big to be downloaded every time (~ 8MB) so you have to load it only once and save it locally:

(function () {

    var blocks = new WinJS.Binding.List();
    var expansions = new WinJS.Binding.List();
    var root = "http://urzagatherer.blob.core.windows.net";

    var processBlocks = function (data) {
        var result = JSON.parse(data);

        for (var blockIndex = 0; blockIndex < result.length; blockIndex++) {
            var block = result[blockIndex];

            block.logo = root + "/blocks/" + block.name.replace(":", "_") + ".png";
            blocks.push(block);

            var sortedExpansions = block.expansions.sort(expansionSorter);

            for (var expansionIndex = 0; expansionIndex < sortedExpansions.length; expansionIndex++) {
                var expansion = sortedExpansions[expansionIndex];
                expansion.block = block;
                expansion.logo = root + "/logos/" + expansion.name.replace(":", "_") + ".png";
                expansions.push(expansion);
            }
        }
    }

    var getBlocksDistant = function (onload) {
        var localFolder = Windows.Storage.ApplicationData.current.localFolder;
        var requestStr = root + "/cards/all.json";

        WinJS.xhr({ url: requestStr }).then(function (request) {
            processBlocks(request.responseText);

            localFolder.createFileAsync("all.json", 
Windows.Storage.CreationCollisionOption.replaceExisting).then(
function (file) { Windows.Storage.FileIO.writeTextAsync(file, request.responseText); }); if (onload) onload(); }); } var getBlocks = function (onload) { var localFolder = Windows.Storage.ApplicationData.current.localFolder; localFolder.getFileAsync("all.json").done(function (file) { return Windows.Storage.FileIO.readTextAsync(file).then(function (data) { processBlocks(data); if (onload) onload(); }); }, function () { getBlocksDistant(onload); }); } var expansionSorter = function (i0, i1) { if (i0.orderInBlock > i1.orderInBlock) return 1; else if (i0.orderInBlock < i1.orderInBlock) return -1; return 0; }; WinJS.Namespace.define("UrzaGatherer", { Blocks: blocks, Expansions: expansions, Init: getBlocks }); })();

Using the WinJS.Namespace.define, you can declare a global object (called UrzaGatherer) available everywhere in your code.

The Init function starts by trying to load data locally and if it fails, it will download it using WinJS.xhr (http://msdn.microsoft.com/en-us/library/windows/apps/br229787.aspx). Init also takes a function in parameter to signal the availability of the data. I use this function to hide my wait ring (a progress bar in “ring” mode).

Preparing the landing page

Navigation system

The default.html page is the landing page i.e. the page where the user lands after launching the application. This page is responsible for creating the navigation system:

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

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

    <!-- UrzaGatherer references -->
    <link href="/default.css" rel="stylesheet">
    <script src="/js/data.js"></script>
    <script src="/js/tools.js"></script>
    <script src="/js/navigator.js"></script>
    <script src="/default.js"></script>
</head>
<body>
    <div id="contenthost" data-win-control="UrzaGatherer.PageControlNavigator" 
data-win-options="{home: '/pages/home/home.html'}"></div> </body> </html>

The page is pretty simple: it references the WinJS files and then load the default stylesheets and the JavaScript code.

It only contains one div which is the host where sub-pages will be loaded. This is an important point for understanding how the navigation works when you use HTML5/JavaScript for Windows 8. Indeed, the pages are not loaded as root pages but as children pages of the default.html page.

To handle this functionality, you have to create the navigator.js page (You can copy it from any project templates in Visual Studio 11 such as ‘Grid Application’):

(function () {
    "use strict";

    var appView = Windows.UI.ViewManagement.ApplicationView;
    var displayProps = Windows.Graphics.Display.DisplayProperties;
    var nav = WinJS.Navigation;
    var ui = WinJS.UI;
    var utils = WinJS.Utilities;

    WinJS.Namespace.define("UrzaGatherer", {
        PageControlNavigator: WinJS.Class.define(
        // Define the constructor function for the PageControlNavigator.
            function (element, options) {
                this.element = element || document.createElement("div");
                this.element.appendChild(this._createPageElement());

                this.home = options.home;

                nav.onnavigated = this._navigated.bind(this);
                appView.getForCurrentView().onviewstatechanged = this._viewstatechanged.bind(this);

                document.body.onkeyup = this._keyupHandler.bind(this);
                document.body.onkeypress = this._keypressHandler.bind(this);
                nav.navigate(this.home);
            }, {
                // This function creates a new container for each page.
                _createPageElement: function () {
                    var element = document.createElement("div");
                    element.style.width = "100%";
                    element.style.height = "100%";
                    return element;
                },

                // This function responds to keypresses to only navigate when
                // the backspace key is not used elsewhere.
                _keypressHandler: function (eventObject) {
                    if (eventObject.key === "Backspace")
                        nav.back();
                },

                // This function responds to keyup to enable keyboard navigation.
                _keyupHandler: function (eventObject) {
                    if ((eventObject.key === "Left" && eventObject.altKey) 
|| (eventObject.key ===
"BrowserBack")) { nav.back(); } else if ((eventObject.key === "Right" && eventObject.altKey)
|| (eventObject.key ===
"BrowserForward")) { nav.forward(); } }, // This function responds to navigation by adding new pages // to the DOM. _navigated: function (eventObject) { var newElement = this._createPageElement(); var parentedComplete; var parented = new WinJS.Promise(function (c) { parentedComplete = c; }); var that = this; WinJS.UI.Pages.render(eventObject.detail.location, newElement,
eventObject.detail.state, parented). then(
function (control) { that.element.appendChild(newElement); that.element.removeChild(that.pageElement); parentedComplete(); document.body.focus(); that.navigated(); }); }, // This function is called by _viewstatechanged in order to // pass events to the page. _updateLayout: { get: function () { return (this.pageControl && this.pageControl.updateLayout) || function () { }; } }, _viewstatechanged: function (eventObject) { (this._updateLayout.bind(this.pageControl))(this.pageElement, eventObject.viewState); }, // This function updates application controls once a navigation // has completed. navigated: function () { // Do application specific on-navigated work here var backButton = this.pageElement.querySelector("header[role=banner] .win-backbutton"); if (backButton) { backButton.onclick = function () { nav.back(); }; if (nav.canGoBack) { backButton.removeAttribute("disabled"); } else { backButton.setAttribute("disabled", "disabled"); } } }, // This is the PageControlNavigator object. pageControl: { get: function () { return this.pageElement && this.pageElement.winControl; } }, // This is the root element of the current page. pageElement: { get: function () { return this.element.firstElementChild; } } } ), // This function navigates to the home page which is defined when the // control is created. navigateHome: function () { var home = document.querySelector("#contenthost").winControl.home; var loc = nav.location; if (loc !== "" && loc !== home) { nav.navigate(home); } }, }); })();
 

As you can see, the PageControlNavigator class is a control that load a page and add it as a child element after removing the previous page. You have to well understand this point because it implies that all loaded css and scripts remain active and exist in a unique and single global page (http://msdn.microsoft.com/en-us/library/windows/apps/hh452768.aspx).

Applying styles to the page

The default.css stylesheet is the root stylesheet and as you know now, it is applied to every loaded page. This file is responsible for setting the global structure such as:

  • A potential background
  • A layout with a room for the banner (title and back button) and the content

I also use it to store global style used everywhere in my application such as the hidden class (used to hide elements):

html {
    cursor: default;
}

body {
    background-image: url('images/background.jpg');
    background-size: 100% 100%
}

#contenthost {
    height: 100%;
    width: 100%;
}

.fragment {
    /* Define a grid with rows for a banner and a body */
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 133px 1fr 0px;
    display: -ms-grid;
    height: 100%;
    width: 100%;
}

.fragment header[role=banner] {
    /* Define a grid with columns for the back button and page title. */
    -ms-grid-columns: 120px 1fr;
    -ms-grid-rows: 1fr;
    display: -ms-grid;
}

.fragment header[role=banner] .win-backbutton {
    margin-left: 39px;
    margin-top: 59px;
}

.fragment header[role=banner] .titlearea {
    -ms-grid-column: 2;
    margin-top: 37px;
}

.fragment header[role=banner] .titlearea .pagetitle {
    width: calc(100% - 20px);
}

.fragment section[role=main] {
    -ms-grid-row: 2;
    height: 100%;
    width: 100%;
}

.hidden {
    display: none;
}
 
 
 

You will notice that I use display:-ms-grid everywhere I can because I think it is really easy to construct the layout and the alignment using the CSS3 grid system (http://msdn.microsoft.com/en-us/library/windows/apps/hh465327.aspx).

For the background, you can obviously ask a designer to help you but you can also use a simple graphic tool to create thin gradient like this one:

Creating the home screen

The first visible screen is the home screen where I want to display the available blocks with theirs expansions.

The base version of the page is like the following :

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>UrzaGatherer</title>
    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.0.6/css/ui-light.css" rel="stylesheet">
    <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
    <script src="//Microsoft.WinJS.0.6/js/ui.js"></script>
    <!-- UrzaGatherer references -->
    <link href="home.css" rel="stylesheet">
    <script src="home.js"></script>
</head>
<body>
    <!--Content-->
    <div class="home fragment">
        <header aria-label="Header content" role="banner">
            <button class="win-backbutton" aria-label="Back" disabled></button>
            <h1 class="titlearea win-type-ellipsis"><span class="pagetitle">UrzaGatherer</span>
            </h1>
        </header>
        <section aria-label="Main content" role="main">

        </section>
    </div>
</body>
</html>

You can note the banner (header) and a section to put the content.

To do so, I used a WinJS.UI.ListView. This control can be used to display a grouped list of values (here it displays a list of expansions grouped by blocks):

 
<div class="blocksList" aria-label="List of blocks" data-win-control="WinJS.UI.ListView"
    data-win-options="{itemTemplate:select('.itemTemplate'), groupHeaderTemplate:select('.headerTemplate')
                       , selectionMode:'none', swipeBehavior:'none', tapBehavior:'invoke',
                        layout:{type:WinJS.UI.GridLayout}}">
</div>

The control references two templates (itemTemplate and headerTemplate) which define how to render every item and the headers:

<div class="headerTemplate" data-win-control="WinJS.Binding.Template">
    <div class="header-title" data-win-bind="innerText: name">
    </div>
    <img class="item-image" data-win-bind="src: logo" src="#" />
</div>
<div class="itemTemplate" data-win-control="WinJS.Binding.Template">
    <img class="item-image" data-win-bind="src: logo" src="#" />
    <div class="item-overlay">
        <h4 class="item-title" data-win-bind="textContent: name"></h4>
    </div>
</div>

As I said before, I always try to create my layout using CSS3 grid. For example, here are the styles for the items in the list:

.home .blocksList .win-item {
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr 30px;
    display: -ms-grid;
    height: 130px;
    width: 260px;
    background: white;
    outline: rgba(0, 0, 0, 0.8) solid 2px;
}

    .home .blocksList .win-item:hover {
        outline: #5F38FF solid 2px;
    }

    .home .blocksList .win-item .item-image-container {
        -ms-grid-columns: 1fr;
        -ms-grid-rows: 1fr;
        -ms-grid-row: 1;
        display: -ms-grid;
        padding: 4px;
        -ms-transition: opacity ease-out 0.2s, -ms-transform ease-out 0.2s;
        -ms-transform: scale(1.0, 1.0);
    }

        .home .blocksList .win-item .item-image-container:hover {
            opacity: 0.9;
            -ms-transform: scale(1.1, 1.1);
        }

    .home .blocksList .win-item .item-image {
        -ms-grid-row: 1;
        -ms-grid-column-align: center;
        -ms-grid-row-align: center;
        max-height: 90px;
    }

    .home .blocksList .win-item .item-overlay {
        -ms-grid-row: 2;
        padding: 3px 15px 2px;
        background-color: rgba(0, 0, 0, 0.8);
    }

Thanks to CSS3 transitions (http://msdn.microsoft.com/en-us/library/windows/apps/Hh781227.aspx), it is also really simple to handle the “hover” state. Please note the use of “.home” as a prefix to only apply these styles to the home.htmlpage (because of the single page navigation system).

The blocksList control is then filled with the data using the createGrouped function of the WinJS.Binding.List class:

var groupDataSource = UrzaGatherer.Expansions.createGrouped(this.groupKeySelector,
                                                            this.groupDataSelector, this.groupCompare);

ui.setOptions(listView, {
    itemDataSource: groupDataSource.dataSource,
    groupDataSource: groupDataSource.groups.dataSource,
    layout: new ui.GridLayout({ groupHeaderPosition: "top" })
});

One important point here is the groupKeySelector function. This function is used to create a key for every group. This key is used to group items and will also be used when you will add a SemanticZoom control:

groupKeySelector: function (item) { return item.block.name + "*" + item.block.index; },

Please note that you MUST return a string and not a number !

The groupCompare function receives the key and must sort them:

groupCompare: function (i0, i1) { 
var index0 = parseInt(i0.split("*")[1]);
var index1 = parseInt(i1.split("*")[1]);

return index1 - index0;
}

Adding a custom control for pictures

The problem with the pictures is that it can be long to download them and when the download is complete they appear without animation and it is like a bad popping effect. So I decided to create a custom control that will add a cool animation to make pictures appear softly.

To declare a custom control it is rather simple (using the WinJS.Class.define function):

(function () {

    var delayImageLoader = WinJS.Class.define(
            function (element, options) {
                this._element = element || document.createElement("div");
                this.element.winControl = this;
                WinJS.Utilities.addClass(this.element, "imageLoader");
                WinJS.Utilities.query("img", element).forEach(function (img) {
                    img.addEventListener("load", function () {
                        WinJS.Utilities.addClass(img, "loaded");
                    });
                });
            },
            {


                element: {
                    get: function () { return this._element; }
                },
            });

    WinJS.Namespace.define("UrzaGatherer.Tools", {
        DelayImageLoader: delayImageLoader
    });
})();

As you can see, the control looks for children images and adds an event listener for the “load” event. All the magic is in the CSS in fact because the control just add the imageLoader class at beginning and the loaded class and the end.

These two classes are defined in the default.css file:

.imageLoader img {
opacity: 0;
-ms-transform: scale(0.8, 0.8);


.imageLoader img.loaded {
    opacity: 1;
    -ms-transition: opacity ease-out 0.2s, -ms-transform ease-out 0.2s;
    -ms-transform: scale(1, 1);
}

Using CSS3 transitions, the picture will softly pop when the download is finished.

 

Adding a semantic zoom

Finally, I added a semantic zoom (http://msdn.microsoft.com/en-us/library/windows/apps/hh465492.aspx) in the home page to allow users to quickly jump to a block:

To do so, you have to embed the initial ListView with another one (the zoomed ListView) inside a WinJS.UI.SemanticZoom control


<
div class="zoomControl" data-win-control="WinJS.UI.SemanticZoom"> <div class="blocksList" aria-label="List of blocks" data-win-control="WinJS.UI.ListView" data-win-options="{itemTemplate:select('.itemTemplate'), groupHeaderTemplate:select('.headerTemplate'), selectionMode:'none', swipeBehavior:'none', tapBehavior:'invoke', layout:{type:WinJS.UI.GridLayout}}"> </div> <div class="zoomedList" aria-label="Zoomed List of blocks" data-win-control="WinJS.UI.ListView" data-win-options="{itemTemplate:select('.semanticZoomTemplate'), selectionMode:'none', swipeBehavior:'none', tapBehavior:'invoke', layout:{type:WinJS.UI.GridLayout}}"> </div> </div>

To synchronize the two ListViews, you just have to use the same datasource for the group in the first list and the items in the second:


var
groupDataSource = UrzaGatherer.Expansions.createGrouped(this.groupKeySelector, this.groupDataSelector, this.groupCompare); ui.setOptions(listView, { itemDataSource: groupDataSource.dataSource, groupDataSource: groupDataSource.groups.dataSource, layout: new ui.GridLayout({ groupHeaderPosition: "top" }) }); ui.setOptions(zoomedListView, { itemDataSource: groupDataSource.groups.dataSource });
 

To be continued

The next article will introduce :

  • The expansion page
  • The card page
  • Settings
  • Offline mode

This article was reprinted with permission from Microsoft Corporation. This site does business with Microsoft Corporation.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured