Polymer, vers un composant plus dynamique

Publié le 18 mars 2015 par Cédric Brancourt | front

Cet article est publié sous licence CC BY-NC-SA

Polymer, vers un composant plus dynamique

Dans le précédent article nous avions réalisé un composant simple et inerte, aujourd’hui nous allons le rendre un peu plus intéressant.

Nous allons d’abord faire un petit nettoyage. Ensuite, nous utiliserons les « data binding » pour avoir un compteur dynamique basé sur une fonction.

Enfin nous rendrons le composant interactif en proposant une action lors d’un « click » ou « tap » (pour les tactiles), pour remettre à zéro les notifications par exemple.

Nettoyage de printemps

Dans le précédent article nous avions construit un composant avec l’intention de démontrer l’utilisation du shadowdom.

Il y avait donc, à des fins de démonstration, des étapes dont nous pouvions nous passer.

Pour mémoire il était utilisé de cette manière :

    <my-notif src="demo-icon.svg">
                <header>Messages</header>
                <span class="count">3</span>
    </my-notif>

Faisons disparaître ces exagérations typiques du cas d’école. Le libellé de notre composant était attendu dans une balise header, le count dans un span. Puisque le count sera dynamique nous pouvons le retirer du contenu de notre élément, le libellé sera le seul contenu de notre « widget », le reste (url de l’icone, et count) feront partie de l’api du composant.

Ainsi il sera utilisé comme suit :

<my-notif id='notif' src="demo-icon.svg">
    Messages
</my-notif>

Nous verrons les attributs count et on-tap à leur tour.

Le compteur

Nous avons déjà utilisé un type de « data binding » précédement, au travers de l’attribut src de notre composant. Cet attribut de notre élément permettait de lier la donnée au « template » (et vice versa …).

Polymer a une approche simple et claire à ce sujet : Le modèle de donnée de l’élément est l’élément lui-même. Dans le contexte de notre composant, nous avons accès directement a this.src en lecture, mais aussi en écriture.

this.src // 'demo-icon.svg'
this.src = 'another-icon.svg' // Change la valeur de src, mais aussi dans le balisage ( Bidirectionnel )

Le « data binding » déclaré au travers des attributs est considéré comme l’api de notre composant. Celui-ci est bi-directionnel, comme le démontre l’exemple ci-dessus. Les valeurs du modèle de mon instance et celles du DOM sont synchronisées.

Le « data binding » permet aussi de lier mon instance et les sous-templates qu’elle peut contenir. L’exemple qui vous parlera surement est celui d’un composant « todo list » :

 <template>
    <ul>
      <template repeat="false"> <!-- Ceci est une expression ce sous template est instancié pour chaque objet de todos -->
        <li>: ></li>
      </template>
    </ul>
  </template>

Ce qui apparait entre double accolades sont des expressions, comme dans notre composant est une expression qui est évaluée this.src. Les expressions explicitement déclarées entre double accolades sont observées, ce qui fait que le changement de leur valeur se propagera dynamiquement dans le DOM. Ajouter un élément dans todos ajoutera un nouveau todo dans le DOM.

Revenons à notre élément et son compteur. Le compteur de notre objet de notification pourrait provenir d’une URL sur laquelle nous adresserions une requête de type get. Il suffirait à l’initialisation de notre element d’effectuer un fetch par exemple et dans le callback de success assigner la valeur de retour à this.count.

De manière simplifiée le script de mon élément pourrait ressembler a ceci:

    Polymer('my-notif', {
      ready: function() {
        fetch('api/count.json').then( function(response) {
            this.src = response.json();
        }
      }
    });

Mais, même en passant l’URL en attribut du composant ça serait plutôt pauvre en design pour un composant réutilisable. Obligeant une réécriture si le count ne vient pas d’un service accessible depuis une URI ( par exemple d’un autre élément de la page).

Nous allons donc préférer externaliser cette fonction qui donne le count pour que le composant puisse être utilisé dans le plus grand nombre de cas. (souvenez vous que l’un des intérêts phare des webcomponents est la réutilisation…)

Notre composant qui a été nettoyé et qui comporte maintenant notre data binding :

    <link rel="import" href="bower_components/polymer/polymer.html">

    <polymer-element name="my-notif" attributes="src" noscript>
        <template>

            <style>
                * {
                    -webkit-box-sizing: border-box;
                    -moz-box-sizing: border-box;
                    -ms-box-sizing: border-box;
                    box-sizing: border-box;
                }


                #notif-icon {
                    width: 16px;
                    vertical-align: middle;
                }
                :host {
                    padding: 5px;
                    border: 1px solid lightgrey;
                    font-weight: bold;
                }
            </style>

            <img id='notif-icon' src=""> <!-- data binding de src avec l'attribut src de notre élement -->
             <!-- Data binding de this.count dans notre template -->
            <content></content>
        </template>
    </polymer-element>

