How to cook a complete Windows 8 application with HTML5, CSS3 and JavaScript in a week – Day 1

The day 0 was dedicated to creating the home page and setting up the connection with data.

Today, you will focus on creating the missing screens and adding offline support.

The wireframe of the complete application is like this:

The complete solution can be found there: https://www.catuhe.com/msdn/urza/day1.zip 

The complete series can be found here:

 

The expansion screen

The expansion screen is built upon a ListView which is used to display all cards belonging to the expansion.

A first row is used to present filters to the users and a second row is filled with the ListView (cardsList):

<!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="expansion.css" rel="stylesheet">
    <script src="expansion.js"></script>
</head>
<body>
    <!--Templates-->
    <div class="itemTemplate" data-win-control="WinJS.Binding.Template">
        <div class="item-image-container" data-win-control="UrzaGatherer.Tools.DelayImageLoader" 
data-win-options="{root: 'cards'}"> <img class="item-image" data-win-bind="src: logo; alt: name" src="#" /> </div> <div class="item-overlay"> <h4 class="item-title" data-win-bind="textContent: name"></h4> </div> </div> <!--Content--> <div class="expansion fragment"> <header aria-label="Header content" role="banner"> <button class="win-backbutton" aria-label="Back"></button> <h1 class="titlearea win-type-ellipsis"><span class="pagetitle" id="pageTitle"></span> </h1> </header> <section aria-label="Main content" role="main"> <div class="filters"> <select id="orderSelect"> <option>By number</option> <option>By name</option> </select> <select id="colorFilter"> </select> <select id="authorFilter"> </select> <select id="rarityFilter"> </select> <select id="typeFilter"> </select> <select id="checkFilter"> <option>All</option> <option>Only missing</option> <option>All except missing</option> </select> <input type="search" id="textFilter" /> </div> <div class="cardsList" aria-label="List of cards" data-win-control="WinJS.UI.ListView" data-win-options="{itemTemplate:select('.itemTemplate'), selectionMode:'none',
swipeBehavior:'none', tapBehavior:'invoke', layout:{type:WinJS.UI.GridLayout}}"> </
div> </section> </div> </body> </html>

As always, the html file is used to create the skeleton of the page and the css file is used to style and define the position of all tags. For instance, the layout of the filters is defined using CSS3 Grid (https://msdn.microsoft.com/en-us/library/windows/apps/hh465327.aspx#css3_grid_alignment):

.expansion .filters {
    margin-left: 120px;
    -ms-grid-row: 1;
    -ms-grid-columns: auto auto auto auto auto auto 1fr;
    display: -ms-grid;
}

    .expansion .filters #orderSelect {
        -ms-grid-column: 1;
    }

    .expansion .filters #colorFilter {
        -ms-grid-column: 2;
        margin-left: 10px;
    }

    .expansion .filters #authorFilter {
        -ms-grid-column: 3;
        margin-left: 10px;
    }

    .expansion .filters #rarityFilter {
        -ms-grid-column: 4;
        margin-left: 10px;
    }

    .expansion .filters #typeFilter {
        -ms-grid-column: 5;
        margin-left: 10px;
    }

    .expansion .filters #checkFilter {
        -ms-grid-column: 6;
        margin-left: 10px;
    }

    .expansion .filters #textFilter {
        -ms-grid-column: 7;
        -ms-grid-column-align: end;
        margin-right: 10px;
    }

Handling zoom level

UrzaGatherer must be able to display the cards at different sizes:

For now the size of the items is controlled by a CSS class:

.expansion .cardsList .win-item {
    height: 340px;
    width: 240px;
    color: white;
    background-color: white;
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr;
    display: -ms-grid;
    outline: rgba(0, 0, 0, 0.8) solid 2px;
}

To change the size of the items, you just have to update this class. To do so, you have to find the css file in the DOM, look for the specific css rule (“.expansion .cardsList .win-item“) and update the width and height rules:

var updateSize = function (forceUpdate) {
    var zoomLevel = Windows.Storage.ApplicationData.current.roamingSettings.values["zoomLevel"];

    if (!zoomLevel)
        return;

    var css = UrzaGatherer.Tools.FindCSS("expansion.css");
    var level = zoomLevel / 100.0;

    for (var index = 0; index < css.cssRules.length; index++) {
        if (css.cssRules[index].selectorText == ".expansion .cardsList .win-item") {
            css.cssRules[index].style.width = (480 * level) + "px";
            css.cssRules[index].style.height = (680 * level) + "px";
        }
    }

    if (forceUpdate) {
        var listView = document.querySelector(".cardsList").winControl;
        listView.forceLayout();
    }
}

