Taille de texte par Defaut
Agrandir la Taille du Texte
Réduire la Taille du Texte
Options d'accessibilité
Informatique

Rencontre avec Polymer : un radial menu

itk IT
5 juillet 2016
Pas de commentaires

Rencontre avec Polymer

Cette année, 9 développeurs sont partis à  DevoxxFR. Certains ont suivi une conférence sur Polymer avec Horacio Gonzalez (lien sur vidéo du talk) et ils nous ont fait une restitution en quelques minutes qui m’a donné envie de tester ce framework.

Le sujet

L’idée originale vient de notre UX Designer Laurent, qui nous avait fait rapidement la conception d’un menu radial il y a quelques temps : un simple bouton qui dévoile des sous-menus lorsque l’on clique dessus. C’est typiquement un bon choix de sujet : partir d’une idée de composant réutilisable et le polymériser…

Polymer : Les bases

Polymer c’est avant tout un développement orienté composant (appelé « Element »). Le but est de définir ses propres balises comme on pourrait le faire avec une directive AngularJS.
Cela fait tellement partie de la philosophie de Polymer que même le contenu de votre index.html est un élément :

<skill-app>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
  <title>Skill Manager</title>
  <link rel="shortcut icon" sizes="32x32" href="/images/app-icon-32.png">
  <script src="./bower_components/webcomponentsjs/webcomponents-lite.js"></script>
  <link rel="import" href="/src/skill-app.html">
</head>
<body>
  <skill-app></skill-app>
</body>
</html>

Et oui c’est tout ! Un script qui contient la base pour les webcomponent et un import pour votre élément principal et c’est tout !

Création d’un élément, le itk-radial-menu

La création d’un élément n’est pas plus compliqué !

radial-menu

Tout élément Polymer comporte les éléments suivants:

<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module id="itk-radial-menu">

    <template>
        <style>
        </style>
    </template>

    <script>
        Polymer({
            is: 'itk-radial-menu',
        });
    </script>

</dom-module>

1- L’import vers Polymer
2- La balise dom-module qui définit les contours de l’élément ainsi que son ID (l’ID doit être formé de la façon suivante <prefix>-<nom de l’élément>)
3- Un template (le code HTML ainsi que le style du composant)
4- Le code JavaScript du composant dans lequel est rappelé l’ID de l’élément

Les propriétés de l’élément

D’un point de vue fonctionnel, cet élément a besoin des propriétés suivantes:
1- D’un rayon sur lequel vont s’afficher les sous-menus
2- D’un angle de départ et d’un angle de fin (les sous-menus seront répartis selon ces 2 angles)
3- D’une liste de sous-menus (avec un icon et un titre)

Pour définir une propriété avec Polymer il faut rajouter l’attribut « properties » et définir chacune des propriétés:

           is: 'itk-radial-menu',
            properties: {
                radius: {
                    type: Number,
                    value: 80,
                    observer: '_radiusChanged'
                },
                startAngle: {
                    type: Number,
                    value: -Math.PI
                },
                endAngle: {
                    type: Number,
                    value: Math.PI
                },
                items: {
                    type: Array,
                    value : function() { return []; }
                }
            },

Chaque propriété définit :
1- Son type : Number, String, Array, Object, Boolean
2- Sa valeur par défaut (optionnel)
3- Et d’autres choses comme par exemple un observer qui sera appelé à chaque modification de valeur (optionnel)

Tips: ATTENTION aux objets de type Array ou Object. Javascript utilise des références.
Typiquement si vous initialisez

items

  de la façon suivante : 

items: {
  type: Array,
  value : [];
}

Alors toutes les instances de type itk-radial-menu vont partager le même tableau : il s’agira d’un singleton. Pour éviter cela il faut définir une fonction qui renvoie un nouveau tableau à chaque appel.

 

Les propriétés sont des caractéristiques dont on peut fixer la valeur à l’extérieur du composant. Ainsi lors de son utilisation du composant, il nous sera possible d’écrire (par convention une propriété en CamelCase est accessible à l’extérieur en utilisant le caractère ‘-‘ lorsqu’une majuscule est présente) :

