Tag Archives: listview

Using WinJS.Binding.List to update the list view

Published December 3, 2013 1:06 pm

WinJS.UI.ListView is the most used control in WinJS library. At the beginning, you typically start with static List views. In such List Views, data does not change after it is bound. You may have to sort or group data which can be achieved using createSorted or createGrouped methods on List. You can order the items, group the items, and order the groups using these methods. If you follow some of the msdn listview samples, it is sort of clear. (Note: not all samples needs to be followed to get hang of list view. Some of them like custom data source, incremental loading are advanced scenarios).

When you working with dynamic data, list view updates as user interacts with the list view. Examples:

  1. User can right click a item, and delete it using contextual app bar command.
  2. User clicks/taps an item. The item is either removed or updated.

List view elements sort order and grouping needs to be maintained after the item change or list changes. In this post, let’s look into (2). I will try to cover (1) in another post. To go over this, let’s take a simple problem of displaying list of todo tasks.

  1. The tasks should be displayed in two  groups – ‘todo’ and ‘done’ groups.
  2. When a task is clicked or tapped in the todo group – it is moved to ‘done’ group.
  3. ‘done’ group should maintain only the latest 5 done tasks. oldest task gets off the list when list grows beyond 5 tasks.

my-tasks-1With this, Let’s look at the HTML

<div id="todoListView" data-win-control="WinJS.UI.ListView"
    data-win-bind="winControl.itemDataSource: items.dataSource; winControl.groupDataSource: items.groups.dataSource"
    data-win-options="{
      itemTemplate: select('#todoItemTemplate'),
      groupHeaderTemplate: select('#groupHeaderTemplate'),
      selectionMode: 'none', swipeBehavior: 'none', tapBehavior: 'invokeOnly' }">
</div>
  1. list should have tapBehavior set to invokeOnly to enable click/tap.
  2. itemTemplate and groupHeaderTemplate are set which is kind of obvious.
  3. this snippet binds itemDataSource and groupDataSource in html. If you don’t have view model (MVVM) separate in your code, you may set it in the .js file.

Let’s look at the sample data.

var data = [
    { status: 'todo', title: 'wake up' },
    { status: 'todo', title: 'brush your teeth' },
    { status: 'todo', title: 'take bath' },
    { status: 'todo', title: 'do yoga' },
    { status: 'todo', title: 'take breakfast' },
    { status: 'todo', title: 'check email' },
    { status: 'todo', title: 'check facebook' },
    { status: 'todo', title: 'check stackoverflow forum' },
];

We will sort the list using the sort key <status, finishTime>. finishTime will be added whenever user taps the item. todo status is sorted before done status. finishTime is sorted in descending order. This way – all done items are order towards end of the list. Further, If any item is to be dropped, it is at the end of the list.

 

    function compareStatus(t1, t2)
    {
        if (t1 == t2)
            return 0;
        else if (t1 == 'todo')
            return -1;
        else
            return 1;
    }

    function compareFinishTime(d1, d2)
    {
        if (d1 == undefined && d2 == undefined)
            return 0;
        // treat unfinished task ahead in sort order
        else if (d1 == undefined)
            return -1;
        else if (d2 == undefined)
            return 1;
        else
        {
            var t1 = d1.getTime(), t2 = d2.getTime();
            // treat recently finished task ahead in sort order
            if (t1 > t2)
                return -1;
            else
                return 1;
        }
    }

    var sortedList = list.createSorted(function compare(i1, i2)
    {
        var c1 = compareStatus(i1.status, i2.status);
        if (c1 != 0)
            return c1;
        var c2 = compareFinishTime(i1.finishTime, i2.finishTime);
        return c2;
    });

Further, we use createGrouped to group the sorted items and order the groups.

    this.items = sortedList.createGrouped(function groupKey(item)
    {
        return item.status;
    },
    function groupData(item)
    {
        return { title: item.status };
    }, 
    function groupSorter(g1, g2)
    {
        // keep todo items group b4 'done' items group
        if (g1 == g2)
            return 0;
        else if (g1 == 'todo')
            return -1;
        else 
            return 1;
    });