The current zoom level is a roaming setting (https://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx) retrieved with Windows.Storage.ApplicationData.current.roamingSettings.values[“zoomLevel”].

To find the good css, the following code is used:

var findCSS = function (name) {
    for (var index = 0; index < document.styleSheets.length; index++) {
        if (document.styleSheets[index].href.indexOf(name) != -1)
            return document.styleSheets[index];
    }
}

WinJS.Namespace.define("UrzaGatherer.Tools", {
    FindCSS: findCSS
});

The updateSize function is called by Windows every time a specific function is called:

Windows.Storage.ApplicationData.current.signalDataChanged();

 

Adding application settings

To configure the zoom level, the settings pane is obviously the best place. Every time you need a setting that is global to your application, the settings pane is the right place (https://msdn.microsoft.com/en-us/library/windows/apps/Hh780611.aspx):

 

To create a settings pane for your application, you have to define the html structure in a html file (I used the default.html file):

<!--Settings-->
<div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout" data-win-options="{width:'narrow'}">
    <div class="win-header">
        <button type="button" onclick="WinJS.UI.SettingsFlyout.show()" class="win-backbutton">
        </button>
        <div class="win-label">Settings</div>
    </div>
    <div class="win-content">
        <h4>Cards zoom level:</h4>
        <input type="range" id="zoomRange" min="20" max="80" value="50" />
    </div>
</div>

As you can notice, I used the WinJS.UI.SettingsFlyout control with a narrow width (I don’t need too much room).

In the default.js file, you just have to add an event listener on the change event of the range control (zoomRange):

// Zoom range
var zoomRange = document.getElementById("zoomRange");

var zoomLevel = Windows.Storage.ApplicationData.current.roamingSettings.values["zoomLevel"];
if (zoomLevel)
    zoomRange.value = zoomLevel;

zoomRange.addEventListener("change", function () {
    Windows.Storage.ApplicationData.current.roamingSettings.values["zoomLevel"] = zoomRange.value;
    Windows.Storage.ApplicationData.current.signalDataChanged();
});

So every time the range is changed, the roaming setting is updated and a signal is sent with signalDataChanged.

Offline mode

All cards are about 150 KB each so you need to provide a way to save locally the downloaded content in order to not rely afterwards on the network.

To do so, you have to change the way pictures are referenced. Indeed, instead of using an url reference to an http resource (such as https://www.mysite.com/card.jpg) you can reference a local resource inside the local folder with this kind of moniker: “ms-appdata:///local/cards/card.jpg“ and in case of failure (the file is not found for instance), you can download the picture and save it locally.

Because you used the DelayImageLoader (see the previous article) to display pictures, you can easily change its behavior to integrate the offline support:

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

            var downloadImage = function (source, filename, img) {
                var url = options ? UrzaGatherer.Root + "/" + options.root + "/" + source : 
UrzaGatherer.Root +
"/" + source; var xmlRequest = new XMLHttpRequest(); xmlRequest.open("GET", url, true); xmlRequest.responseType = "blob"; xmlRequest.onreadystatechange = function () { if (xmlRequest.readyState === 4) { UrzaGatherer.Stats.DecrementDownloads(); if (xmlRequest.status == 200) { var blob = xmlRequest.response; var input = blob.msDetachStream(); Windows.Storage.ApplicationData.current.localFolder.createFileAsync(filename, Windows.Storage.CreationCollisionOption.replaceExisting).then(
function (file) { file.openAsync(Windows.Storage.FileAccessMode.readWrite).then(
function (output) { Windows.Storage.Streams.RandomAccessStream.copyAsync(input, output).then(
function () { output.flushAsync().then(function () { input.close(); output.close(); img.src = "ms-appdata:///local/" + source; }); }); }); }); } } }; xmlRequest.send(null); } WinJS.Utilities.addClass(this.element, "imageLoader"); WinJS.Utilities.query("img", element).forEach(function (img) { img.addEventListener("load", function () { WinJS.Utilities.addClass(img, "loaded"); }); img.addEventListener("error", function (err) { if (img.src.substring(0, 5) == "http:") { } else { var source = img.src.replace("ms-appdata:///local/", ""); var filename = source.replace("/", "\"); Windows.Storage.ApplicationData.current.localFolder.getFileAsync(filename).then(
function (file) { var url = options ? UrzaGatherer.Root + "/" + options.root + "/" + source :
UrzaGatherer.Root +
"/" + source; img.src = url; }, function () { // Not found UrzaGatherer.Stats.IncrementDownloads(); downloadImage(source, filename, img); }); } }); }); }, { element: { get: function () { return this._element; } }, }); WinJS.Namespace.define("UrzaGatherer.Tools", { DelayImageLoader: delayImageLoader });

By handling the “error” event for each image, you can detect the load failure and launch the download of the picture with a XmlHttpRequest. And once the data is downloaded, you can create the local file and relaunch the image load.

 

Adding filters

Finally, you have to change the way the data is linked to the ListView to take in account filters. To do so, you can use the createFiltered function of the WinJS.Binding.List:

updateLayout: function (element, viewState) {
    updateSize();

    var listView = element.querySelector(".cardsList").winControl;

    var sorted = expansion.cardsList.createSorted(sortFunction);
    var filtered = sorted.createFiltered(filterFunction);

    if (viewState === appViewState.snapped) {
    } else {
        ui.setOptions(listView, {
            itemDataSource: filtered.dataSource,
            layout: new ui.GridLayout({ groupHeaderPosition: "top" })
        });
    }
}

This code uses filterFunction function:

 var filterFunction = function (item) {
     var result = true;

     //Filters
     for (var index = 0; index < filters.length; index++) {
         var filter = document.getElementById(filters[index][0] + "Filter").value;

         if (filter.substring(0, 3) != "All") {
             result &= (item[filters[index][0]] == filter);
         }
     }

     // Text
     var textFilter = document.getElementById("textFilter").value.toLowerCase();

     if (textFilter != "") {
         result &= (item.name.toLowerCase().indexOf(textFilter) != -1
             || item.text.toLowerCase().indexOf(textFilter) != -1
             || item.flavor.toLowerCase().indexOf(textFilter) != -1);
     }

     return result;
 }

Instead of directly refer to properties, this function uses the filters array declared as follow:

var filters = [["color", "colors"], ["author", "authors"], ["rarity", "rarities"], ["type", "types"]];

With this array, it is easy to add new filters. Only the text filter is treated separately because it applies to more than one property.

The filters array is also used to fill the combo boxes:

// Filters
for (var index = 0; index < filters.length; index++) {
    prepareFilter(filters[index][0], filters[index][1], that);
}
var prepareFilter = function (property, plural, that) {
    var filter = document.getElementById(property + "Filter");
    var results = [];

    for (var cardIndex = 0; cardIndex < expansion.cards.length; cardIndex++) {
        var value = expansion.cards[cardIndex][property];

        if (results.indexOf(value) == -1)
            results.push(value);
    }

    results.push("All " + plural);
    var sortedResults = results.sort(function (i0, i1) {
        if (i0 == i1)
            return 0;

        if (i0.substring(0, 3) == "All")
            return -1;

        if (i1.substring(0, 3) == "All")
            return 1;

        if (i0 > i1)
            return 1;

        return -1;
    });

    for (var index = 0; index < sortedResults.length; index++) {
        filter.options[index] = new Option(sortedResults[index]);
    }

    filter.addEventListener("change", function () {
        that.updateLayout(document, appView.value);
    });
};

 

The card screen

The card screen is pretty simple:

The card is linked to the UI with the following code (direct mapping):

(function () {
    "use strict";

    var appView = Windows.UI.ViewManagement.ApplicationView;
    var appViewState = Windows.UI.ViewManagement.ApplicationViewState;
    var nav = WinJS.Navigation;
    var ui = WinJS.UI;
    var utils = WinJS.Utilities;

    var card;

    ui.Pages.define("/pages/card/card.html", {
        ready: function (element, options) {
            card = options.card;

            document.getElementById("pageTitle").innerText = card.name;

            document.querySelector("#picture").src = card.logo;
            document.querySelector(".item-number").innerText = card.number + " / " + 
card.expansion.cards.length; document.querySelector(
".item-type").innerText = card.type; document.querySelector(".item-color").innerText = card.color; document.querySelector(".item-power").innerText = card.power; document.querySelector(".item-text").innerText = card.text; document.querySelector(".item-flavor").innerText = card.flavor; document.querySelector(".item-author").innerText = card.author; document.querySelector("#expansion-picture").src = card.expansion.banner; this.updateLayout(element, appView.value); }, updateLayout: function (element, viewState) { if (viewState === appViewState.snapped) { } else { } } }); })();

Fairly simple, isn’t it ? Sourire

 

To be continued

The next part will introduce:

  • Localization
  • Snapped views
  • More settings
  • Adaptation to form factors