Metro: Dynamically Switching Templates with a WinJS ListView

Posted by Stephen.Walther on Stephen Walter See other posts from Stephen Walter or by Stephen.Walther
Published on Wed, 23 May 2012 16:15:59 +0000 Indexed on 2012/06/07 4:46 UTC
Read the original article Hit count: 426

Filed under:

Imagine that you want to display a list of products using the WinJS ListView control. Imagine, furthermore, that you want to use different templates to display different products. In particular, when a product is on sale, you want to display the product using a special “On Sale” template.

clip_image002

In this blog entry, I explain how you can switch templates dynamically when displaying items with a ListView control. In other words, you learn how to use more than one template when displaying items with a ListView control.

Creating the Data Source

Let’s start by creating the data source for the ListView. Nothing special here – our data source is a list of products. Two of the products, Oranges and Apples, are on sale.

(function () {
    "use strict";

    var products = new WinJS.Binding.List([
            { name: "Milk", price: 2.44 },
            { name: "Oranges", price: 1.99, onSale: true },
            { name: "Wine", price: 8.55 },
            { name: "Apples", price: 2.44, onSale: true },
            { name: "Steak", price: 1.99 },
            { name: "Eggs", price: 2.44 },
            { name: "Mushrooms", price: 1.99 },
            { name: "Yogurt", price: 2.44 },
            { name: "Soup", price: 1.99 },
            { name: "Cereal", price: 2.44 },
            { name: "Pepsi", price: 1.99 }
    ]);

    WinJS.Namespace.define("ListViewDemos", {
        products: products
    });

})();

The file above is saved with the name products.js and referenced by the default.html page described below.

Declaring the Templates and ListView Control

Next, we need to declare the ListView control and the two Template controls which we will use to display template items. The markup below appears in the default.html file:

<!-- Templates -->
<div id="productItemTemplate" data-win-control="WinJS.Binding.Template">
    <div class="product">
        <span data-win-bind="innerText:name"></span>
        <span data-win-bind="innerText:price"></span>
    </div>
</div>

<div id="productOnSaleTemplate" data-win-control="WinJS.Binding.Template">
    <div class="product onSale">
        <span data-win-bind="innerText:name"></span>
        <span data-win-bind="innerText:price"></span>
        (On Sale!)
    </div>
</div>

<!-- ListView -->
<div id="productsListView"
    data-win-control="WinJS.UI.ListView"
    data-win-options="{
            itemDataSource: ListViewDemos.products.dataSource,
            layout: { type: WinJS.UI.ListLayout }

        }">
</div>

In the markup above, two Template controls are declared. The first template is used when rendering a normal product and the second template is used when rendering a product which is on sale. The second template, unlike the first template, includes the text “(On Sale!)”.

The ListView control is bound to the data source which we created in the previous section. The ListView itemDataSource property is set to the value ListViewDemos.products.dataSource. Notice that we do not set the ListView itemTemplate property. We set this property in the default.js file.

Switching Between Templates

All of the magic happens in the default.js file. The default.js file contains the JavaScript code used to switch templates dynamically.

Here’s the entire contents of the default.js file:

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {

            WinJS.UI.processAll().then(function () {
                var productsListView = document.getElementById("productsListView");
                productsListView.winControl.itemTemplate = itemTemplateFunction;

            });;

        }
    };

    function itemTemplateFunction(itemPromise) {
        return itemPromise.then(function (item) {
            // Select either normal product template or on sale template
            var itemTemplate = document.getElementById("productItemTemplate");
            if (item.data.onSale) {
                itemTemplate = document.getElementById("productOnSaleTemplate");
            };

            // Render selected template to DIV container
            var container = document.createElement("div");
            itemTemplate.winControl.render(item.data, container);
            return container;
        });
    }

    app.start();
})();

In the code above, a function is assigned to the ListView itemTemplate property with the following line of code:

productsListView.winControl.itemTemplate = itemTemplateFunction;

 

The itemTemplateFunction returns a DOM element which is used for the template item. Depending on the value of the product onSale property, the DOM element is generated from either the productItemTemplate or the productOnSaleTemplate template.

Using Binding Converters instead of Multiple Templates

In the previous sections, I explained how you can use different templates to render normal products and on sale products. There is an alternative approach to displaying different markup for normal products and on sale products. Instead of creating two templates, you can create a single template which contains separate DIV elements for a normal product and an on sale product.

The following default.html file contains a single item template and a ListView control bound to the template.

<!-- Template -->
<div id="productItemTemplate" data-win-control="WinJS.Binding.Template">
    <div class="product" data-win-bind="style.display: onSale ListViewDemos.displayNormalProduct">
        <span data-win-bind="innerText:name"></span>
        <span data-win-bind="innerText:price"></span>
    </div>

    <div class="product onSale" data-win-bind="style.display: onSale ListViewDemos.displayOnSaleProduct">
        <span data-win-bind="innerText:name"></span>
        <span data-win-bind="innerText:price"></span>
        (On Sale!)
    </div>
</div>

<!-- ListView -->
<div id="productsListView"
    data-win-control="WinJS.UI.ListView"
    data-win-options="{
            itemDataSource: ListViewDemos.products.dataSource,
            itemTemplate: select('#productItemTemplate'),
            layout: { type: WinJS.UI.ListLayout }
        }">
</div>

The first DIV element is used to render a normal product:

<div class="product" data-win-bind="style.display: onSale ListViewDemos.displayNormalProduct">
    <span data-win-bind="innerText:name"></span>
    <span data-win-bind="innerText:price"></span>
</div>

The second DIV element is used to render an “on sale” product:

<div class="product onSale" data-win-bind="style.display: onSale ListViewDemos.displayOnSaleProduct">
    <span data-win-bind="innerText:name"></span>
    <span data-win-bind="innerText:price"></span>
    (On Sale!)
</div>

Notice that both templates include a data-win-bind attribute. These data-win-bind attributes are used to show the “normal” template when a product is not on sale and show the “on sale” template when a product is on sale. These attributes set the Cascading Style Sheet display attribute to either “none” or “block”.

The data-win-bind attributes take advantage of binding converters. The binding converters are defined in the default.js file:

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
            WinJS.UI.processAll();
        }
    };

    WinJS.Namespace.define("ListViewDemos", {
        displayNormalProduct: WinJS.Binding.converter(function (onSale) {
            return onSale ? "none" : "block";
        }),

        displayOnSaleProduct: WinJS.Binding.converter(function (onSale) {
            return onSale ? "block" : "none";
        })

    });

    app.start();
})();

The ListViewDemos.displayNormalProduct binding converter converts the value true or false to the value “none” or “block”. The ListViewDemos.displayOnSaleProduct binding converter does the opposite; it converts the value true or false to the value “block” or “none” (Sadly, you cannot simply place a NOT operator before the onSale property in the binding expression – you need to create both converters).

The end result is that you can display different markup depending on the value of the product onSale property. Either the contents of the first or second DIV element are displayed:

clip_image003

Summary

In this blog entry, I’ve explored two approaches to displaying different markup in a ListView depending on the value of a data item property. The bulk of this blog entry was devoted to explaining how you can assign a function to the ListView itemTemplate property which returns different templates. We created both a productItemTemplate and productOnSaleTemplate and displayed both templates with the same ListView control.

We also discussed how you can create a single template and display different markup by using binding converters. The binding converters are used to set a DIV element’s display property to either “none” or “block”. We created a binding converter which displays normal products and a binding converter which displays “on sale” products.

© Stephen Walter or respective owner

Related posts about metro