Now, to move the item from ‘todo’ group to ‘done’ group – we can change the item in the iteminvoked handler and notify the list for item mutation using notifyMutated method. Once notified, list sorted and grouped projection will update themselves. It will reflect in the bound list view.

 
registerItemInvokedHandler: function registerItemInvokedHandler()
{
    function oniteminvoked(event)
    {
        var index = event.detail.itemIndex;
        var item = this.viewModel.items.getAt(index);
        // click on 'done' tasks should be noop. 
        if (item.status == 'done')
            return;
        item.status = 'done';
        item.finishTime = new Date();
        this.viewModel.items.notifyMutated(index);
        // schedule removal for later using setImmediate
        WinJS.Promise.timeout().then(removeIfReqd.bind(this));
    }

    function removeIfReqd()
    {
        var count = 0;
        var items = this.viewModel.items;
        for (var i = items.length; i > 0; i--)
        {
            var item = items.getAt(i - 1);
            if (item.status == 'todo')
                break;
            count++;
        }

        // remove only if more than 5 'done' items
        if (count > 5)
            items.splice(items.length - 1, 1);
    }

    todoListView.addEventListener('iteminvoked', oniteminvoked.bind(this));
},

my-tasks-3* Note the code above references items from the viewModel. That needs to be modified to reference items as per your code.

This way – we saw in this post – how createSorted, createGrouped along with notifyMutated – can be used to build a dynamic list view.

Handling click event on elements within WinJS Listview ItemTemplate

Published May 29, 2013 5:26 pm

Typically – there is need to handle ‘itemInvoked’ event in a ListView. but there are some cases when you are looking for handling click event on a element defined in list view item template. Since item template is rendered multiple times for each item in the list view, how do we do this?

Let’s take an example where item template has two anchors. On click on each anchor, you want to tranverse to another page but parameter passed to the page will differ based on which anchor was clicked.

<div id="itemTemplate" data-win-control="WinJS.Binding.Template">
    <div>
        <h3 class="win-type-ellipsis date cell" data-win-bind="innerText: date"></h3>
        <h3 class="win-type-ellipsis hour cell" data-win-bind="innerText: hour"></h3>
        <a class="win-type-ellipsis counter1 cell" data-win-bind="innerText: counter1" ></a>
        <a class="win-type-ellipsis counter2 cell" data-win-bind="innerText: counter2" ></a>
    </div>
</div>

To do this, we need to register a wrapper itemTemplate function in code like this. That will enable us to register event handlers for counter1 and counter2 cells.

    listView.winControl.itemTemplate = this._itemTemplate.bind(this);
    _itemTemplate: function itemTemplate(itemPromise)
    {
        var self = this;
        var container = document.createElement('div');
        return itemPromise.then(function onitem(item)
        {
            return itemTemplate.winControl.render(item.data, container);
        }).then(function onrendercompete(c)
        {
            // register event listener
            var counter1Element = c.querySelector('.counter1);
            var counter2Element = c.querySelector('.counter2);
            counter1Element.onclick = self._oncounter1click.bind(self);
            counter2Element.onclick = self._oncounter2click.bind(self);
            return container;
        });
    },

In the event handler, we can use ListView.indexOfElement() method to get the item corresponding to the element clicked.

    _oncounter1click: function oncounter1click(event)
    {
        this._navigateToRequestsPage(event.currentTarget, 'counter1');
    },
    _oncounter2click: function oncounter2click(event)
    {
        this._navigateToRequestsPage(event.currentTarget, 'counter2');
    },
    _navigateToRequestsPage: function navigateToRequestsPage(itemElement, counterFlag)
    {
        var index = hourlyStatListView.winControl.indexOfElement(itemElement);
        var item = this.viewModel.items.getAt(index);
        var hour = item.date + 'T' + item.hour;
        WinJS.Navigation.navigate('/pages/itemDetails/itemDetails.html', { hour: hour, counterFlag: counterFlag});
    }

In summary, we saw how we can use wrapper item template function to register event handlers with elements declared within item template. We also saw the use and relevance of ListView.indexOfElement() function in the scenario.