Tutoriel pour apprendre à utiliser le framework Vue.js version 2

Les bases


précédentsommaire

VII. Les composants (2/2)

J'ai commencé à présenter les composants dans le précédent chapitre. On a vu qu'ils constituent un aspect important de Vue.js et qu'il sont faciles à créer. On a également vu qu'ils sont organisés hiérarchiquement et qu'on peut passer des informations (props) du parent à l'enfant.

On va continuer notre exploration des composants dans le présent chapitre en voyant la communication inverse : de l'enfant vers le parent. On va en profiter pour évoquer aussi quelques autres éléments.

VII-A. La communication entre composants

Les composants étant isolés, il faut prévoir une procédure pour qu'ils échangent des informations. On a vu les props dans le précédent article, c'est la communication descendante, du parent vers l'enfant. De façon symétrique on aura des événements pour la communication ascendante : de l'enfant vers le parent :

Image non disponible

VII-A-1. Compléments sur les props

Pour les props il faut tenir compte d'une contrainte : si la valeur change au niveau du parent ce changement est répercuté à l'enfant, mais l'inverse n'est pas vrai ! En effet on pourrait avoir des comportements douteux si un enfant pouvait modifier des valeurs chez un parent…

Donc il ne faut pas modifier la valeur d'un prop dans un composant !

Si on a besoin de changer la valeur d'un prop dans un enfant il faut donc d'abord créer une propriété locale ou calculée. On verra ça dans l'exemple en fin d'article.

Il est aussi possible d'effectuer une validation des props pour être sûr de recevoir des données correctes. On peut obliger une valeur à être présente, prévoir une valeur par défaut, un traitement personnalisé… Cette possibilité n'est judicieuse que si vous avez l'intention de créer un composant public. On a ces types disponibles :

  • String ;
  • Number ;
  • Boolean ;
  • Function ;
  • Object ;
  • Array.

Voici un résumé qui s'inspire de l'exemple de la documentation :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
Vue.component('mon_composant', {
  props: {
    prop1: Number,  // type unique
    prop2: [Object, Array],  // types multiples
    prop3: {
      type: Boolean,
      required: true  // valeur booléenne requise
    },
    prop4: {
      type: String,
      default: 'chat' //  une valeur par défaut
    },
    prop5: {
      type: Object,
      default: function () {
        return { animal: 'chat' }  // une fonction pour construire la valeur par défaut
      }
    },
    prop6: {
      validator: function (value) {
        return value < 4  // une validation personnalisée
      }
    }
  }
})

VII-B. Les événements