<itk-radial-menu radius="70" start-angle="[[sAngle]]" end-angle="[[eAngle]]" items='[{"icon": "favorite"},{"icon": "menu"},{"icon": "file-download"}]'></itk-radial-menu>
....
this.sAngle = Math.PI/2;
this.eAngle = 3 * Math.PI/2;

Ici on fixe les valeurs sAngle et eAngle et on les passe aux propriétés start-angle et end-angle du composant. On notera la notation [[sAngle]] qui signifie que même si le composant itk-radial-menu modifie cette valeur, on ne souhaite pas répercuter ce changement dans sAngle. Si j’avais utilisé {{sAngle}}, un changement de valeur dans le composant aurait répercuté la valeur dans startAngle.
La propriété radius est utilisée avec une valeur constante. Quant à la propriété items c’est un tableau d’objets JSON qui est utilisé.

Le code HTML

        <nav class="circular-menu">
            <div id="circle" class="circle">
                <template is="dom-repeat" items="{{items}}" as="menu">
                    <paper-icon-button style="{{_style(index)}}" icon="{{menu.icon}}" src="{{menu.src}}" title="{{menu.title}}" on-tap="_tapAction"></paper-icon-button>
                </template>
            </div>
            <paper-fab elevation="1" on-tap="_toggleMenu" icon="apps"></paper-fab>
        </nav>

Simple comme code ! Non ?

<template is="dom-repeat" items="{{items}}" as="menu">

En fait il s’agit tout simplement d’une boucle (

is="dom-repeat"

 ) sur la propriété « items ». A l’intérieur du template, chaque élément sera accessible via la variable « menu » (

as='menu"

 ). Il est possible d’utiliser les attributs de la variable via la « notation « dot » (ie

{{menu.title}}

 ).

Le code HTML utilise 2 tags non standards :

paper-fab

  et

paper-icon

 . Il s’agit d’éléments prêts à l’emploi dans le catalogue de Polymer.
Pour pouvoir utiliser un élément externe (du catalogue Polymer ou d’ailleurs), il suffit d’installer l’élément dans son projet

bower install --save PolymerElements/paper-fab

Puis d’importer l’élément dans sa page

<link rel="import" href="../bower_components/paper-fab/paper-fab.html">

Méthodes de l’élément

Déclarer une méthode sur un composant est aussi simple que rajouter une fonction dans le code Javascript de l’élément

        Polymer({
            is: 'itk-radial-menu',
            ....
            _style: function(index) {
                var partialCircle = Math.abs(this.endAngle - this.startAngle) == 2 * Math.PI ? 0 : 1;
                var anglePerItem = (this.endAngle - this.startAngle) / (this.items.length - partialCircle);
                var angle = this.startAngle + anglePerItem * index;
                var style = 'left: '+(this.radius + this.radius * Math.cos(angle)).toFixed(4) + "px; ";
                style = style + 'top: ' + (this.radius + this.radius * Math.sin(angle)).toFixed(4) + "px; ";
                return style;
            },

Par convention il est bien de spécifier les méthodes dites privées avec un ‘_’. Cette méthode permet de calculer la position des sous-menus en fonction du rayon et des 2 angles définis de façon dynamique en fonction de chaque élément. Elle est appelée dans le code HTML en se servant d’une variable toujours à disposition dans les templates dom-repeat : « index ».

<paper-icon-button style="[[_style(index)]]" ....

Ouverture du menu

Notre élément commence à prendre forme mais les items du sous-menu ne sont pas visibles ! Il nous faut donc réagir au click sur le bouton principal pour ouvrir le menu.
Pour cela il faut rajouter la gestion de l’événement « on-tap » sur le bouton principal afin d’appeler une méthode de notre élément (on-tap est préférable à on-click pour gérer correctement les tablettes et smartphones).

<paper-fab elevation="1" on-tap="_toggleMenu" icon="apps"></paper-fab>

Dans la méthode, on permute une classe CSS afin de faire jouer des transitions CSS

        Polymer({

            is: 'itk-radial-menu',
            ....
            _toggleMenu: function() {
                this.$.circle.classList.toggle('open');
            },

Note: Pour accéder facilement à un élément de notre DOM de l’élément qui comporte un ID, il suffit d’utiliser la notation 

this.$.<id du noeud DOM>

A noter que cette notation ne marche que pour les éléments statiques. Pour les boucles qui construisent des noeuds dynamiquement, il est possible d’utiliser la notation  

this.$$.<id du noeud DOM>

  (je n’ai pas encore eu l’occasion de tester cette notation…).

Un peu de CSS

Maintenant, il ne reste qu’à enrober tout cela d’une pincée de CSS, ne serait-ce que pour les transitions.

            .circle {
                position:absolute;
                top:50%;
                left:50%;
                opacity: 0;
                -webkit-transform: scale(0);
                -moz-transform: scale(0);
                transform: scale(0);
                -webkit-transition: all 0.2s ease-out;
                -moz-transition: all 0.2s ease-out;
                transition: all 0.2s ease-out;
                box-shadow: var(--itk-radial-menu-circle);
                border-radius:50%;
            }

            .open.circle {
                opacity: 1;
                -webkit-transform: scale(1);
                -moz-transform: scale(1);
                transform: scale(1);
            }

Avoir un seul menu ouvert

Dernière fonctionnalité du composant, un seul menu ouvert même si plusieurs instances de itk-radial-menu cohabitent sur la même page !
Pour cela, chaque ouverture du menu va générer un événement « open ». Chaque élément ouvert qui recevra cet événement saura alors qu’il doit se fermer puisqu’un autre menu quelque part dans l’espace vient de s’ouvrir !

Pour cela on surcharge la méthode « ready » qui fait partie du cycle de vie de Polymer (cette méthode est appelée lorsque les propriétés de l’élément sont initialisées), dans laquelle on va déclarer le listener sur l’événement « open »

Polymer({

    is: 'itk-radial-menu',
    ....
    ready: function() {
        this.open = false;
        document.addEventListener('open', function(event) {
            if (event.detail !== this && this.$.circle.classList.contains('open')) {
                this._toggleMenu();
            }
        }.bind(this));
    }

Et on déclenche l’événement à l’ouverture du menu

_toggleMenu: function() {
    this.$.circle.classList.toggle('open');
    if (this.$.circle.classList.contains('open')) {
        this.fire("open", this);
    }
    this.open = !this.open;
},

Conclusion

Voilà notre 1er composant prêt et opérationnel !

Alors qu’est ce que j’en pense au final ? Du bien, du très grand bien !
+ D’abord parce que le développement orienté composant est une bonne manière de voir les choses
+ Polymer est une implémentation des WebComponent qui va être implémentée dans tous les navigateurs
+ Polymer comble les manques encore non implémentés par les navigateurs grâce aux Polyfill
+ Facile à prendre en main
+ Les styles sont scopés à votre élément donc pas de collision
– Beaucoup de choses et malgré une documentation soignée, il ne faut pas faire l’impasse sur la lecture de la documentation: RTFM
– Ca reste du Javascript (le self, les observers, l’initialisation des Object ou Array, …)
– Le debug reste compliqué
– Pas d’information dans la console si vous avez oublié un import mais que vous utilisez un élément
– Le Tooling est en cours et cela commence à ressembler à quelque chose (comme dans le monde Javascript d’ailleurs)
– Pas d’erreur lorsque l’on tape quelque chose de faux (IntelliJ aide mais quand même…)

Il faudrait pousser les et développer des composants Polymer, les intégrer dans une application AngularJS / ReactJS pour voir si cela fonctionne vraiment bien et estimer le poids supplémentaire induit par cette utilisation.
Ceci dit je crois que les WebComponents sont un bon moyen de s’abstraire de frameworks de plus haut niveau et de pouvoir rapidement changer de technos. Polymer étant une implémentation et qui en plus comble le manque des navigateurs grâce aux Polyfill, je pense que c’est un bon choix aujourd’hui… Si vous avez des éléments dans ce sens ou dans le sens contraire, n’hésitez pas à faire un commentaire il est plus que bienvenu !

PS: Cet élément s’intègre dans une application plus vaste en cours de construction et qui va servir à gérer les compétences à ITK. Voici une petite animation de ce que cela donne.

skill-manager

 

Commentaires 0

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.