Tutoriel pour apprendre le framework Vue.js version 1

Plus loin avec Vue.js

Nous avons vu dans la première partie de cette série des notions utiles en programmation et prédéfines dans Vue.js. Dans ce tutoriel, nous allons voir comment définir les notions de filtres, directives et composants sur mesure selon ses besoins.

7 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Créer un filtre

I-A. Les filtres prédéfinis

Vue.js est déjà bien équipé en filtres prédéfinis :

  • capitalize : met le premier caractère en capitale ;
  • uppercase : met tous les caractères en capitales ;
  • lowercase : met tous les caractères en minuscules ;
  • currency : ajoute en début le symbole de la monnaie qu'on peut passer en argument (défaut : $) ;
  • pluralize : permet de mettre au pluriel avec la possibilité de jouer avec plusieurs formes ;
  • json : transforme en JSON ;
  • limitBy : limite le nombre d'arguments dans une liste générée par v-for ;
  • filterBy : filtre les éléments d'une liste générée par v-for ;
  • orderBy : trie les éléments d'une liste générée par v-for.

Ces filtres sont nombreux et bien pratiques, certains sont équipés d'arguments permettant de moduler leurs effets. Nous les avons presque tous rencontrés au cours de la première partie de cette série de tutoriels.

I-B. Un filtre sur mesure

Si on sort des cas d'utilisation des filtres prédéfinis alors c'est le moment d'en créer un personnalisé. Par exemple, il se peut que le filtre currency ne nous plaise pas trop parce qu'il ajoute le symbole de la monnaie au début alors qu'on préfèrerait le voir à la fin. Une bonne occasion de créer un filtre !

Voici le HTML :

 
Sélectionnez
<div id="tuto">
  <p v-text="montant | euros"></p>
</div>

On voit qu'on a associé à la propriété montant le filtre euros qui n'existe pas dans la panoplie de base.

Voici le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
new Vue({
  el: '#tuto',
  data: {
    montant: '23.50'
  },
  filters: {
    euros: function(valeur) {
      return valeur + ' ';
    }
  }
});

La partie nouvelle dans ce code est constituée par la propriété filters qui permet de créer des filtres. Ici on crée le filtre euros et on définit son action avec une fonction.

Un filtre personnalisé comporte automatiquement un paramètre qui transmet la valeur de la propriété associée, ici le contenu de montant. Il suffit alors de retourner ce montant en lui ajoutant le symbole à la fin et on obtient :

23.50 €

Voici une illustration du fonctionnement :

Image non disponible

I-C. Passage d'un argument

Un filtre sur mesure peut aussi accepter un ou plusieurs arguments. Reprenons le cas du symbole monétaire vu ci-dessus et améliorons-le pour accepter le symbole :

Voici le nouveau HTML :

 
Sélectionnez
<p v-text="montant | devise '€'"></p>

Et le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
filters: {
  devise: function(valeur, symbole) {
    return valeur + ' ' + symbole;
  }
}

On voit qu'on récupère l'argument dans la fonction en deuxième position puisque la première est d'office affectée à la valeur de la propriété. Pour le reste le fonctionnement est exactement le même à la différence que maintenant on peut choisir le symbole monétaire sans changer le filtre.

Voici un schéma de fonctionnement :

Image non disponible

I-C-1. Argument optionnel

L'argument d'un filtre peut être optionnel, dans ce cas il prend une valeur par défaut si aucune valeur n'est transmise. C'est le cas de certains filtres prédéfinis. Voyons un cas avec un filtre sur mesure.

Supposons que nous voulons un filtre qui affiche une valeur avec le symbole pourcentage et en limitant le nombre de décimales selon une valeur passée comme argument, avec deux décimales par défaut.

Voici le HTML :

 
Sélectionnez
<div id="tuto">
  <p v-text="montant | pourcentage"></p>
</div>

Et voici le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
new Vue({
  el: '#tuto',
  data: {
    montant: 23.514
  },
  filters: {
    pourcentage: function(valeur, decimales) {
      if(decimales === undefined) {
        decimales = 2;
      }
      return Math.round(valeur * Math.pow(10, decimales)) / Math.pow(10, decimales) + ' %';
    }
  }
});

Le résultat est :

23.51 %

Comme on n'a pas passé de valeur, le nombre de décimales s'est fixé par défaut à 2. Si on en passe un :

 
Sélectionnez
<p v-text="montant | pourcentage 1"></p>

Alors le résultat change en conséquence :

23.5 %

I-D. Argument dynamique

L'argument peut lui-même être une variable est donc posséder une valeur selon le contexte du script. Cela peut être très intéressant, mais commençons par voir ce fonctionnement avec un cas simple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
<div id="tuto">
  <form>
    <div class="form-group">
      <input type="text" class="form-control" v-model="saisie">
    </div>
  </form>
  <p>{{ 'Texte saisi : ' | ajoute saisie }}</p>
</div>

On a une zone de texte liée à la propriété saisie. D'autre part on affiche avec mustache un texte avec un filtre (ajoute) et un argument dynamique pour ce filtre qui est justement la propriété saisie.

Voici le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
new Vue({
  el: '#tuto',
  data: {
    saisie: ''
  },
  filters: {
    ajoute: function(valeur, saisie) {
      return valeur + saisie;
    }
  }
});

Le filtre se contente d'ajouter ce qui est dans saisie au texte :

Image non disponible

Voici une illustration des liaisons :

Image non disponible

Bon c'est un peu embrouillé, mais ça donne une idée du fonctionnement.

L'exemple ci-dessus n'est pas très réaliste, mais il a le mérite de montrer comment ça fonctionne. Voyons donc à présent un cas un peu plus intéressant. On va créer un filtre pour limiter le nombre de mots affichés dans un texte :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
<div id="tuto">
  <p>{{ texte | limite nbrMots }}</p>
  <form>
    <div class="form-group">
      <select class="form-control" v-model="nbrMots">
        <option>1</option>
        <option>2</option>
        <option>3</option>
        <option>4</option>
        <option>5</option>
        <option>6</option>
      </select>
    </div>
  </form>
