Ioannis Panagopoulos blog

Tutorials on HTML5, Javascript, WinRT and .NET

WinJS ListView - The most important features and how to use them

by Ioannis Panagopoulos

The WinJS.UI.ListView control is one of the most important in Window Store Applications. It is the basis of the application's UI and the one most familiar to users. In this post will explore the features/tweaks available for the control that are most needed to the developer when used. This post can serve as a cheatsheet to anyone wishing to use the WinJS.UI.ListView control in his applications.

Adding a ListView, its Data Template, its Data Items and styling it

In the part of the page you want the list to be displayed just add a div element with the data-win-control attribute set to WinJS.UI.ListView.

<div id="demolistview" data-win-control="WinJS.UI.ListView"></div>

The ListView by default will have a height of 400px and a width of 100%. Next, we need to add a data template for the list elements. This is done by creating a div element and setting its data-win-control attribute to WinJS.Binding.Template. Then we can use the data-win-bind attribute to bind to specific properties of the elements in the data list used for the ListView. One representative example of a data template is the following:

<div id="dataTemple" data-win-control="WinJS.Binding.Template">
    <div class="dataTemplateContainer">
        <div class="title" data-win-bind="textContent:title;style.backgroundColor:backColor"></div>
        <img src="#" class="image" data-win-bind="src:image" />
    </div>
</div>

We also need some demo data list that will hydrate the ListView. We can use a javascript Array for this purpose but it is better to create a WinJS.Binding.List for our data. This kind of list provides notifications to the ListView control toget automatically updated when items are inserted/removed. Below is an example of such a list with demo data for our purpose. Note that the properties of the elements of the list match the names that are used in the "data-win-bind" attributes of the elements in the data template.

var dataBindingList = new WinJS.Binding.List();
for (var i = 0; i < 100; i++) {
    dataBindingList.push({ 
    	title: 'Item ' + i.toString(), 
    	image: '/images/demo.jpg', 
    	backColor: 'rgba(255,0,0,0.5)' 
    });
}

Next we need to bind the data template and the data list with the ListView. This is done using the code below:

var listViewControl = document.querySelector("#demolistview").winControl;
listViewControl.itemDataSource = dataBindingList.dataSource;
listViewControl.itemTemplate = document.querySelector("#dataTemple");

That is we get the ListView object (listViewControl) and then associate the data list (dataBindingList) and the data template. There is a last think necessary to specify which is the default size of each element in the list. This is set for the div element of the list item with class name "win-item". This element gets generated for you by the framework. So for our example this should be elements of 100x100 pixels size:

#demolistview .win-item{
	width:100px;
	height:100px;
}

