WinJS Listview : itemtemplates, templating functions and more

, , 3 Comments


The ListView is the most used control of WinJS in Windows 8 metro apps. It’s build with three things : the data, a layout and an item templates.

In this post, we will discover the different way to provide an item template and how to customize it.

The standard way

The easiest thing is to create an item template in the html.
A template is a specific WinJS control : “WinJS.Binding.Template”. You can declare it as any other control using the data-win-control attribute.
[html]
<div class="itemtemplate" data-win-control="WinJS.Binding.Template">
<div class="item-info">
<h4 class="item-title"
data-win-bind="textContent: title"></h4>
<h6 class="item-subtitle win-type-ellipsis"
data-win-bind="textContent: content"></h6>
</div>
</div>
[/html]

As you can see, data binding are defined to push the value in the html element. For example, the title property of the data item will be pushed as the text content of the h4 element with the “item-title” class.

This template is then provided to the listview in the HTML using the options :
[html]
<div class="itemslist"
data-win-control="WinJS.UI.ListView"
data-win-options="{ itemTemplate : select(‘.itemtemplate’) }"></div>
[/html]

The “select” is a special helper which query the DOM for the provided selector and retrieve the first element which match. In your case, the item template is retrieved using its class name. You could also provide the item template in the code-behind js file :
[javascript]
var listView = element.querySelector(".itemslist").winControl;
listView.itemTemplate = element.querySelector(".itemtemplate");
[/javascript]
Please note that the html element is provided as an item template AND NOT a win control (because it’s not one).

The listview then use the item template to display each of the item in its datasource. One html element is created by data and the data bindings are applied. You can style each of them using the class name of the item template’s inner elements.

Another way : using a templating function

A more flexible way of providing html for each item in the data source is to define a templating function.

This is a simple js function which could returns two different object :

  • A promise of an html element for the given item.
  • A json object with two properties : element and renderComplete. The first one is a place holder element and the second one is a promise which completes when the rendering is over.

The templating function takes two arguments : the first is a promise to the actual data item and the second is an item to recycle (more on that later).

You can assign a templating function to a listview as you have already done for the template before.
[javascript]
var listView = element.querySelector(".itemslist").winControl;
listView.itemTemplate = myTemplatingFunction;
[/javascript]

Now let’s define a basic templating function which returns an html element for a given data item. It returns the same html that we used in the first example.
[javascript]
function myTemplatingFunction(itemPromise) {

//"wait" for the data item
return itemPromise.then(function (item) {

//Create a main element
var mainElementDiv = document.createElement("div");
WinJS.Utilities.addClass(mainElementDiv, "item-info");

//Create the title and set the text in it
var h4Title = document.createElement("h4");
WinJS.Utilities.addClass(h4Title, "item-title");
h4Title.innerHTML = item.data.title;
mainElementDiv.appendChild(h4Title);

//Create the content and set the text in it
var h6Content = document.createElement("h6");
WinJS.Utilities.addClass(h6Content, "item-subtitle win-type-ellipsis");
h6Content.innerHTML = item.data.content;
mainElementDiv.appendChild(h6Content);

//Return the html for the given data
return mainElementDiv;

});

}
[/javascript]

What is done ? First, we wait to get the item data using the itemPromise. Once we’ve got it, we create an html element from scratch (ie the document) and we fill it with the values of the data item. Notice that there is no binding involved, this is all hand made. We also set the different class on the elementw as it was done in the first html fragment.

As you can see, the templating function is returning a promise to a single html element representing the data item.

If you run this code, you will get the same view as in the first sample. Nothing more at this point.

Templating function : advanced scenario

Often, you fetch the data from the web and you don’t know when it will be availabe and how fast it will be to retrieve it. When it takes a lot of time, the view is no more responsive because the templating function wait for the data before to return the html element.

You can then use the templating function to return a placeholder instead of the actual representation. Because you need to fill the item with the actual data, you also have to provide a promise which will complete when the data is available and the actual html element representing it created.

