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

Dans un des articles précédents, on a vu comment alimenter un composant « ListView » Windows 8, depuis un appel XHR:

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

Pour cela, on assigne la propriété « itemDataSource » que l’on va lier à la propriété « dataSource » d’un objet de type WinJS.Binding.List. Un objet de type List est une sorte de tableau qui supporte le Binding sur une liste (liaison dynamique entre la vue et le model). Cette List peut donc contenir différents types d’objets. Même si cela n’est pas conseillé, vous pouvez mélanger des choux et des carottes.

On a ensuite vu que pour effectuer le rendu d’un élément de ListView, on utilisait un Template, sous la forme d’un bout de HTML avec l’option itemTemplate à laquelle on donne un sélecteur DOM:

itemTemplate: select('.commentTemplate')

Seulement, dans ce cas-là vous allez faire le rendu de tous les items avec le même template. Si vous avez des objets utilisateurs et des objets playlist dans votre List, vous voudrez représenter chaque objet différemment.

Simple, si vous réalisez votre application en C#

JavaScript n’est pas la seule possibilité pour réaliser une application Windows 8 Metro. Vous pouvez aussi la réaliser en C# / XAML. Dans ce cas-là, vous avez une propriété « ItemTemplateSelector » sur « GridView » qui prend comme valeur une fonction qui va renvoyer le template à utiliser. Tout est détaillé dans ce très bon tutorial:

Windows 8 – Appliquer un template en fonction des items d’une liste

Pour les flexeurs, cela ressemble beaucoup à une itemRendererFunction :)

Moins simple, si vous réalisez votre application en HTML/JS

Il se peut aussi que comme moi, vous ayez choisi de la faire en HTML. Si vous regardez la documentation de ListView, vous verrez que cette propriété n’est pas implémentée:

WinJS.UI.ListView object

La solution est en fait plus complexe que cela. Pour trouver une solution acceptable, j’ai du retourner WinJS, et notamment le mécanisme de rendu interne de ListView.

Réalisation de la solution en JavaScript

On prépare d’abord notre page HTML pour accueillir une ListView:

<section aria-label="Main content" role="main">
  <div class="tracklist" style="height:100%;" data-win-control="WinJS.UI.ListView"
data-win-options="{ selectionMode: 'none'}"></div>
</section>

Ensuite dans le code JS correspondant à notre page, on ajoute un jeu de données:

(function () {
    "use strict";

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

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

        models: null,

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

Puis on va définir notre template HTML:

...
</head>
<body>
    <div class="templateKindZero" data-win-control="WinJS.Binding.Template">
        <div class="item-overlay">
            <h4 class="item-title" data-win-bind="textContent: title"></h4>
            <h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6>
        </div>
    </div>

On pourrait assigner simplement notre template à la propriété « itemTemplate » de notre ListView:

            WinJS.UI.setOptions(listView, {
                itemDataSource: this.models.dataSource,
                itemTemplate: select('.templateKindZero')
            });

On obtient:

Maintenant, on va créer un autre template pour représenter chaque autre type d’élément. Bien sûr, vous pourrez être plus imaginatif que moi :).

    <div class="templateKindZero" data-win-control="WinJS.Binding.Template">
        <div class="item-overlay">
            <h4 class="item-title" data-win-bind="textContent: title"></h4>
            <h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6>
        </div>
    </div>
    <div class="templateKindOne" data-win-control="WinJS.Binding.Template">
        <div class="item-overlay" style="background-color:red;">
            <h3 class="item-title" data-win-bind="textContent: title"></h3>
            <h5 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h5>
        </div>
    </div>
   <div class="templateKindTwo" data-win-control="WinJS.Binding.Template">
        <div class="item-overlay" style="background-color:blue;">
            <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>

On a simplement des couleurs de fond différentes et des tailles de font différentes.

En fait, au lieu d’assigner un sélecteur DOM à la propriété « itemTemplate », on va assigner une fonction. Cette fonction a la signature suivante:

object renderItem(itemPromise, recycledElement)

Et doit renvoyer soit

  • un élément DOM
  • un objet contenant une propriété « element » de type élément DOM et un propriété « renderComplete » de type Promise.

Commençons par assigner notre fonction:

        itemTemplateFunction : function (itemPromise){

        },

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

On pourrait construire directement les éléments DOM qui vont bien, par exemple:

function itemTemplateFunction(itemPromise) {

       return itemPromise.then(function (item) {
           var div = document.createElement("div");

           var img = document.createElement("img");
           img.src = item.data.picture;
           img.alt = item.data.title;
           div.appendChild(img);

           var childDiv = document.createElement("div");

           var title = document.createElement("h4");
           title.innerText = item.data.title;
           childDiv.appendChild(title);

           var desc = document.createElement("h6");
           desc.innerText = item.data.text;
           childDiv.appendChild(desc);

           div.appendChild(childDiv);
           
           return div;
       });
    };

C’est l’exemple qui est donné dans l’exemple de la documentation ListView. Simple si vous avez un DOM qui tient en quelques lignes. Moins si vous avez un DOM complexe. La notation HTML est tout de même plus lisible et plus cohérente pour votre programme.

La solution est en fait assez simple, quand on a compris comment se faisait le rendu des templates en interne dans WinJS. Un composant de type WinJS.Binding.Template contient en fait une méthode « render » qui va prendre en premier argument, un objet de données (l’objet à binder au template) et en second argument, l’élément DOM qui va venir contenir le template. Cet élément DOM peut être un simple DIV, cela n’a pas d’importance.

Voici donc la fonction à adopter. Bien sûr, celle-ci devra être adaptée selon votre propre logique métier.

itemTemplateFunction : function (itemPromise, recycledElement) {
    return itemPromise.then(function (currentItem) {
        var itemData = currentItem.data;
        var tpl = null;
        switch (itemData.kind) {
            case 0:
                tpl = document.querySelector(".templateKindZero");
                break;
            case 1:
                tpl = document.querySelector(".templateKindOne");
                break;
            case 2:
                tpl = document.querySelector(".templateKindTwo");
                break;
            default:
                // do something, really
                break;
        }
        // dumb div to contain the result
        var result = document.createElement("div");
        return tpl.winControl.render(itemData, result);
    })
},

Et voici le résultat:

Cela se chevauche car je n’ai pas précisé la talle des cases de mon tableau (qui du coup sont variables). Mais ce sera pour un autre tutorial :). J’utilise par exemple cette technique dans Clouder! pour Windows 8 pour le système de pagination:

Le dernier item est différent et sert à charger une nouvelle page. On verra comment faire dans un prochain tutorial de html5-tutorial.fr !

Laisser un commentaire

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