One thing to keep in mind is that when styling the list elements you should not be using the id of the element defined as the template (in our case #dataTemplate) since this will not work. Insted use an element within the template (and this is the reasong why in our case we define the element with the dataTemplateContainer class name). For example, here is a possible styling for the example's template:

.dataTemplateContainer {
	display:-ms-grid;
	height:100%;
	-ms-grid-columns:1fr;
	-ms-grid-rows:50px 1fr;
}
.dataTemplateContainer .title {
	-ms-grid-row:1;
}
.dataTemplateContainer .image {
	-ms-grid-row-span:2;
}

Populating the ListView with data from a service

In this case you create the ListView as stated above and also the dataList as before but you do not fill it with data. You can then perform an asynchronous request (WinJS.xhr) to get some data from a web service and once you get them you push the items to the data list as follows (the following example uses the Web API )

WinJS.xhr({ url: 'SERVICE URL' }).then(function (data) {
    var responseJSON = JSON.parse(data.response);
    var items = responseJSON.results;
    for (var i = 0; i < items.length; i++)
        dataBindingList.push({
            title: items[i].aliases,
            image: items[i].image ? items[i].image.medium_url : '/images/listview/noImage.png',
            backColor: 'rgba(255,255,255,0.5)'
        });
});

Note that the respose is contained in data.response and you need to convert it to a JSON object using JSON.parse. You then cycle through the results an map them one to one to the elements of the list. Since the list is of WinJS.Binding.List type the moment you push the items, they appear to the UI.

Changing the layout

The default list layout is grid layout. This means that items will first occupy all their container height and then, they will start expanding to the right. You can also set the layout to list layout which will expand your items top to bottom like an ordinary list making them occupy all the available horizontal space. The layout is chosen by setting the listViewControl.layout property to either "new WinJS.UI.GridLayout()" or "new WinJS.UI.ListLayout()".

Creating a grouped view

One of the features of the ListView is its ability to group its items and display them to the user as groups with a title. For this to work each item in the list needs to carry a property that will serve as the group definition for the item. Say for example that you have 3 groups with the following data:

var groups = [
    {id:1,title:'Group 1'},
    {id:2,title:'Group 2'},
    {id:3,title: 'Group 3' }
];

Each item in the list has a group property that points to one of those groups. Next, you execute the following, which creates your grouped datasource:

dataBindingList = dataBindingList.createGrouped(function getKey(item) {
    return item.group.id;
}, function getGroup(item) {
    return item.group;
}, function sortGroup(groupKey1,groupKey2) {
    if (groupKey1 > groupKey2) return 1;
    return -1;
});

Which means that you change your initial WinJS.Binding.List with the createGrouped method to which you supply three functions. One that returns the group key for each item, one that returns the group object for each item and one that helps in the sorting of the groups. The last needs you to return for each two group key combinations a,b, 1 if a>b and -1 if a<b.

The last step is associating the new datasource with the ListView as follows:

listViewControl.itemDataSource = dataBindingList.dataSource;
listViewControl.groupDataSource = dataBindingList.groups.dataSource;

If you do all this and execute the app you will notice that the list items are getting grouped but the group title displays the group object itself serialized as a JSON string. This is due to the fact that your ListView does not have a template to use in order to diplay the group. So you need to define one, for example:

<div id="groupTemplate" data-win-control="WinJS.Binding.Template">
    <div class="groupTemplateContainer">
		<div class="title" data-win-bind="textContent:title"></div>
    </div>
</div>

To be able to use this template you need to declare it in javascript as follows:

 listViewControl.groupHeaderTemplate = document.querySelector("#groupTemplate");

List items with different sizes

So what if you want to support different dimensions for your tiles? Say for each group you have one "featured" element which you want to reside in a bigger tile. With the ListView this is possible provided that the two different sized tiles are both divisible with the same "minimum" grid size that you define. In mathematical terms the two (or more sizes) should have the same GCD for each one of their dimensions which will be used as the "minimum" grid size (you also need to consider the margin as you will see in the following example).

Let's say we want to support an 130x130 dimension for the items and a dimension around 390x390 for the featured item. Then the "GCD" for both dimensions is 130x130 but since we also have a 10 pixels margin between tiles in both directions the small tile should be 130 by 130 and the big one 390+20 by 390+20 due to the margins (3 times the small tile plus 2 times the margins between them). In javascript we need to define the GCD (130 by 130) as follows:

listViewControl.layout = new WinJS.UI.GridLayout({
    groupHeaderPosition: "top",
    groupInfo: {
        enableCellSpanning: true,
        cellWidth: 130,
        cellHeight: 130
    }
});

We also define the template for the featured item:

<div id="dataTemplateFeatured" data-win-control="WinJS.Binding.Template">
    <div class="dataTemplateContainer featured">
		<img src="#" class="image" data-win-bind="src:image" />
        <div class="title" data-win-bind="textContent:title;style.backgroundColor:backColor"></div>  
    </div>
</div>

Finally we specify a function that gives the appropriate template based on the tile's data:

listViewControl.itemTemplate = function (itemPromise) {
    return itemPromise.then(function (item) {
        var itemTemplate = getTemplateForItem(item);
        var container = document.createElement("div");
        itemTemplate.winControl.render(item.data, container).done();
        return container;
    });
}

The getTemplateForItem() function is the one deciding which template will be sent to the view. In our case it is implemented as follows:

function getTemplateForItem (item) {
        var itemData = item.data;

        if (itemData.isFeatured)
            return document.querySelector('#dataTemplateFeatured');

        return document.querySelector('#dataTemplate');;
    }

Get the item pressed

This is an easy one. You just put a callback on the oniteminvoked event. To get the element that was pressed use the index provided to you by the event.detail.index property to search in the list as follows:

listViewControl.oniteminvoked = function (event) {
    var index = event.detail.itemIndex;
    var item = dataBindingList.getAt(index);
    // ...
}

Get the group item that was pressed

This is more tricky and in my opinion it is kind of a hack. You bind to a click event on the group title element itself and you provide the argument you need to know which group was invoked. For example you specify a global method named groupHeaderInvoked which accepts as the first argument the group's key as follows:

ipplos.groupHeaderInvoked = function (key) {
    // ...
}

You then bind to the click event in the group's template. The ony thing you need is to be able to pass the key value. This is done by attaching an extra attribute to the element via data-wind-bind:

<div id="groupTemplate" data-win-control="WinJS.Binding.Template">
	<div class="groupTemplateContainer">
		<div class="title" data-win-bind="textContent:title;groupKey:id" onclick="ipplos.groupHeaderInvoked(event.srcElement.groupKey)"></div>
    </div>
</div>

Note this method which may come in handy in many other situations. In the case where your groupHeaderInvoked method is in a page cotnrol you can refer to it by the Application.navigator.pageControl property which holds the current active page control.

blog comments powered by Disqus
hire me