Instead of returning a promise to an html element, you return an object defining two properties : element and rendercomplete.. element is a placeholder and renderComplete is a promise which complete when the item template is fully rendered.
[javascript]
{
element: element,
renderComplete: itemPromise.then(function (item) { /* some code here */ }
}
[/javascript]

Let’s improve the previous example to use this advanced technique :
[javascript]
function myAdvancedTemplatingFunction(itemPromise) {

//Create a main element
var mainElementDiv = document.createElement("div");
WinJS.Utilities.addClass(mainElementDiv, "item-info");

//Create the title
var h4Title = document.createElement("h4");
WinJS.Utilities.addClass(h4Title, "item-title");
h4Title.innerHTML = "Loading …";
mainElementDiv.appendChild(h4Title);

//Create the content
var h6Content = document.createElement("h6");
WinJS.Utilities.addClass(h6Content, "item-subtitle win-type-ellipsis");

mainElementDiv.appendChild(h6Content);

var toReturn =
{
//placeholder
element: mainElementDiv,

//promise which set the value in the place holder from the data
renderComplete: itemPromise.then(function (item) {

//Simulate latency
return WinJS.Promise.timeout(Math.random() * 3000)
.then(function () {

//Set the actual values in the placeholder
mainElementDiv.querySelector(".item-title").innerHTML
= item.data.title;
mainElementDiv.querySelector(".item-subtitle").innerHTML
= item.data.content;

//listView.recalculateItemPosition();
});
}),
};

return toReturn;
}
[/javascript]

The snippet is in fact quite similar to the previous one. First I create the place holder with some text telling the user to wait because it’s loading. Then I return the previously mentioned item defining the renderComplete property as a promise. This promise sets the actual values in the placeholder from the retrieved data. I wanted to simulate some latency so I added a WinJS.Promise.Timeout but it’s not necessary.

The ListView don’t know the actual size of your elements before to actually render them. You then have two solutions to fix it :

  1. Set the size of your placeholder so that it have the same one that your final item. This is the solution I chose and I do it using CSS. Each item has a width 210px and a height of 50px. This solution is not good if you deduce the size of the item from the data.
  2. You can call after each rendering the recalculateItemPosition() function of the listview. It will place the items correctly in the Listview. The issue is that this call is not immediate and so the items may stay in a bad place for some times (hundred of ms) and create strange screens.

Here is an example of these strange screens :

UI virtualization – recycling

The templating function takes in fact one another parameter which is a recycled element.
If the listview already has created a lot of element, it can be better for performance sake to reuse them instead of create them each time we need one. This can be considered as UI virtualization and this is exactly what the listview will try to do !

Then instead, of creating the item, you reuse the one the listview gives you and reasign the values in it as it has been done previously. Don’t forget to clear the text already in the recycled element otherwise the displayed data will be the previous one until the renderComplete promise is completed. Here is the previous snippet updated :
[javascript]
function myRecyclingTemplatingFunction(itemPromise, recycled) {

var mainElementDiv;
if (recycled) {
//use the recycled element and clear the values
//already in it !
mainElementDiv = recycled;
//Set the actual values in the placeholder
mainElementDiv.querySelector(".item-title").innerHTML = "Loading RECYCLING";
mainElementDiv.querySelector(".item-subtitle").innerHTML = "";

} else {
/* same as before */
}

var toReturn =
{
/* same as before */
};

return toReturn;
}
[/javascript]

I love can’t live without Bindings !

Ok, all what we have seen it’s interesting but sometimes what you need is just a little modification of the rendered item depending of the data and with these techniques you are loosing all the binding benefits. Too bad.

In fact, you can still use the template and the bindings really easily. Let’s see the code :
[javascript]function myTemplatingFunctionWithBindings(itemPromise) {

//could have been done once elsewhere
var itemtemplate = document.querySelector(".itemtemplate");
var placeholder = document.createElement("div");

//Wait for the item
var renderCompletePromise =
itemPromise.then(function (item) {

//use the item template
return itemtemplate

//wrap the retrieved value for instant access
.renderItem(WinJS.Promise.wrap(item))
.renderComplete

//Extend the rendercomplete promise
.then(function (renderedElement) {

//do something depending of the data
if (item.data.number % 10 == 0) {
WinJS.Utilities.addClass(renderedElement.firstElementChild, "pinkElement");
}

placeholder.appendChild(renderedElement);

});
});

var toReturn = {
element: placeholder,
renderComplete: renderCompletePromise
}
return toReturn;
}
[/javascript]

First, we retrieve the item template from the DOM. It can be done once when the page is loaded but here we do it each time for demonstration purpose.

Then we wait for the data to come and call the renderItem function of the item template and provide the data using the … (what ?) … WinJS.Promise.wrap. We can’t give the data because the render item wants an item promise but if we do so, we’ll have to wait for the data to come again (it can be loooonnnggg from the web) so instead we give a promise to the data we already have using the helpful WinJS.Promise.wrap function.

Finally the renderComplete promise of the itemtemplate is extended to do something on the rendered element depending of the data. We then have something (beautiful ?) to display :

Did you said item template selector ?

Yes, I did ! You can easily extend the previous snippet to create an item template selector. The only things to do is to choose the correct template once the data is retrieved :
[javascript]function myItemTemplateSelector(itemPromise) {

//could have been done once elsewhere
var defaultItemtemplate = document.querySelector(".itemtemplate");
var fancyItemtemplate = document.querySelector(".fancyItemtemplate");

//Wait for the item
var renderCompletePromise =
itemPromise.then(function (item) {

var itemTemplate;
if (item.data.number % 10 == 0) {
itemtemplate = fancyItemtemplate;
} else {
itemtemplate = defaultItemtemplate;
}

//use the item template
return itemtemplate.renderItem(WinJS.Promise.wrap(item)).renderComplete;
});

return renderCompletePromise;
}
[/javascript]

Do you want more ?

And I first though it will be a short blog post ūüôĀ

Here are some interesting links for you if you need/want more :

  1. MSDN page on templating function
  2. Code sample of listview item template
  3. French Win8 dev community

The full solution is available here too.

 

3 Responses

  1. juna

    15/11/2014 8 h 34 min

    I am trying to add a list view having varying height and width for the item templates similar to the scenario 4 in this sample.

  2. Dimitry K

    07/05/2015 13 h 48 min

    In the the code sample "Did you said item template selector?", the return statement `return itemtemplate.renderItem(WinJS.Promise.wrap(item)).renderComplete;` didn't work for me. But it worked when I omitted `.renderComplete`.
    So seems correct way to do it would be: `return itemtemplate.renderItem(WinJS.Promise.wrap(item));` (then object with fields `element` and `renderComplete`) would be returned.

Comments are closed.