</div>

On a donc un paragraphe pour l'affichage et une liste de choix avec des valeurs de 1 à 6.

Voici le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
new Vue({
  el: '#tuto',
  data: {
    texte: 'Juste un petit texte pour tester ce filtre.',
    nbrMots: 3
  },
  filters: {
    limite: function(valeur, nbrMots) {
      return valeur.split(' ').slice(0, nbrMots).join(' ');
    }
  }
});

On crée le filtre limite qui attend comme second argument le nombre de mots à afficher.

On obtient :

Image non disponible

On affiche bien trois mots. Si on change la valeur :

Image non disponible

On affiche cette fois cinq mots.

I-E. Filtre à double sens

Jusque là on a vu uniquement comment on peut modifier à l'affichage une donnée récupérée dans le modèle. Et si on faisait aussi l'inverse ?

Prenons un exemple simple : on va entrer du texte en interdisant de raccourcir la longueur du texte déjà saisi :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
<div id="tuto">
  <p>{{ message }}</p>
  <form>
    <div class="form-group">
      <input type="text" class="form-control" v-model="message | limite">
    </div>
  </form>
</div>

Donc une simple zone de texte et une directive v-model dans laquelle on a prévu un filtre personnalisé.

Voilà le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
new Vue({
  el: '#tuto',
  data: {
    message: 'Bonjour à toi'
  },
  filters: {
    limite: {
      read: function(valeur){
        return valeur;
      },
      write: function(nouvelleValeur, ancienneValeur){
        return nouvelleValeur.length < ancienneValeur.length ? ancienneValeur : nouvelleValeur;
      }            
    }
  }
});

On divise le code du filtre en deux fonctions, une pour la lecture (read) et l'autre pour l'écriture (write). Au départ on a :

Image non disponible

Si on ajoute du texte pas de souci :

Image non disponible

Mais on ne peut pas le raccourcir…

Notez que pour la partie lecture, on se contente de renvoyer la valeur. En effet, ce genre de filtre ne peut se concevoir que pour un contrôle de formulaire avec une directive v-model. Or dans ce cas, il y a un double lien et la partie lecture est donc inutile.

I-F. Le panier amélioré

Maintenant que nous savons créer des filtres, même dans les deux sens, nous allons en profiter pour améliorer l'exemple du panier que nous avons vu à la fin de la première partie de cette série de tutoriels. Nous allons :

  • utiliser le filtre devise que nous avons créé dans ce tutoriel ;
  • créer un filtre entier pour filtrer uniquement les nombres entiers en prévoyant une valeur maximale à passer en argument ;
  • créer un filtre flottant pour filtrer les nombres flottants.

Voici le nouveau code de la page :

 
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.
<!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.6/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" id="tuto">
      <br>
 
      <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 in panier">
              <td>{{ item.article | capitalize }}</td>
              <td>{{ item.quantite }}</td> 
              <td>{{ item.prix | devise '' }}</td>
              <td>{{ item.quantite * item.prix | devise '' }}</td>
              <td><button class="btn btn-info btn-block" v-on:click="modifier($index)"><i class="fa fa-edit fa-lg"></button></td>
              <td><button class="btn btn-danger btn-block" v-on:click="supprimer($index)"><i class="fa fa-trash-o fa-lg"></i></button></td>
            </tr> 
            <tr>
              <td colspan="3"></td>
              <td><strong>{{ total | devise '' }}</strong></td>
              <td colspan="2"></td>
            </tr> 
            <tr>
              <td><input type="text" class="form-control" v-model="input.article | capitalize" v-el:modif placeholder="Article"></td>
              <td><input type="text" class="form-control" v-model="input.quantite | entier 10" placeholder="Quantité"></td>
              <td><input type="text" class="form-control" v-model="input.prix | flottant" placeholder="Prix"></td>
              <td colspan="3"><button class="btn btn-primary btn-block" v-on:click="ajouter()">Ajouter</button></td>
            </tr>
          </tbody>       
        </table>
      </div> 
  
    </div>
 
    <script src="http://cdn.jsdelivr.net/vue/1.0.10/vue.min.js"></script>
 
    <script>
 
      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 }
          ],
          input: { article: '', quantite: 0, prix: 0 }
        },
        computed: {
          total: function () {
            var total = 0;
            this.panier.forEach(function(el) {
              total += el.prix * el.quantite;
            });
            return total; 
          }
        },
        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.$$.modif.focus();
          },
          supprimer: function(index) {
            this.panier.splice(index, 1);
          },
        },
        filters: {
          devise: function(valeur, symbole) {
            return valeur + ' ' + symbole;
          },
          entier: {
            read: function(valeur) {
              return valeur;
            },
            write: function(nouvelleValeur, ancienneValeur, max) {
              var valeur = parseInt(nouvelleValeur);
              if(valeur % 1 === 0) {
                return valeur > max ? ancienneValeur : valeur;
              }
              return 0;
            }
          },
          flottant: {
            read: function(valeur) {
              return valeur;
            },
            write: function(nouvelleValeur, ancienneValeur) {
              return isNaN(nouvelleValeur) ? ancienneValeur : nouvelleValeur;
            }
          }       
        }
      });
 
    </script>
 
  </body>
 
</html>

On voit qu'on utilise maintenant le filtre devise à la place du filtre prédéfini currency :

 
Sélectionnez
1.
2.
3.
4.
<td>{{ item.prix | devise '' }}</td>
<td>{{ item.quantite * item.prix | devise '' }}</td>
,..
<td><strong>{{ total | devise '' }}</strong></td>

On voit également l'ajout des filtres entier et flottant pour la quantité et le prix :

 
Sélectionnez
<td><input type="text" class="form-control" v-model="input.quantite | entier 10" placeholder="Quantité"></td>
<td><input type="text" class="form-control" v-model="input.prix | flottant" placeholder="Prix"></td>