Son instanciation et l’alimentation du count :

<!doctype html>
<html lang="en">
    <link rel="import" href="my-notif.html">
<head>
    <meta charset="UTF-8">
    <title>My Notif Demo</title>
</head>
    <body unresolved>
        <my-notif  id='notif' src="demo-icon.svg">
            Messages
        </my-notif>
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            document.querySelector('#notif').count= function () {return Math.floor(Math.random() * 20)}.call();
        });
    </script>
    </body>
</html>

Comme vous le voyez ci-dessus, ça nous laisse un contrôle quasi total sur la valeur de notre count et sa mise à jour… Plutôt pratique quand on veut faire du générique !

Ici comme source du count j’ai choisi un resultat aléatoire. Changer la valeur de count se reflétera directement dans le DOM. Nous pourrions aussi appliquer la fonction (call) dans notre élément lors de l’évènement ready par exemple, mais si ça ne correspond pas au workflow désiré ?

Et surtout si nous voulons, nous pouvons intégrer notre élément primitif dans un élément qui sera beaucoup plus spécialisé dans lequel notre count serait mis à jour au travers d’un websocket par exemple.

Ce sont là je pense des clés pour créer des composants qui soient réutilisables et composables !

Composer les composants de Polymer

Polymer comme je l’avais précisé ultérieurement c’est aussi une collection de composants prêts à l’emploi et à être composés au seing d’autres éléments.

Il existe les « core » éléments qui sont des composants communs (facilitant l’accessibilité, les requêtes ajax …) et les « paper elements » qui sont des composants d’IHM qui reposent sur le material design (si vous ne connaissez pas, j’insiste, ca vaut le détour !)

Pour illustrer ce propos nous allons modifier notre icône pour utiliser l’élément core-icon-button qui est utilisable pour représenter un bouton muni d’une icône. (un bouton car nous allons lui donner un comportement par la suite) Nous allons intégrer un composant dans notre composant !

Premièrement pour télécharger les sources nous utilisons bower puisque l’on gère nos dépendances avec celui-ci.

bower install --save Polymer/core-icon-button

Puis nous importons ce composant dans le notre grâce aux html imports :

<!-- my-notif.html -->
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-icon-button/core-icon-button.html">
<!-- … -->

Enfin nous remplaçons notre balise img par notre core-icon-button. Comme nous voulons rester génériques nous n’utiliserons pas les icônes qui viennent avec le composant, mais laisserons à l’utilisateur (développeur qui utilise notre composant) la liberté de préciser l’URL de l’image de son choix. ( grace au data binding de src)

<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-icon-button/core-icon-button.html">

<polymer-element name="my-notif" attributes="src" noscript>

    <template>

        <style>
            * {
                -webkit-box-sizing: border-box;
                -moz-box-sizing: border-box;
                -ms-box-sizing: border-box;
                box-sizing: border-box;
            }

            :host {
                padding: 10px;
                border: 1px solid lightgrey;
                font-weight: bold;
            }
        </style>


        <core-icon-button src=""></core-icon-button>
        
        <content></content>
    </template>

</polymer-element>

Ce qui réduit de nouveau la quantité de code et le balisage à maintenir ! Pour instancier l’élément rien n’a changé il suffit toujours de passer l’URI de l’icône dans l’attribut src

Lézévènements

Un bouton c’est bien, mais encore faut-il qu’il ait une fonction sinon c’est de l’acné !

Polymer fournit des raccourcis pour attacher de évènements aux composants.

Nous allons donc faire en sorte de pouvoir affecter un « callback » à notre instance pour qu’il soit exécuté lors d’un « tap » sur notre icône.

Une fois n’est pas coutume, faisons simple et utilisons l’api declarative pour ajouter un « event listener » et son « callback » interne au composant.