Un composant peut :

  • émettre un événement : $emit(nom de l'événement) ;
  • écouter un événement : $on(nom de l'événement) ou v-on dans le template pour un parent.

On va considérer un petit exemple pour illustrer ça : on a un nombre auquel on peut ajouter ou retirer des chiffres. On va pour cela utiliser deux boutons : un pour ajouter un chiffre, et un autre pour enlever un chiffre. On va créer un composant bouton pour réaliser ça. Voici le code complet de l'exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
<!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.7/css/bootstrap.min.css">
  </head>

  <body>

    <div class="container">
      <br>
      <div id="tuto">
        <bouton titre="Ajoute chiffre" type='ajoute' @ajoute="ajoute"></bouton>
        <bouton titre="Enlève chiffre" type='enleve' @enleve="enleve"></bouton>
        <h1>{{ nombre }}</h1>
      </div>
    </div>

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

    <script>

      Vue.component('bouton', {
        props: ['titre', 'type'],
        template: '<a class="btn btn-info" href="#" @click="action">{{ titre }}</a>',
        methods: {
          action: function() {
              this.$emit(this.type);
          }
        }
      })

      new Vue({
        el: '#tuto',
        data: {
          nombre: '0'
        },
        methods: {
          ajoute: function() {
            this.nombre += Math.floor(Math.random() * 10);
          },
          enleve: function() {
            this.nombre = this.nombre.slice(0, -1);
          }
        }
      });

    </script>

  </body>

</html>

Au départ on a cet aspect :

Image non disponible

On ajoute ou retire un chiffre en cliquant sur le bouton correspondant :

Image non disponible

Bon ! mon exemple ne sert à rien, je l'avoue, sauf à illustrer le sujet de ce tutoriel !

VII-B-1. Le composant

Voici le code du composant bouton :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
Vue.component('bouton', {
  props: ['titre', 'type'],
  template: '<a class="btn btn-info" href="#" @click="action">{{ titre }}</a>',
  methods: {
    action: function() {
        this.$emit(this.type);
    }
  }
})

On a deux props :

  • titre : le titre du bouton ;
  • type : le type du bouton : ajoute ou enleve.

On a également une détection d'un clic sur le bouton avec une méthode action. On voit que lors d'un clic on émet l'événement qui a comme nom le type du bouton (this.type).

On peut résumer les échanges :

Image non disponible

VII-B-2. Le parent

Dans le template du parent on va utiliser deux boutons :

 
Sélectionnez
<bouton titre="Ajoute chiffre" type='ajoute' @ajoute="ajoute"></bouton>
<bouton titre="Enlève chiffre" type='enleve' @enleve="enleve"></bouton>

On transmet à ce niveau deux props : titre et type. D'autre part on écoute l'événement ajoute ou enleve selon le type du bouton. On procède alors au traitement correspondant dans les méthodes appelées :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
methods: {
  ajoute: function() {
    this.nombre += Math.floor(Math.random() * 10);
  },
  enleve: function() {
    this.nombre = this.nombre.slice(0, -1);
  }
}

Vous voyez qu'il est simple de faire communiquer ainsi des composants hiérarchiques.

VII-C. Contrôle de saisie et événement

On peut aussi imaginer avoir un composant avec un contrôle de saisie, par exemple une zone de texte en liaison avec un composant parent. Par exemple, on peut compléter l'exemple ci-dessus en ajoutant un composant avec une zone de texte reliée à la valeur du nombre. Voilà l'exemple modifié en conséquence :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
<!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.7/css/bootstrap.min.css">
  </head>

  <body>

    <div class="container">
      <br>
      <div id="tuto">
        <bouton titre="Ajoute chiffre" type='ajoute' @ajoute="ajoute"></bouton>
        <bouton titre="Enlève chiffre" type='enleve' @enleve="enleve"></bouton>
        <saisie v-model="nombre"></saisie>
        <h1>{{ nombre }}</h1>
      </div>
    </div>

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

    <script>

      Vue.component('bouton', {
        props: ['titre', 'type'],
        template: '<a class="btn btn-info" href="#" @click="action">{{ titre }}</a>',
        methods: {
          action: function() {
            this.$emit(this.type);
          }
        }
      })

      Vue.component('saisie', {
        template: '<input @input="onInput">',
        methods: {
          onInput: function (event) {
            this.$emit('input', event.target.value);
          }
        }
      })

      new Vue({
        el: '#tuto',
        data: {
          nombre: '0'
        },
        methods: {
          ajoute: function() {
            this.nombre += Math.floor(Math.random() * 10);
          },
          enleve: function() {
            this.nombre = this.nombre.slice(0, -1);
          }
        }
      });

    </script>

  </body>

</html>

Voici le composant pour la zone de saisie :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
Vue.component('saisie', {
  template: '<input @input="onInput">',
  methods: {
    onInput: function (event) {
      this.$emit('input', event.target.value);
    }
  }
})

On intercepte l'événement input, et avec la méthode onInput on émet l'événement input à destination du parent avec la valeur actuelle.

Il suffit d'insérer le composant en prévoyant la liaison de données :

 
Sélectionnez
<saisie v-model="nombre"></saisie>

Maintenant la zone de saisie reflète la valeur du nombre et permet de le modifier :

Image non disponible

On peut se demander quel est l'intérêt de procéder ainsi ; dans la documentation est évoquée la possibilité des types personnalisés de zones de saisie…

Il y aurait encore des choses à dire sur les composants, mais vous avez déjà acquis l'essentiel. Pour le reste je vous renvoie à la documentation.

VII-D. Le panier revisité

Pour terminer ce tutoriel on va reprendre l'exemple du panier, en ajoutant un composant pour l'édition et l'ajout des articles. Voilà le nouveau code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
<!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.7/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/united/bootstrap.min.css">
  </head>

  <body>

    <div class="container">
      <br>

      <script type="text/x-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> 
                <editeur :article="article" @add="ajouter"></editeur>
              </tbody>       
            </table>
          </div>  
      </script>

      <script type="text/x-template" id="editeur-template">
        <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" @click="ajouter()">Ajouter</button></td>
        </tr>
      </script>
      
      <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 {
            article: { 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: {
            modifier: function(index) {
                this.article = this.panier[index];
                this.panier.splice(index, 1);
            },
            supprimer: function(index) {
                this.panier.splice(index, 1);
            },
            ajouter: function(input) {
                this.panier.push(input);
                this.article = { article: '', quantite: 0, prix: 0 };
            }
        },
        components: {
          'editeur': {
            props: ['article'],
            template: '#editeur-template',
            computed: {
                input: function() {
                    return this.article;
                }
            },
            methods: {
                ajouter: function() {
                    this.$emit('add', this.input);
                }
            }            
          }
        }
      });
        
      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>

L'aspect est le même, j'ai juste changé le thème pour varier un peu l'apparence :

Image non disponible

On a toujours le composant panier :

Image non disponible

Et dans celui-ci, on a le composant editeur :

Image non disponible

Le composant editeur est déclaré dans le composant panier :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
components: {
  'editeur': {
    props: ['article'],
    template: '#editeur-template',
    computed: {
        input: function() {
            return this.article;
        }
    },
    methods: {
        ajouter: function() {
            this.$emit('add', this.input);
        }
    }            
  }
}

En entrée (props) on reçoit l'article à éditer article. Comme on sait qu'on n'a pas le droit de le modifier directement, on utilise une propriété calculée input qu'on initialise avec la valeur d'article.

En sortie on émet l'événement add avec comme argument input.

Image non disponible

Le composant editeur est ainsi intégré dans le template du panier :

 
Sélectionnez
<editeur :article="article" @add="ajouter"></editeur>

Pour le reste on retrouve ce qu'on a précédemment mis en œuvre.

Pour la déclaration des templates, j'ai changé de système parce que le fait d'avoir deux balises template sur la même page ne semble pas fonctionner correctement. Je suis donc passé par une balise script de type text/x-template qui ne pose aucun souci.

VII-E. En résumé

  • Un composant enfant ne doit pas modifier la valeur d'un prop.
  • On peut valider le type d'un prop.
  • Un composant enfant envoie des informations à son parent par des événements.

VII-F. Remerciements

Nous tenons à remercier Maurice Chavelli qui nous a donné l'autorisation de publier ce tutoriel.

Nous remercions également Winjerome pour la mise au gabarit, et Maxy35 pour la correction orthographique.


précédentsommaire

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2017 Laravel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.