Windows 8 – Réaliser une ListView avec pagination intégrée

Vous savez sûrement déjà créer une liste de type Windows 8 Metro UI avec le composant ListView. Sinon, vous pouvez commencer par lire ces articles:

Windows Metro – Clouder! (8) – Chargement des commentaires depuis l’API SoundCloud

Windows 8 – Utiliser un template différent selon l’élément affiché dans une ListView

Cela fonctionne bien si vous avez une dizaine voire une vingtaine d’éléments à afficher. Au delà de cela, il se peut que le premier chargement soit long et que le rendu puisse ralentir l’application. Dans ce genre de situation, la solution est bien sûr de paginer ses résultats, c’est-à-dire de les afficher par tranches de 20 ou 30.

La méthode simple

La méthode la plus simple est d’ajouter un bouton dans l’interface pour charger la prochaine page. A l’appui sur le bouton, on fait un appel XHR pour récupérer les nouveaux résultats. On peut ensuite les ajouter à notre objet de type WinJS.Binding.List et notre grille va se mettre à jour toute seule.

La méthode est simple mais pas très élégante car vous devrez trouver un endroit adapté pour placer votre bouton. Les applications Windows 8 ayant généralement une interface plein écran, vous n’allez pas vraiment pouvoir le caser quelque part.

Une solution plus élégante est de regarder le scroll de la liste et quand il arrive au bout, charger automatiquement une autre page (alias infinite scroll). Perso, je préfère avoir une action utilisateur avec un bouton à presser, pour éviter que le programme fuck up le scroll pendant que je me déplace.

La méthode intégrée

Une méthode plus sympa est de placer un élément à la fin de la liste qui va servir pour la pagination. Cet élément sera complètement intégré au rendu de la liste, vous n’aurez donc pas à vous soucier de son placement.

On va aussi voir comment se servir de cet élément bidon pour afficher que le reste de la liste est en chargement à l’utilisateur avec un composant <progress>.

Pour l’exemple de la liste, veillez vous reporter à l’exemple de cet article que l’on va modifier:

Windows 8 – Utiliser un template différent selon l’élément affiché dans une ListView

On va commencer par créer un template pour les éléments de base et un pour l’élément de pagination:

   <div class="templateBase" data-win-control="WinJS.Binding.Template">
        <div class="item-overlay" style="background-color:blue;width:200px;height:200px;">
            <h2 class="item-title" data-win-bind="textContent: title"></h2>
            <h4 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h4>
        </div>
    </div>

    <!-- PAGINATION TEMPLATE -->
    <div class="paginationTemplate" data-win-control="WinJS.Binding.Template">
        <div style="background-color:#FF6600;width:200px;height:200px;">
           <button  class="paginationIcon" data-win-control="WinJS.UI.AppBarCommand" 
            data-win-options="{icon:'add'}"
               style="margin-left:50px;margin-top:60px;"
            ></button>
            <progress class="win-ring win-large paginationRing" style="color:#FFFFFF;padding-left:65px;padding-top:65px;"></progress>
        </div>
    </div>

Puis on va modifier un peu notre JavaScript pour y intégrer un élément de pagination à la fin de la liste. On va aussi modifier notre fonction itemTemplate pour détecter la propriété « pagination » dans les objets envoyés:

(function () {
    "use strict";

    WinJS.UI.Pages.define("/pages/home/home.html", {

        pageSize: 5,
        currentPage:0,

        sampleItems: [
            {  title: "Item Title: 9", subtitle: "Item Subtitle: 0" },
            { title: "Item Title: 1", subtitle: "Item Subtitle: 1" },
            {  title: "Item Title: 2", subtitle: "Item Subtitle: 2" },
            {  title: "Item Title: 3", subtitle: "Item Subtitle: 3" },
            {  title: "Item Title: 4", subtitle: "Item Subtitle: 4"},
            {  title: "Item Title: 5", subtitle: "Item Subtitle: 5" }],

        models: null,

        paginationItem : {pagination:true},

        itemTemplateFunction : function (itemPromise, recycledElement) {
            return itemPromise.then(function (currentItem) {
                var itemData = currentItem.data;
                var tpl = null;
                if (itemData.pagination) {
                    tpl = document.querySelector(".paginationTemplate");
                } else {
                    tpl = document.querySelector(".templateBase");
                }
                // dump div to contain the result
                var result = document.createElement("div");
                return tpl.winControl.render(itemData, result);
            })
        },

        ready: function (element, options) {
            var listView = document.querySelector(".tracklist").winControl;
            this.models = new WinJS.Binding.List(this.sampleItems);
            this.models.push(this.paginationItem);
            WinJS.UI.setOptions(listView, {
                itemDataSource: this.models.dataSource,
                itemTemplate: this.itemTemplateFunction
            });
        }
    });
})();