Sur notre core-icon-button ajoutons le « listener » on-tap que nous lions au callback interne que nous nommerons tapedIcon

<core-icon-button src="" on-tap=></core-icon-button>

Puisque nous allons ajouter des scripts dans notre élément n’oublions pas de retirer le noscript de la balise et ajoutons les fonctions qui géreront nos évènements:

<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-icon-button/core-icon-button.html">

<polymer-element name="my-notif" attributes="src">

    <template>

        <style>
            * {
                -webkit-box-sizing: border-box;
                -moz-box-sizing: border-box;
                -ms-box-sizing: border-box;
                box-sizing: border-box;
            }

            :host {
                padding: 10px;
                border: 1px solid lightgrey;
                font-weight: bold;
            }
        </style>


        <core-icon-button id='notif-icon' src="" on-tap=""></core-icon-button>
        
        <content></content>
    </template>

    <script>
        Polymer('my-notif', {
                    tapedIcon: function () {
                        return this.tapedCallback();
                    },
                    tapedCallback: function () {
                        return console.log('taped icon');
                    }
                }
        );
    </script>

</polymer-element>

Dans la fonction d’enregistrement de notre élément Polymer() nous ajoutons une première fonction tapedIcon qui sera appelée sur l’évènement « tap » et sera chargée d’appeler une autre fonction.

Je procède comme ceci, car ca permet de gérer un cycle de vie interne avant d’appeler la fonction tapedCallback qui sera utilisée pour passer un « callback » depuis l’exterieur de notre composant. Par défaut elle affichera un message dans la console.

Puis le plus simplement du monde puisque nous voulions un moyen de mettre a zéro notre compteur de notification, nous allons modifier ce comportement par défaut.

document.querySelector('#notif').tapedCallback= function () {
    return this.count = 0;
}

Simplissime n’est-ce pas ? Si notre compteur était alimenté par une requête à un backend notre fonction pourrait envoyer une requête à celui-ci pour remettre à zéro coté backend également.

Dans ce sens notre composant a une API très flexible !

Voici le fichier du composant au complet :

<!-- my-notif.html -->
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-icon-button/core-icon-button.html">

<polymer-element name="my-notif" attributes="src">

    <template>

        <style>
            * {
                -webkit-box-sizing: border-box;
                -moz-box-sizing: border-box;
                -ms-box-sizing: border-box;
                box-sizing: border-box;
            }

            :host {
                padding: 10px;
                border: 1px solid lightgrey;
                font-weight: bold;
            }
        </style>

        <core-icon-button id='notif-icon' src="" on-tap=""></core-icon-button>
        
        <content></content>
    </template>

    <script>
        Polymer('my-notif', {
                    tapedIcon: function () {
                        return this.tapedCallback();
                    },
                    tapedCallback: function () {
                        return console.log('taped icon');
                    }
                }
        );
    </script>

</polymer-element>

Et le fichier de démo où nous utilisons notre composant :

<!doctype html>
<html lang="en">
    <link rel="import" href="my-notif.html">
<head>
    <meta charset="UTF-8">
    <title>My Notif Demo</title>
</head>
    <body unresolved>
        <my-notif  id='notif' src="demo-icon.svg">
            Messages
        </my-notif>
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            document.querySelector('#notif').count= function () {return Math.floor(Math.random() * 10)}.call();
            document.querySelector('#notif').tapedCallback= function () {
                return this.count = 0;
            }
        });
    </script>
    </body>
</html>

Enfin voici la version CodePen, pour un aperçu :

See the Pen Polymer example by Cedric Brancourt (@Electron-libre) on CodePen.

Conclusion

Nous avons survolé plusieurs principes de Polymer dans un exemple dont le cas d’utilisation est simpliste.

Les « data binding », la composition d’éléments et une notion de gestion d’évènements, mais le plus important, c’est qu’ils soient réutilisables. Ce qui permettra de recomposer notre élément dans un composant plus spécialisé, qui se chargera par exemple de lier les attributs count et tapedCallback à des requêtes ajax et qui exposera comme api dans ses attributs les url de ses requêtes.

Il pourrait ressembler à ceci :

<my-remote-notif count-url='/api/count.json' reset-url='/api/count.json' reset-method='post'></my-remote-notif>

Essayez, ça peut être un bon exercice !

L’équipe Synbioz.

Libres d’être ensemble.