Les filtres sont définis dans la partie JavaScript :

 
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.
filters: {
  devise: function(valeur, symbole) {
    return valeur + ' ' + symbole;
  },
  entier: {
    read: function(valeur) {
      return valeur;
    },
    write: function(nouvelleValeur, ancienneValeur, max) {
      var valeur = parseInt(nouvelleValeur);
      if(valeur % 1 === 0) {
        return valeur > max ? ancienneValeur : valeur;
      }
      return 0;
    }
  },
  flottant: {
    read: function(valeur) {
      return valeur;
    },
    write: function(nouvelleValeur, ancienneValeur) {
      return isNaN(nouvelleValeur) ? ancienneValeur : nouvelleValeur;
    }
  }       
}

On voit qu'on peut ajouter un argument (ou même plusieurs) à la partie « write » d'un filtre bidirectionnel.

Le fonctionnement est le même que celui que nous avons vu précédemment pour ce panier. La différence introduite par le filtre devise est que maintenant le symbole monétaire se situe après la valeur :

Image non disponible

D'autre part dans le formulaire on ne peut entrer que des valeurs entières inférieures à 10 dans la zone « quantité » avec le filtre entier, et que des nombres (entiers ou flottants) dans la zone « prix » à l'aide du filtre flottant.

I-G. En résumé

  • Vue.js est équipé de nombreux filtres prédéfinis.
  • Vue.js permet de créer des filtres personnalisés.
  • On peut ajouter des arguments à un filtre personnalisé.
  • Un argument de filtre peut être dynamique.
  • Un filtre peut être bidirectionnel.

II. Créer une directive

Nous avons vu les directives dans la première partie de cette série de tutoriels. Il s'agissait alors des directives prédéfinies dans vue.js. Dans le présent chapitre, nous allons voir comment créer une directive sur mesure selon ses besoins pour étendre les possibilités de vue.js.

II-A. Les directives prédéfinies

Vue.js est déjà bien équipé en directives prédéfinies :

  • v-text : modifie le contenu texte ;
  • v-html : modifie le contenu HTML ;
  • v-if : fait apparaître ou disparaître un élément selon une valeur booléenne ;
  • v-show : permet de masquer ou faire apparaître un élément ;
  • v-else : à utiliser avec v-if et v-show ;
  • v-on : permet de mettre en place un événement ;
  • v-bind : permet de créer une liaison ;
  • v-model : permet de lier la valeur d'un contrôle de formulaire ;
  • v-for : crée un objet vue.js pour chaque élément se trouvant dans un tableau ou un objet ;
  • v-transition : applique une transition ;
  • v-ref : crée une référence d'un composant enfant pour son parent ;
  • v-el : crée une référence d'un élément du DOM.

Ces directives sont nombreuses et bien pratiques. Nous en avons rencontré plusieurs lors de la première partie de ce cours.

II-B. Une directive personnalisée

Si on sort des cas d'utilisation des directives prédéfinies alors c'est le moment d'en créer une sur mesure. Par exemple nous voulons créer une directive pour afficher un titre avec des règles de style spécifiques. Sur le fond, l'exemple est un peu stupide parce qu'il suffit de créer une classe CSS pour aboutir au même résultat, mais ça va nous permettre de commencer avec la création de directives de façon simple :

 
Sélectionnez
<div id="tuto">
  <div v-titre="Mon titre"></div>
</div>

Je définis une directive v-titre et je donne le texte du titre de façon littérale.

Voici le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
new Vue({
  el: '#tuto',
  directives: {
    titre: {
      bind: function() {
        this.el.style.color = 'Yellow';
        this.el.style.backgroundColor = 'DarkBlue';
        this.el.style.textAlign = 'center';
        this.el.style.fontSize = 'x-large';
        this.el.style.padding = '20px';
        this.el.innerHTML = this.expression;
      }
    }
  }
});

On voit apparaître une nouvelle propriété pour notre objet vue.js : directives. C'est ici que nous définissons les directives propres à cet objet.

On commence par nommer la directive (titre) et ensuite on définit son fonctionnement.

La méthode bind a pour objet de relier la directive à l'élément du DOM, elle est appelée une seule fois au démarrage (de la même manière on peut utiliser unbind qui est appelée une seule fois lorsque la liaison avec le DOM est supprimée). On a deux propriétés de la directive intéressantes :

  • el : pour connaître l'élément du DOM lié à la directive, on peut alors manipuler cet élément à notre guise, ici je fixe des règles de style et le contenu ;
  • expression : le contenu de l'expression.

Si on regarde de plus près les propriétés, on trouve :

Image non disponible

Le résultat visuel est :

Image non disponible

II-C. Un argument

Une directive peut utiliser un argument. Voici un exemple :

 
Sélectionnez
<div id="tuto">
  <div v-titre:1="titre"></div>
</div>

Ici la directive v-titre a l'argument 1 et la valeur titre. Voici le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
new Vue({
  el: '#tuto',
  data: {
    titre: 'Mon titre'
  },
  directives: {
    titre: function(value) {
      this.el.innerHTML = 
        '<h' + this.arg + '>' + 
        value + 
        '</h' + this.arg + '>';
    }
  }
});

On voit qu'il y a juste une fonction pour la directive qui correspond en fait à update. Comme c'est la seule utilisée, on n'a pas besoin de préciser qu'il s'agit d'elle.

Une directive peut avoir ces trois fonctions toutes optionnelles :

  • bind : on l'a déjà vue ci-dessus, elle est appelée une seule fois lors de la liaison ;
  • update : appelée juste après bind et à chaque fois que les données liées changent ;
  • unbind : appelée une seule fois lors de la rupture de la liaison.

Voilà un petit point sur les propriétés :

Image non disponible

Le résultat est la valeur de « titre » dans une balise h1 :

Image non disponible

Si on utilise :

 
Sélectionnez
<div v-titre:2="titre"></div>

Alors on aura une balise h2 et ainsi de suite.

On se rend compte qu'une directive possède un certain nombre de propriétés bien pratiques. Faisons un peu le point :

  • el : il s'agit de l'élément du DOM en liaison avec la directive (dans notre cas « div ») ;
  • expression : l'expression de la liaison en excluant l'argument ou un éventuel filtre (dans notre cas c'est « titre ») ;
  • arg : l'argument (dans notre cas c'est « 1 »).

On a d'autres propriétés moins utiles :

  • raw : c'est la totalité de ce qui est transmis (dans notre cas « titre » ) ;
  • name : c'est le nom de la directive (dans notre cas « titre »).

II-D. Plusieurs valeurs

Des fois, on a besoin de plusieurs valeurs. Voyons un exemple de réalisation :

 
Sélectionnez
<div id="tuto">
  <div v-titre="{type : 1, texte : titre}"></div>
</div>

On transmet un littéral objet. Voici le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
new Vue({
  el: '#tuto',
  data: {
    titre: 'Mon titre'
  },
  directives: {
    titre: function(value) {
      this.el.innerHTML = 
        '<h' + value.type + '>' + 
        value.texte + 
        '</h' + value.type + '>';
    }
  }
});

Voici l'état des propriétés :

Image non disponible

On obtient à nouveau le texte dans une balise h1 :

Image non disponible

Dans ce cas, on récupère directement les valeurs à partir de l'expression. On pourrait aussi transmettre les valeurs séparément, mais alors la directive serait appelée plusieurs fois.

II-E. Une directive élément

Une autre possibilité intéressante est de pouvoir créer une directive pour avoir une nouvelle balise personnalisée (à la manière de « restrict : 'E' » de AngularJS). Voici un exemple :

 
Sélectionnez
<div id="tuto">
  <titre>Mon titre</titre>
</div>

La balise <titre> n'existe pas dans le HTML, on la crée avec ce code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
new Vue({
  el: '#tuto',
  elementDirectives: {
    titre: {
      bind: function() {
        this.el.innerHTML = '<h1>' + this.el.innerHTML + '</h1>';
        this.el.style.color = 'Blue';
      }
    }
  }
});

La propriété ne s'appelle plus « directives », mais « elementDirectives ». On peut juste utiliser le bind pour définir la balise. Ici je me suis contenté de mettre le texte dans une balise h1 et de changer la couleur :

Image non disponible

On dispose en fait de peu de possibilités :

Image non disponible

En fait, on peut seulement accéder à l'élément.

II-F. Le panier

Reprenons l'exemple du panier en utilisant une directive personnalisée. Voici le nouveau code de la page :

 
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.6/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" id="tuto">
      <br>
 
      <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 in panier">
              <td>{{ item.article | capitalize }}</td>
              <td>{{ item.quantite }}</td> 
              <td>{{ item.prix | devise '' }}</td>
              <td>{{ item.quantite * item.prix | devise '' }}</td>
              <td><button class="btn btn-info btn-block" v-on:click="modifier($index)"><i class="fa fa-edit fa-lg"></button></td>
              <td><button class="btn btn-danger btn-block" v-on:click="supprimer($index)"><i class="fa fa-trash-o fa-lg"></i></button></td>
            </tr> 
            <tr>
              <td colspan="3"></td>
              <td><strong>{{ total | devise '' }}</strong></td>
              <td colspan="2"></td>
            </tr> 
            <tr>
              <td><input type="text" class="form-control" v-model="input.article | capitalize" v-el:modif placeholder="Article"></td>
              <td><input type="text" class="form-control" v-model="input.quantite | entier 10" placeholder="Quantité"></td>
              <td><input type="text" class="form-control" v-model="input.prix | flottant" placeholder="Prix"></td>
              <td colspan="3"><btn_ajouter></btn_ajouter></td>
            </tr>
          </tbody>       
        </table>
      </div> 
  
    </div>
 
    <script src="http://cdn.jsdelivr.net/vue/1.0.10/vue.min.js"></script>
 
    <script>
 
      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 }
          ],
          input: { article: '', quantite: 0, prix: 0 }
        },
        computed: {
          total: function () {
            var total = 0;
            this.panier.forEach(function(el) {
              total += el.prix * el.quantite;
            });
            return total; 
          }
        },
        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.$$.modif.focus();
          },
          supprimer: function(index) {
            this.panier.splice(index, 1);
          },
        },
        filters: {
          devise: function(valeur, symbole) {
            return valeur + ' ' + symbole;
          },
          entier: {
            read: function(valeur) {
              return valeur;
            },
            write: function(nouvelleValeur, ancienneValeur, max) {
              var valeur = parseInt(nouvelleValeur);
              if(valeur % 1 === 0) {
                return valeur > max ? ancienneValeur : valeur;
              }
              return 0;
            }
          },
          flottant: {
            read: function(valeur) {
              return valeur;
            },
            write: function(nouvelleValeur, ancienneValeur) {
              return isNaN(nouvelleValeur) ? ancienneValeur : nouvelleValeur;
            }
          }       
        },
        elementDirectives: {
          btn_ajouter: {
            bind: function() {
              this.el.innerHTML = '<button class="btn btn-primary btn-block">Ajouter</button>';
              this.el.addEventListener('click', function(e) {
                this.vm.ajouter();
              }.bind(this));
            }
          }
        }
      });
 
    </script>
 
  </body>
 
</html>

Vous pouvez voir dans le HTML une nouvelle balise :

 
Sélectionnez
<td colspan="3">
    <btn_ajouter></btn_ajouter>
</td>

C'est une directive « élément » qui est définie dans cette partie du code JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
elementDirectives: {
  btn_ajouter: {
    bind: function() {
      this.el.innerHTML = '<button class="btn btn-primary btn-block">Ajouter</button>';
      this.el.addEventListener('click', function(e) {
        this.vm.ajouter();
      }.bind(this));
    }
  }
}

On définit le contenu HTML et on ajoute l'événement pour le clic sur le bouton en appelant dans ce cas la méthode ajouter.

II-G. En résumé

  • Vue.js est équipé de nombreuses directives prédéfinies.
  • On peut créer une directive personnalisée.
  • On peut transmettre un littéral dynamique dans une directive.
  • On peut utiliser un ou plusieurs arguments dans une directive.
  • On peut créer une directive « élément ».

III. Créer un composant

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 ce chapitre 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.

III-A. Mon premier composant

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

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

La méthode component crée 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 :

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

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

 
Sélectionnez
<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 le précédent chapitre avec une directive élément. Mais on va voir que les composants vont bien plus loin !

Une petite illustration du fonctionnement :

Image non disponible

J'ai déclaré le composant de façon globale, mais on pourrait aussi le créer propre à la VueModèle avec la propriété components :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
new Vue({
  el: '#tuto',
  components: {
    'mon-composant': {
      template: '<p>Mon premier composant !</p>'
    }
  }
});

La méthode component est un raccourci qui étend la bibliothèque et enregistre le composant. La syntaxe développée est :

 
Sélectionnez
1.
2.
3.
4.
5.
var MonComposant = Vue.extend({
  template: '<p>Mon premier composant !</p>'
});
 
Vue.component('mon-composant', MonComposant);

III-B. Passage de données

III-B-1. Passage d'une valeur

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 :

 
Sélectionnez
1.
2.
3.
4.
Vue.component('nom', {
  props: ['nom'],
  template: '<p>Mon nom est {{nom}}</p>'
});

Les props sont des champs 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 :

 
Sélectionnez
<div id="tuto">
  <nom nom="Toto"></nom>
</div>

Ce qui donne finalement :

Mon nom est Toto

Voici une schématisation du fonctionnement :

Image non disponible

III-B-2. camelCase

Les attributs HTML ne sont pas sensibles à la casse, 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 :

 
Sélectionnez
1.
2.
3.
4.
Vue.component('nom', {
  props: ['monNom'],
  template: '<p>Mon nom est {{monNom}}</p>'
});

Et vous utilisez ce HTML :

 
Sélectionnez
<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) :

 
Sélectionnez
<nom mon-nom="Toto"></nom>

Vous obtenez bien alors :

Mon nom est Toto

III-B-3. 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
<div id="tuto">
  <input v-model="nomSaisi">
  <br>
  <nom v-bind:nom="nomSaisi"></nom>
</div>

On prévoit l'argument nom pour le composant avec la directive v-bind, une propriété liée nomSaisi pour la VueModèle.

Avec ce JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
Vue.component('nom', {
  props: ['nom'],
  template: '<p>Mon nom est <strong>{{nom}}</strong></p>'
});
 
new Vue({
  el: '#tuto'
});

Avec ce résultat :

Image non disponible

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

Notez la possibilité d'avoir une syntaxe raccourcie :

 
Sélectionnez
<nom :nom="nomSaisi"></nom>

III-C. 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 :

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

Et le JavaScript :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
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 :

Image non disponible

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
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"},
    ]
  }
});

Notez aussi dans ce cas la possibilité de la syntaxe raccourcie :

 
Sélectionnez
<liste v-for="personne in personnes" :personne="personne"></liste>

III-D. Un tableau

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

 
Sélectionnez
<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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
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 :

Image non disponible

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

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

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

III-E. 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
<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 :

 
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('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 :

Image non disponible

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.

III-F. 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 un attribut un peu particulier de la balise <script> qui permet de définir non pas un script, mais un template. Voici le nouveau HTML intégrant le template :

 
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.
<script type="text/x-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>
</script>
 
<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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
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 :

Image non disponible

Cette fois le code est vraiment propre !

III-G. Le panier revisité

Comme exemple du précédent chapitre 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 de tutoriels.

Voici le code complet de la page :

 
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.
136.
137.
138.
139.
140.
<!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>
 
      <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 in panier">
                <td>{{ item.article | capitalize }}</td>
                <td>{{ item.quantite }}</td> 
                <td>{{ item.prix | devise '' }}</td>
                <td>{{ item.quantite * item.prix | devise '' }}</td>
                <td><button class="btn btn-info btn-block" v-on:click="modifier($index)"><i class="fa fa-edit fa-lg"></button></td>
                <td><button class="btn btn-danger btn-block" v-on:click="supprimer($index)"><i class="fa fa-trash-o fa-lg"></i></button></td>
              </tr> 
              <tr>
                <td colspan="3"></td>
                <td><strong>{{ total | devise '' }}</strong></td>
                <td colspan="2"></td>
              </tr> 
              <tr>
                <td><input type="text" class="form-control" v-model="input.article | capitalize" v-el:modif placeholder="Article"></td>
                <td><input type="text" class="form-control" v-model="input.quantite | entier 10" placeholder="Quantité"></td>
                <td><input type="text" class="form-control" v-model="input.prix | flottant" placeholder="Prix"></td>
                <td colspan="3"><button class="btn btn-primary btn-block" v-on:click="ajouter()">Ajouter</button></td>
              </tr>
            </tbody>       
          </table>
        </div> 
      </script>
 
      <div id="tuto">
        <panier :panier="panier"></panier>
      </div>
 
    </div>
 
    <script src="http://cdn.jsdelivr.net/vue/1.0.10/vue.min.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; 
          }
        },
        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.$$.modif.focus();
          },
          supprimer: function(index) {
            this.panier.splice(index, 1);
          },
        },
        filters: {
          devise: function(valeur, symbole) {
            return valeur + ' ' + symbole;
          },
          entier: {
            read: function(valeur) {
              return valeur;
            },
            write: function(nouvelleValeur, ancienneValeur, max) {
              var valeur = parseInt(nouvelleValeur);
              if(valeur % 1 === 0) {
                return valeur > max ? ancienneValeur : valeur;
              }
              return 0;
            }
          },
          flottant: {
            read: function(valeur) {
              return valeur;
            },
            write: function(nouvelleValeur, ancienneValeur) {
              return isNaN(nouvelleValeur) ? ancienneValeur : nouvelleValeur;
            }
          }       
        }
      });
 
      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 :

Image non disponible

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

Vous avez sans doute remarqué que la propriété data du composant comporte une fonction. En effet chaque instance du composant aura ses propres données, il est donc nécessaire de les isoler.

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

 
Sélectionnez
1.
2.
3.
4.
<div id="tuto">
  <panier :panier="panier"></panier>
  <pre>{{$data | json}}</pre>
</div>

Le filtre json permet d'afficher correctement les données :

Image non disponible

Ce chapitre est loin d'épuiser les possibilités des composants, mais vous avez à présent de très bonnes bases. Je vous invite à consulter la documentation et cet excellent exemple pour compléter vos connaissances.

III-H. 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.

IV. Les extensions

La bibliothèque la mieux dotée nécessite forcément des extensions et Vue.js n'échappe pas à la règle. On a déjà vu qu'il est possible de créer des composants. Vue.js permet l'écriture de plugins et il commence à y en avoir quelques-uns de disponibles. Dans ce chapitre, on va utiliser le plugin vue-resource qui permet de faciliter la communication avec un serveur.

IV-A. Un plugin ?

Un plugin (on dit aussi un greffon parfois pour rester Français) est une source complémentaire qui étend les fonctionnalités globales d'une bibliothèque. La nécessité d'utiliser un plugin pour vue.js peut se détailler en :

  • la nécessité de disposer de méthodes globales ;
  • la nécessité de disposer de filtres, directives…
  • la nécessité de disposer de nouvelles méthodes pour les VueModèles.

Le but du présent chapitre n'est pas de créer un plugin, mais juste d'en utiliser un.

IV-B. Le plugin vue-resource

On trouve ce plugin sur Github :

Image non disponible

Ce qui va essentiellement nous intéresser ce sont ces fichiers JavaScript :

Image non disponible

De façon classique on dispose d'une version lisible et commentée et d'une autre version minifiée pour alléger le chargement. Si vous voulez effectuer du débogage dans le code, utilisez la première version, sinon adoptez directement la seconde.

Il suffit de télécharger le fichier, de le disposer dans un dossier, et de le référencer correctement dans la page HTML, par exemple :

 
Sélectionnez
<script src="js/vue-resource.min.js"></script>

On peut également utiliser des outils pour rassembler les ressources dans un fichier unique si vous voulez optimiser le chargement.

Une autre possibilité est d'utiliser un CDN :

 
Sélectionnez
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.7.0/vue-resource.min.js"></script>

Ce plugin offre deux services :

1. Un simple service HTTP avec ces méthodes :

  • get(url, [data], [success], [options]),
  • post(url, [data], [success], [options]),
  • put(url, [data], [success], [options]),
  • patch(url, [data], [success], [options]),
  • delete(url, [data], [success], [options]),
  • jsonp(url, [data], [success], [options]) ;

2. Un service de ressource avec une seule méthode pour créer toutes les possibilités :

  • resource(url, [params], [actions], [options]).

Ce qui a pour effet de générer ces méthodes :

  • get: {method: 'GET'} ;
  • save: {method: 'POST'} ;
  • query: {method: 'GET'} ;
  • update: {method: 'PUT'} ;
  • remove: {method: 'DELETE'} ;
  • delete: {method: 'DELETE'}.

On a donc tout ce qui est nécessaire pour dialoguer facilement avec un serveur. Dans ce chapitre, on va utiliser le second service.

IV-C. Une API

IV-C-1. Un serveur de développement

Pour nos tests on a besoin d'un serveur qui réponde aux requêtes en offrant les services d'une API.

Nos besoins vont être une API répondant à L'URL « /noms » avec ces trois routes :

  • GET pour récupérer tous les noms ;
  • POST pour ajouter un nom ;
  • DELETE pour supprimer un nom avec l'identifiant en paramètre.

Pour ne pas trop charger le code, je ne prévois pas la modification du nom. Le but de ce chapitre n'étant pas consacré aux scripts côté serveur, on va se contenter de mettre en place quelque chose de simple. Si vous savez déjà comment réaliser cela, par exemple en utilisant Lumen, vous pouvez sauter cette partie (arrangez-vous toutefois pour adapter les données transmises au code JavaScript utilisé).

Il vous faut un serveur local genre WAMP ou autre. PHP dispose aussi maintenant en interne d'un serveur simplifié parfaitement adapté au développement.

Créez un dossier sur votre serveur pour les tests pour ce cours. Par exemple, j'ai personnellement créé www/vuejs/serveur pour créer les codes de ce chapitre.

IV-C-2. Composer

Composer est un gestionnaire de dépendances pour PHP. On lui dit quels composants on désire et lui se fait un plaisir de les installer pour nous. En plus il met en place un chargement automatique des classes concernées. Et pour clôturer la chose, il assure aussi les mises à jour !

Si vous ne disposez pas de Composer sur votre machine, c'est le moment de l'installer. Le plus simple est de le placer globalement, référez-vous à ces instructions d'installation qui dépendent de votre système d'exploitation. Si vous ne désirez pas cette installation globale, vous pouvez tout simplement utiliser le fichier phar. Il suffit de mettre ce fichier dans votre dossier sur le serveur !

IV-C-2-a. On crée notre application côté serveur

Vous avez à présent tout ce qu'il faut pour créer l'application côté serveur. Ouvrez votre console et positionnez-vous dans le dossier que vous avez créé sur le serveur et entrez cette commande pour installer un routeur :

 
Sélectionnez
composer require league/route

Si vous utilisez le fichier phar, il faudra adapter la syntaxe :

 
Sélectionnez
php composer.phar require league/route

Vous devriez obtenir quelque chose dans ce genre :

Image non disponible

Vous allez trouver plein de choses dans votre dossier :

Image non disponible

Je ne rentre pas dans le détail de tout ça, mais maintenant on dispose d'un routeur simple et rapide largement suffisant pour nos tests.

Ce package dispose d'une documentation complète.

Il faut aussi ajouter un fichier .htaccess pour simplifier nos URL :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews
    </IfModule>
 
    RewriteEngine On
 
    # Redirect Trailing Slashes...
    RewriteRule ^(.*)/$ /$1 [L,R=301]
 
    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

Comme on va utiliser un fichier index.php pour récupérer toutes les URL on explique à Apache qu'il doit tout envoyer sur lui. On va d'ailleurs créer ce fichier :

 
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.
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use League\Route\RouteCollection;
  
// Autoload
require 'vendor/autoload.php';
  
// Création du routeur
$router = new RouteCollection;
  
// Création routes
$router->addRoute('GET', '/noms', function (Request $request, Response $response) {
 
    $content = json_encode(file('noms.txt', FILE_IGNORE_NEW_LINES));
    $response->setContent($content);
    $response->setStatusCode(200);
    return $response;
  
});
$router->addRoute('POST', '/noms', function (Request $request, Response $response) {
 
    $data = json_decode($request->getContent(), true);
    file_put_contents('noms.txt', $data['nom'] . "\n", FILE_APPEND);
    $response->setStatusCode(200);
    return $response;
  
});
$router->addRoute('DELETE', '/noms', function (Request $request, Response $response) {
  
    $index = $_GET['id'];
    $arr = file('noms.txt');
    unset($arr[$index]);
    $fp = fopen('noms.txt', 'w+');
    foreach($arr as $line) { 
        fwrite($fp,$line);
    };
    fclose($fp);
    $response->setStatusCode(200);
    return $response;
  
});
  
// On traite la requête
$dispatcher = $router->getDispatcher();
$request = Request::createFromGlobals();
$response = $dispatcher->dispatch($request->getMethod(), $request->getPathInfo());
  
// On renvoie la réponse
$response->send();

J'ai prévu un code simplifié que je ne détaillerai pas parce que ce n'est pas l'objet de ce cours.

Il ne nous manque plus qu'un petit fichier texte pour mémoriser les noms (ça sera plus simple que de mettre en œuvre une base de données ). Prévoyez quelques noms au départ :

 
Sélectionnez
Dupont
Durand

Nommez ce fichier noms.txt et positionnez-le aussi dans le serveur.

Finalement, vous devez avoir ceci :

Image non disponible

Maintenant tout est prêt côté serveur, on va pouvoir revenir à Vue.js côté client…

IV-D. L'application

Voici le code complet de la page :

 
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.
<!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.6/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>
 
      <script type="text/x-template" id="panneau-template">
        <div class="alert alert-success" v-show="success">
          <button type="button" class="close" v-on:click="closeSuccess()">
            <span>×</span>
          </button>
          Serveur mis à jour avec succès ! 
        </div>
        <div class="alert alert-danger" v-show="danger">
          <button type="button" class="close" v-on:click="closeDanger()">
            <span>×</span>
          </button>
          Echec de la communication avec le serveur ! 
        </div>
        <div class="panel panel-primary">
          <div class="panel-heading">Liste des noms</div>        
          <table class="table table-bordered table-striped">
            <thead>
              <tr>
               <th class="col-sm-10">Nom</th>
               <th class="col-sm-2"></th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="nom in noms">
                <td>{{ nom }}</td>
                <td><button class="btn btn-warning btn-block" v-on:click="supprimer($index)">Supprimer</button></td>  
              </tr>  
              <tr>
                <td><input type="text" class="form-control" v-model="inputNom" placeholder="Nom"></td>
                <td colspan="1"><button class="btn btn-primary btn-block" v-on:click="ajouter()">Ajouter</button></td>
              </tr>
            </tbody>       
          </table>
        </div>  
      </script>
 
      <div id="tuto"></div>
 
    </div>
 
    <script src="http://cdn.jsdelivr.net/vue/1.0.10/vue.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.1.17/vue-resource.min.js"></script>
 
    <script>
 
      var resource = null;
 
      new Vue({
        el: '#tuto',
        data: {
          noms: [],
          inputNom: '',
          success: false,
          danger: false
        },
        template: '#panneau-template',
        ready: function() {
          resource = this.$resource('serveur/noms');
          resource.get(function (data) {
            this.noms = data;
          }).error(function () {
            this.danger = true;
          })
        },
        methods: {
          ajouter: function() {
            resource.save({nom: this.inputNom}, function () {
              this.success = true;
              this.noms.push(this.inputNom);
              this.inputNom = '';
            }).error(function () {
              this.danger = true;
            })
          },
          supprimer: function(index) {
            resource.delete({id: index}, function () {
              this.success = true;
              this.noms.splice(index, 1);
            }).error(function () {
              this.danger = true;
            })
          },
          closeSuccess: function() {
            this.success = false;
          },
          closeDanger: function() {
            this.danger = false;
          }
        }
      });
 
    </script>
 
  </body>
 
</html>

Pour que l'application fonctionne, il faut bien référencer l'URL du serveur au niveau de cette ligne :

 
Sélectionnez
resource = this.$resource('serveur/noms');

Vous devez adapter l'URL selon la configuration de vos dossiers. Ici j'ai mis le serveur dans un dossier enfant « serveur ».

Au chargement, si tout se passe bien on obtient :

Image non disponible

On retrouve les deux noms qu'on a entrés dans le fichier noms.txt dans le serveur (ou ailleurs dans une base de données si vous avez adopté une autre approche pour votre API).

IV-D-1. Ajouter un nom

On peut ajouter un nom avec la zone de texte prévue, ce nom vient se positionner dans la liste et un message d'information apparaît :

Image non disponible

On peut faire disparaître la barre d'information en cliquant sur la petite croix.

Si on regarde dans le fichier noms.txt on trouve bien le nom en fin de liste :

 
Sélectionnez
Dupont
Durand
Carlito

IV-D-2. Supprimer un nom

De la même manière, on peut supprimer un nom :

Image non disponible

On a à nouveau la barre d'information et dans le fichier du serveur on a bien la disparition du nom :

 
Sélectionnez
Dupont
Carlito

Notre application fonctionne correctement . Il ne nous reste plus qu'à voir tout ça de plus près…

IV-E. Le template

J'ai prévu de référencer le template :

 
Sélectionnez
template: '#panneau-template',

C'est plus propre. On retrouve donc ce template dans le flux du HTML, mais il est géré uniquement par JavaScript :

 
Sélectionnez
<script type="text/x-template" id="panneau-template">
  ... Le template ici
</script>

La partie qui concerne la gestion l'affichage des noms correspond à ce que nous avons déjà vu dans ce cours avec des directives v-for, v-model et v-on :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
<div class="panel panel-primary">
  <div class="panel-heading">Liste des noms</div>        
  <table class="table table-bordered table-striped">
    <thead>
      <tr>
       <th class="col-sm-10">Nom</th>
       <th class="col-sm-2"></th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="nom in noms">
        <td>{{ nom }}</td>
        <td><button class="btn btn-warning btn-block" v-on:click="supprimer($index)">Supprimer</button></td>  
      </tr>  
      <tr>
        <td><input type="text" class="form-control" v-model="inputNom" placeholder="Nom"></td>
        <td colspan="1"><button class="btn btn-primary btn-block" v-on:click="ajouter()">Ajouter</button></td>
      </tr>
    </tbody>       
  </table>
</div>

Pour les barres d'information, on a du code classique de Bootstrap :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
<div class="alert alert-success" v-show="success">
  <button type="button" class="close" v-on:click="closeSuccess()">
    <span>×</span>
  </button>
  Serveur mis à jour avec succès ! 
</div>
<div class="alert alert-danger" v-show="danger">
  <button type="button" class="close" v-on:click="closeDanger()">
    <span>×</span>
  </button>
  Echec de la communication avec le serveur ! 
</div>

Par contre, on n'utilise pas la bibliothèque JavaScript de Bootstrap pour le bouton de fermeture puisqu'on dispose déjà de vue.js qui nous offre des possibilités plus larges. C'est pour cette raison que j'ai prévu la directive v-on pour appeler une méthode de la VueModèle pour gérer la visibilité des barres.

IV-F. La ressource

Pour mettre en place le service de ressource et demander au serveur la liste de noms, on a ce code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
ready: function() {
  resource = this.$resource('serveur/noms');
  resource.get(function (data) {
    this.noms = data;
  }).error(function () {
    this.danger = true;
  })
},

Nous n'avions pas encore rencontré la fonction ready. Il faut un certain temps pour que la VueModèle soit créée et le template compilé. Avant de communiquer avec le serveur, on va attendre d'être sûr que tout cela est réalisé. Lorsque c'est le cas, vue.js appelle la fonction ready si elle existe. Vous avez toutes les informations concernant le cycle de vie d'une VueModèle dans la documentation.

La première chose qu'on fait est de créer la ressource :

 
Sélectionnez
resource = this.$resource('serveur/noms');

On utilise ensuite la méthode get pour créer la requête pour l'API :

 
Sélectionnez
1.
2.
3.
4.
5.
resource.get(function (data) {
  this.noms = data;
}).error(function () {
  this.danger = true;
})

On dispose ensuite de blocs de code en cas de réussite ou d'échec. À ce stade une requête avec le verbe GET est envoyé à l'API avec l'URL « …/noms ». L'API lit le fichier des noms et renvoie les noms :

 
Sélectionnez
["Dupont","Durand"]

Il suffit de renseigner la propriété noms de la VueModèle pour avoir l'affichage des noms :

 
Sélectionnez
this.noms = data;

En cas d'échec, on met la propriété danger à true :

 
Sélectionnez
this.danger = true;

Dans le template est prévue une directive v-show avec cette propriété :

 
Sélectionnez
<div class="alert alert-danger" v-show="danger">

Ce qui permet de gérer simplement son affichage :

Image non disponible

Pour cacher la barre, il est fait appel à la méthode closeDanger :

 
Sélectionnez
closeDanger: function() {
  this.danger = false;
}

IV-G. Ajouter un nom

On a prévu un bouton dans le template pour ajouter un nom :

 
Sélectionnez
1.
2.
3.
4.
5.
<td colspan="1">
    <button class="btn btn-primary btn-block" v-on:click="ajouter()">
        Ajouter
    </button>
</td>

Un clic sur le bouton appelle la méthode ajouter :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
ajouter: function() {
  resource.save({nom: this.inputNom}, function () {
    this.success = true;
    this.noms.push(this.inputNom);
    this.inputNom = '';
  }).error(function () {
    this.danger = true;
  })
},

Cette fois on utilise la méthode save de la ressource. Le premier paramètre sert à transmettre l'information, en l'occurrence le nom ajouté.

Cette fois, on a un verbe POST avec comme contenu de requête cette information JSON :

 
Sélectionnez
{"nom":"Tartine"}

L'API stocke le nom et se contente de renvoyer un statut 200 pour dire que tout s'est bien passé. On met alors la liste et la zone de saisie à jour et la barre d'information de réussite est affichée.

En cas d'échec, on affiche la barre d'erreur comme on l'a vu précédemment.

IV-H. Supprimer un nom

Pour chaque nom on dispose d'un bouton de suppression :

 
Sélectionnez
1.
2.
3.
4.
5.
<td>
    <button class="btn btn-warning btn-block" v-on:click="supprimer($index)">
        Supprimer
    </button>
</td>

On appelle la méthode supprimer en précisant l'index du nom :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
supprimer: function(index) {
  resource.delete({id: index}, function () {
    this.success = true;
    this.noms.splice(index, 1);
  }).error(function () {
    this.danger = true;
  })
},

On transmet cette fois l'identifiant du nom avec une requête DELETE de cette forme : « …/noms?id=2 ». L'API supprime le nom de la liste et renvoie un statut 200. On affiche la barre de réussite et on supprime le nom de la liste.

En cas d'erreur, c'est exactement comme pour le cas de l'ajout d'un nom.

Notre application est évidemment très simple et pourrait être améliorée et complétée sur bien des aspects, mais elle vous donne de bonnes bases pour l'utilisation de ce plugin.

IV-I. En résumé

  • Vue.js dispose de quelques plugins et devrait élargir rapidement sa panoplie.
  • Le plugin vue-resource permet très simplement d'établir une communication avec un serveur.
  • il est facile de mettre en place une application cliente qui fait appel à une API.

V. Remerciements

Nous remercions Maurice Chavelli de nous avoir autorisés à publier ce tutoriel.

Nous tenons également à remercier Winjerome pour la mise au gabarit et Claude Leloup pour la correction orthographique.

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.