Voici le résultat dans l’application:

 

Ensuite, on va appeler une fonction lorsque l’utilisateur clique sur un bouton de pagination:

(function () {
    "use strict";

    WinJS.UI.Pages.define("/pages/home/home.html", {

        pageSize: 5,
        currentPage:1,

        sampleItems: [
            {  title: "Item Title: 9", subtitle: "Item Subtitle: 0" },
            { title: "Item Title: 1", subtitle: "Item Subtitle: 1" },
            {  title: "Item Title: 2", subtitle: "Item Subtitle: 2" },
            {  title: "Item Title: 3", subtitle: "Item Subtitle: 3" },
            {  title: "Item Title: 4", subtitle: "Item Subtitle: 4"},
            {  title: "Item Title: 5", subtitle: "Item Subtitle: 5" }],

        models: null,

        paginationItem: { pagination: true },

        getNextPage : function(){
            // implement your XHR logic here
            // you should do that in the callback
            // remove the pagination element
            this.models.pop();
            // sample data
            var offset = this.currentPage * this.pageSize;
            var limit = offset + this.pageSize;
            for (var i = offset; i < limit; i++) {
                var title = "Item Title: " + i;
                var subtitle = "Item Subtitle: " + i;
                this.models.push({ title: title, subtitle: subtitle});
            }
            // add pagination element if needed
            this.models.push(this.paginationItem);
            this.currentPage++;
        },

        onItem : function (item) {
            if (item.data.pagination) {
                this.getNextPage();
            } else {
                // user selected something do something about it
            }
        },

        onItemInvoked : function(args){
            args.detail.itemPromise.done(this.onItem.bind(this));
        },

        itemTemplateFunction : function (itemPromise, recycledElement) {
            return itemPromise.then(function (currentItem) {
                var itemData = currentItem.data;
                var tpl = null;
                if (itemData.pagination) {
                    tpl = document.querySelector(".paginationTemplate");
                } else {
                    tpl = document.querySelector(".templateBase");
                }
                // dump div to contain the result
                var result = document.createElement("div");
                return tpl.winControl.render(itemData, result);
            })
        },

        ready: function (element, options) {
            var listView = document.querySelector(".tracklist").winControl;
            this.models = new WinJS.Binding.List(this.sampleItems);
            this.models.push(this.paginationItem);
            WinJS.UI.setOptions(listView, {
                itemDataSource: this.models.dataSource,
                itemTemplate: this.itemTemplateFunction,
                oniteminvoked: this.onItemInvoked.bind(this)
            });
        }
    });
})();

Et vous aurez un résultat correct, avec en plus les animations par défaut de Windows 8:

Un peu de style en CSS

Dans cet exemple, pas d’appel XHR donc par de latence mais dans un cas d’utilisation concret, l’opération de pagination ne sera pas immédiate, il faudra indiquer à l’utilisateur qu’un chargement s’effectue.

Ma technique est de changer le pictogramme « + » en un <progress> de type ring. L’effet est plutôt bon. Pour cela, on va assigner un style CSS à notre liste pendant le chargement de la liste: « loading ». Avec quelques directives CSS, on va donc afficher / masquer ce qu’il faut:

.loading .paginationIcon {
    display:none;
    margin:0;
}

.loading .paginationRing {
    display: block;
}

.paginationRing {
     display: none;
}

Puis dans le code JS, on assigne la classe au bon moment:

onNextPage: function () {
    var listView = document.querySelector(".tracklist");
    WinJS.Utilities.removeClass(listView, "loading");
    // you should do that in the callback
    // remove the pagination element
    this.models.pop();
    // sample data
    var offset = this.currentPage * this.pageSize;
    var limit = offset + this.pageSize;
    for (var i = offset; i < limit; i++) {
        var title = "Item Title: " + i;
        var subtitle = "Item Subtitle: " + i;
        this.models.push({ title: title, subtitle: subtitle });
    }
    // add pagination element if needed
    this.models.push(this.paginationItem);
    this.currentPage++;
},

getNextPage: function () {
    var listView = document.querySelector(".tracklist");
    WinJS.Utilities.addClass(listView, "loading");
    // XHR simulation
    // implement your XHR logic here
    setTimeout(this.onNextPage.bind(this), 3000);
},

Pendant quelques secondes, vous aurez donc le progress ring de Windows 8:

Bien sûr, vous devrez venir greffer votre logique métier au milieu. Ajouter aussi un petit boolean qui va indiquer quand on est en train de faire un appel XHR pour éviter que l’utilisateur ne clique plusieurs fois et ne lance plusieurs requêtes.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *