Vue.js 2 : les composants (1/2)

Vous connaissez peut-être les Web Components. C’est un nouveau standard qui permet d’enrichir le HTML de façon modulaire. En gros on peut créer une entité qui utilise des éléments HTML et des fonctionnalités propres, le tout facile à intégrer avec une simple balise personnalisée. On peut espérer que ce standard réduira la prolifération des widgets Javascript et engendrera une certaine homogénéité. Ce qui est certain c’est que le HTML est plutôt limité et ne propose que des éléments simples, on aimerait disposer d’info-bulles, de menus… Mais on est encore loin du but, vous pouvez lire cet excellent article qui résume la situation.

On va voir dans cet article que vue.js propose une approche simple et efficace très fortement inspirée de polymer avec comme grande différence qu’au niveau de la machinerie il est fait appel exclusivement aux dernières possibilités des web components, ce qui améliore les performances.

Pouvoir créer des composant est sans doute un des aspects les plus intéressants de Vue.js, surtout lorsqu’on se lance dans une application ambitieuse. Le code est ainsi bien encapsulé et réutilisable. Un composant est ainsi un élément isolé, autonome, qui possède des propriétés et des comportement et qu’on peut utiliser dircetement dans le HTML…

On va commencer à vois les composants dans cet article. Je développerai le sujet dans un prochain article.

Mon premier composant

Alors on se lance et on crée un petit composant. Au niveau du Javascript c’est tout simple :

Vue.component('mon-composant', {
  template: '<p>Mon premier composant !</p>'
});

La méthode component crée (on dit qu’on l’enregistre) le composant dont on précise le nom (mon-composant) et le template. C’est vraiment le minimum pour un composant ! Il faut aussi initialiser la VueModèle comme d’habitude :

new Vue({
  el: '#tuto'
});

Ensuite on utilise la balise personnalisée dans le HTML :

<div id="tuto">
  <mon-composant></mon-composant>
</div>

Avec ce résultat :

Mon premier composant !

Bon pour le moment c’est très simple et on a vu qu’on pouvait réaliser cela dans l’article précédent avec une directive élément. Mais on va voir que les composants vont bien plus loin !

Une petite illustration du fonctionnement :

Passage de données

Les props

Créer un composant c’est bien mais tout ce qui se trouve à l’intérieur est isolé du reste du monde. Autrement dit le composant n’a accès à aucune donnée en dehors des siennes. Il arrive souvent qu’on ait besoin de transmettre des informations pour renseigner le composant. Voyons comment réaliser cela. On va ajouter une propriété au composant :

Vue.component('nom', {
  props: ['nom'],
  template: '<p>Mon nom est {{nom}}</p>'
});

Les props sont des propriétés pour lesquels le composant attend des valeurs. C’est un tableau, ici on a juste prévu la clé nom. Il ne reste plus qu’à renseigner cette clé dans le HTML :

<div id="tuto">
  <nom nom="Toto"></nom>
</div>

Ce qui donne au final :

Mon nom est Toto

Voici une schématisation du fonctionnement :

img07

Lorsqu’on crée un composant il devient un enfant du composant dans lequel on le crée. Ici le parent est l’instance principale.

camelCase

Les attributs HTML ne sont pas sensibles à la casses, majuscules ou minuscules, ils digèrent tout ça indifféremment. Cela peut être un piège si vous utilisez la notation camelCase. Par exemple vous créez ce composant :

Vue.component('nom', {
  props: ['monNom'],
  template: '<p>Mon nom est {{monNom}}</p>'
});

Et vous utilisez ce HTML :

<nom monNom='Toto'></nom>

Mais tout ce que vous obtenez est :

Mon nom est

Pour obtenir le bon résultat il faut utiliser l’équivalent avec trait d’union (hyphenated) :

<nom mon-nom="Toto"></nom>

Vous obtenez bien alors :

Mon nom est Toto

Attribut dynamique

Un attribut statique c’est bien mais un dynamique c’est encore mieux. On voudrait par exemple entrer le nom dans une zone de texte et qu’il s’affiche avec le composant. Essayons avec ce HTML :

<div id="tuto">
  <input v-model="nomSaisi">
  <br>
  <nom :nom="nomSaisi"></nom>
</div>

On prévoit l’argument nom pour le composant avec la directive v-bind (avac sa syntaxe simplifiée : ), une propriété liée nomSaisi pour la VueModèle.

Avec ce Javascript :

Vue.component('nom', {
  props: ['nom'],
  template: '<p>Mon nom est <strong>{{nom}}</strong></p>'
});

new Vue({
  el: '#tuto',
  data: { nomSaisi: '' }
});

Avec ce résultat :

img01

On retrouve en fait les possibilités qu’on avait déjà rencontrées mais adaptées à un composant.

Gestion des listes

Une action très fréquente consiste à générer une liste de données, on a déjà eu l’occasion d’utiliser la directive v-for pour le réaliser. Est-ce que ça peut fonctionner avec un composant ? Voici un exemple :

<div id="tuto">
  <liste v-for="personne in personnes" :personne="personne"></liste>
</div>

Et le Javascript :

new Vue({
  el: '#tuto',
  data: {
    personnes: [
      {nom: "Durand", prenom: "Jacques"},
      {nom: "Dupont", prenom: "Albert"},
      {nom: "Martin", prenom: "Denis"},
    ]
  },
  components: {
    'liste': {
      props: ['personne'],
      template: '<li>{{personne.nom}} {{personne.prenom}}</li>'
    }
  }
});

Avec ce résultat :

img02

Pour chaque élément de la liste une instance du composant est créée, comme on pouvait logiquement s’y attendre. Notez que les données ne sont pas automatiquement envoyées dans le composant qui est parfaitement isolé. Il faut encore déclarer une propriété.

On pourrait évidemment là aussi déclarer le composant de façon globale avec le même fonctionnement :

Vue.component('liste', {
  props: ['personne'],
  template: '<li>{{personne.nom}} {{personne.prenom}}</li>'
});

new Vue({
  el: '#tuto',
  data: {
    personnes: [
      {nom: "Durand", prenom: "Jacques"},
      {nom: "Dupont", prenom: "Albert"},
      {nom: "Martin", prenom: "Denis"},
    ]
  }
});

Un tableau

Allons un peu plus loin et créons un composant pour générer un tableau. On veut ce HTML :

<div id="tuto">
  <tableau :personnes="personnes"></tableau>
</div>

Donc un composant tableau auquel on transmet les données à afficher. On va conserver les données vues précédemment. Voici le Javascript :

var vm = new Vue({
  el: '#tuto',
  data: {
    personnes: [
      {nom: "Durand", prenom: "Jacques"},
      {nom: "Dupont", prenom: "Albert"},
      {nom: "Martin", prenom: "Denis"},
    ]
  },
  components: {
    tableau: {
      props: ['personnes'],
      template: '<table class="table table-bordered">\n' + 
        '<tr v-for="personne in personnes">\n' +
        '<td v-text="personne.nom"></td>\n' +
        '<td v-text="personne.prenom"></td>\n' +
        '</tr>\n' + 
        '</table>\n'            
    }
  }
});

Avec création du tableau :

img03

On peut répercuter tout changement dans les données au niveau du tableau. Par exemple si vous ajoutez ce code :

setTimeout(function() {
  Vue.set(vm.personnes, 1, {nom: "Claret", prenom: "Marcel"});
}, 2000);

Au bout de 2 secondes vous allez voir le tableau changer pour cette ligne.

Un composant générique

Le tableau réalisé ci-dessus est totalement adapté aux données concernées. On pourrait aborder cela d’une façon plus générale et créer un composant réutilisable qui accepterait des noms de colonnes et des données sans nécessairement en connaître le nombre :

<div id="tuto">
  <tableau
    :colonnes="colonnes" 
    :lignes="personnes">
  </tableau>
</div>

Ici on utilise un composant tableau en lui transmettant les colonnes et les lignes de données.

Voici le Javascript :

Vue.component('tableau', {
  props: ['colonnes', 'lignes'],
  template: '<table class="table table-bordered">\n' +
    '<thead>\n' +
    '<tr>\n' +
    '<th v-for="value in colonnes">{{ value }}</th>\n' + 
    '</tr>\n' +
    '</thead>\n' + 
    '<tr v-for="ligne in lignes">\n' +
    '<td v-for="value in ligne">{{ value }}</td>\n' +
    '</tr>\n' + 
    '</table>\n'  
});

new Vue({
  el: '#tuto',
  data: {
    colonnes: ['Nom', 'Prénom'],
    personnes: [
      ["Durand", "Jacques"],
      ["Dupont", "Albert"],
      ["Martin", "Denis"],
    ]
  }
});

Et ce résultat :

img04

On constate que maintenant le composant peut resservir dans un autre contexte parce qu’il est codé de façon générique. Remarquez que j’ai également transformé les données des personnes pour les rendre également génériques et ainsi simplifier le codage.

Un template élégant

Le code ci-dessus est élégant, mis à part la partie template qui fait un peu désordre. Il serait bien de pouvoir définir ce template avec une mise en page du code harmonieuse.

On peut inclure le template dans le HTML avec la nouvelle balise template qui est maintenant bien prise en charge par les navigateurs. Voici le nouveau HTML intégrant le template :

<template id="tableau-template">
  <table class="table table-bordered">
    <thead>
      <tr>
        <th v-for="value in colonnes">
          {{ value }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="ligne in lignes">
        <td v-for="value in ligne">
          {{ value }}
        </td>
      </tr>
    </tbody>
  </table>
</template>

<div id="tuto">
  <tableau
    :colonnes="colonnes" 
    :lignes="personnes">
  </tableau>
</div>

Remarquez qu’on a un identifiant (tableau-template) qui va permettre de référencer ce template dans le composant :

Vue.component('tableau', {
  props: ['colonnes', 'lignes'],
  template: '#tableau-template'
});

new Vue({
  el: '#tuto',
  data: {
    colonnes: ['Nom', 'Prénom'],
    personnes: [
      ["Durand", "Jacques"],
      ["Dupont", "Albert"],
      ["Martin", "Denis"],
    ]
  }
});

Avec évidemment le même résultat que ci-dessus :

img04

Cette fois le code est vraiment propre.

Le panier revisité

Comme exemple du précédent article sur les directives personnalisées on a amélioré le panier. Je vous propose de reprendre cet exemple mais cette fois de créer ce panier sous forme de composant. On va au passage se débarrasser de la directive personnalisée qui n’est plus pertinente, par contre on va voir qu’un composant peut disposer de toutes les propriétés que nous avons rencontrées dans cette série d’articles.

Voici le code complet de la page :

<!DOCTYPE html>
<html lang="fr">

  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test vue.js</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
  </head>

  <body>

    <div class="container">
      <br>

      <template id="panier-template">
          <div class="panel panel-primary">
            <div class="panel-heading">Panier</div>        
            <table class="table table-bordered table-striped">
              <thead>
                <tr>
                 <th class="col-sm-4">Article</th>
                 <th class="col-sm-2">Quantité</th>
                 <th class="col-sm-2">Prix</th>
                 <th class="col-sm-2">Total</th>
                 <th class="col-sm-1"></th>
                 <th class="col-sm-1"></th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="(item, index) in panier">
                  <td>{{ item.article }}</td>
                  <td>{{ item.quantite }}</td> 
                  <td>{{ item.prix }} €</td>
                  <td>{{ (item.quantite * item.prix).toFixed(2) }} €</td>
                    <td><button class="btn btn-info btn-block" @click="modifier(index)"><i class="fa fa-edit fa-lg"></i></button></td>
                  <td><button class="btn btn-danger btn-block" @click="supprimer(index)"><i class="fa fa-trash-o fa-lg"></i></button></td>
                </tr> 
                <tr>
                  <td colspan="3"></td>
                  <td><strong>{{ total }} €</strong></td>
                  <td colspan="2"></td>
                </tr> 
                <tr>
                  <td><input type="text" class="form-control" v-model="input.article" ref="modif" placeholder="Article"></td>
                  <td><input type="text" class="form-control" v-model="input.quantite" placeholder="Quantité"></td>
                  <td><input type="text" class="form-control" v-model="input.prix" placeholder="Prix"></td>
                  <td colspan="3"><button class="btn btn-primary btn-block" v-on:click="ajouter()">Ajouter</button></td>
                </tr>
              </tbody>       
            </table>
          </div>  
      </template>

      <div id="tuto">
        <panier :panier="panier"></panier>
      </div>

    </div>

    <script src="https://unpkg.com/vue@2.0.3/dist/vue.js"></script>

    <script>

      Vue.component('panier', {
        props: ['panier'],
        template: '#panier-template',
        data: function () {
          return {
            input: { article: '', quantite: 0, prix: 0 }
          }
        },
        computed: {
            total: function () {
                var total = 0;
                this.panier.forEach(function(el) {
                    total += el.prix * el.quantite;
                });
                return total.toFixed(2);
            }
        },
        methods: {
            ajouter: function() {
                this.panier.push(this.input);
                this.input = { article: '', quantite: 0, prix: 0 };
            },
            modifier: function(index) {
                this.input = this.panier[index];
                this.panier.splice(index, 1);
                this.$refs.modif.focus();
            },
                supprimer: function(index) {
                this.panier.splice(index, 1);
            },
        }
      });

      new Vue({
        el: '#tuto',
        data: {
          panier: [
            { article: "Cahier", quantite: 2, prix: '5.30' },
            { article: "Crayon", quantite: 4, prix: '1.10' },
            { article: "Gomme", quantite: 1, prix: '3.25' }
          ],
        }
      });

    </script>

  </body>

</html>

Avec évidemment les mêmes rendu et fonctionnement que lors du chapitre précédent :

img09

Vous voyez ainsi qu’il est relativement facile de créer des composant réutilisables !

Si vous voulez suivre l’évolution des données lors des manipulations du panier il suffit d’ajouter cette ligne au HTML :

<div id="tuto">
  <panier :panier="panier"></panier>
  <pre>{{ $data }}</pre>
</div>

Ca permet d’afficher correctement les données :

img05

Cet article est loin d’épuiser les possibilités des composants mais vous avez à présents de très bonnes bases. Je vous invite à consulter la documentation et cet excellent exemple pour compléter vos connaissances.

Vue devtools

Pour déboguer les application vue.js il existe un plugin pour Chrome. Lorsque vous l’avez installé vous disposez d’un nouveau bouton dans la fenêtre des outils de développement :

img16Vous pouvez ainsi inspecter vos composants, voilà ce que ça donne pour le panier :

img17

c’est un outil qui devient très intéressant lorsque vous avez de nombreux composants !

En résumé

  • Vue.js permet la création de composants pour enrichir le HTML.

  • Un composant peut utiliser toutes les options existantes (mis à part el).

  • Il est possible de passer des données à un composant.

  • Un composant peut hériter des données de son parent.

  • On peut manipuler des listes avec un composant.

  • On peut ajouter un outil de débogage pour vue.js à Chrome.

Laisser un commentaire