Vue.js

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

J’ai commencé à présenter les composants dans le précédent article. 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 article en voyant la communication inverse : de l’enfant vers le parent. On va en profiter pour évoquer aussi quelques autres éléments.

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 :

img18

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 comportement 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 :

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
      }
    }
  }
})

 

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 pour enlever un chiffre. On va créer un composant bouton pour réaliser ça. Voici le code complet de l’exemple :

<!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 :

img19

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

img20

Bon mon exemple ne sert à rien, je l’avoue, mais au moins à illustrer le sujet de cet article !

Le composant

Voici le code du composant bouton :

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 :

img21

Le parent

Dans le template du parent on va utiliser deux boutons :

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

On transmet à ce niveau des 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 :

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.

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 :

<!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 :

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ée :

<saisie v-model="nombre"></saisie>

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

img22

On peut se demander l’intérêt de procéder ainsi, dans la documentation il est évoqué 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à l’essentiel. Pour le reste je vous renvoie à la documentation.

Le panier revisité

Pour terminer cet article on va reprendre l’exemple du panier en ajoutant un composant pour l’édition et l’ajout des articles. Voilà le nouveau code :

<!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 vairer un peu l’apparence :

img23

On a toujours le composant panier :

img24

Et dans celui-ci on a le composant editeur :

img25

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

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.

img26

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

<editeur :article="article" @add="ajouter"></editeur>

Pour le reste on retrouve ce qu’on a vu précédemment mis en oeuvre.

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.

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.
Print Friendly, PDF & Email

Laisser un commentaire