VI. Les composants (1/2)▲
Vous connaissez peut-être les Web Components. C'est un nouveau standard qui permet d'enrichir le HTML de façon modulaire. En gros on peut créer une entité qui utilise des éléments HTML et des fonctionnalités propres, le tout étant 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'infobulles, 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.
Pouvoir créer des composants est sans doute un des aspects les plus intéressants de Vue.js, surtout lorsqu'on se lance dans une application ambitieuse. Le code est ainsi bien encapsulé et réutilisable. Un composant est ainsi un élément isolé, autonome, qui possède des propriétés et des comportements et qu'on peut utiliser directement dans le HTML…
On va commencer à voir les composants dans ce chapitre. Je développerai le sujet dans un prochain tutoriel.
VI-A. Mon premier composant▲
Alors on se lance et on crée un petit composant. Au niveau du JavaScript c'est tout simple :
Vue.component
(
'mon-composant'
,
{
template
:
'<p>Mon premier composant !</p>'
}
);
La méthode component crée (on dit qu'on l'enregistre) le composant dont on précise le nom (mon-composant) et le template. C'est vraiment le minimum pour un composant ! Il faut aussi initialiser la VueModèle comme d'habitude :
new Vue
({
el
:
'#tuto'
}
);
Ensuite on utilise la balise personnalisée dans le HTML :
<
div id
=
"
tuto
"
>
<
mon-composant></mon-composant
>
<
/div
>
Avec ce résultat :
Mon premier composant !
Bon, pour le moment c'est très simple, et on a vu qu'on pouvait réaliser cela dans 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 :
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 :
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 :
2.
3.
4.
5.
var MonComposant =
Vue.extend
({
template
:
'<p>Mon premier composant !</p>'
}
);
Vue.component
(
'mon-composant'
,
MonComposant);
VI-B. Passage de données▲
VI-B-1. Les props▲
Créer un composant c'est bien, mais tout ce qui se trouve à l'intérieur est isolé du reste du monde. Autrement dit le composant n'a accès à aucune donnée en dehors des siennes. Il arrive souvent qu'on ait besoin de transmettre des informations pour renseigner le composant. Voyons comment réaliser cela. On va ajouter une propriété au composant :
2.
3.
4.
Vue.component
(
'nom'
,
{
props
:
[
'nom'
],
template
:
'<p>Mon nom est
{{
nom}}
</p>'
}
);
Les props sont des propriétés pour lesquels le composant attend des valeurs. C'est un tableau, ici on a juste prévu la clé nom. Il ne reste plus qu'à renseigner cette clé dans le HTML :
Ce qui donne finalement :
Mon nom est Toto
Voici une schématisation du fonctionnement :
Lorsqu'on crée un composant il devient un enfant du composant dans lequel on le crée. Ici le parent est l'instance principale.
VI-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 :
2.
3.
4.
Vue.component
(
'nom'
,
{
props
:
[
'monNom'
],
template
:
'<p>Mon nom est
{{
monNom}}
</p>'
}
);
Et vous utilisez ce HTML :
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) :
Vous obtenez bien alors :
Mon nom est Toto
VI-B-3. Attribut dynamique▲
Un attribut statique c'est bien, mais s'il est 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 :
On prévoit l'argument nom pour le composant avec la directive v-bind (avec sa syntaxe simplifiée : ), une propriété liée nomSaisi pour la VueModèle.
Avec ce JavaScript :
2.
3.
4.
5.
6.
7.
8.
9.
Vue.component
(
'nom'
,
{
props
:
[
'nom'
],
template
:
'<p>Mon nom est <strong>
{{
nom}}
</strong></p>'
}
);
new Vue
({
el
:
'#tuto'
,
data
:
{
nomSaisi
:
''
}
}
);
Avec ce résultat :
On retrouve en fait les possibilités qu'on avait déjà rencontrées, mais adaptées à un composant.
VI-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 la réaliser. Est-ce que ça peut fonctionner avec un composant ? Voici un exemple :
<
div id
=
"
tuto
"
>
<
liste v-for
=
"
personne in personnes
"
:personne
=
"
personne
"
></liste
>
<
/div
>
Et le JavaScript :
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 :
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 :
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"
},
]
}
}
);
VI-D. Un tableau▲
Allons un peu plus loin et créons un composant pour générer un tableau. On veut ce HTML :
Donc on veut un composant tableau auquel on transmet les données à afficher. On va conserver les données vues précédemment. Voici le JavaScript :
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 :
On peut répercuter tout changement dans les données au niveau du tableau. Par exemple, si vous ajoutez ce code :
setTimeout
(
function(
) {
Vue.set
(
vm.
personnes,
1
,
{
nom
:
"Claret"
,
prenom
:
"Marcel"
}
);
},
2000
);
Au bout de deux secondes vous allez voir le tableau changer pour cette ligne.
VI-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 :
Ici on utilise un composant tableau en lui transmettant les colonnes et les lignes de données.
Voici le JavaScript :
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"
],
]
}
}
);
Ce qui donne ce résultat :
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.
VI-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 préférable de pouvoir définir ce template avec une mise en page du code harmonieuse.
On peut inclure le template dans le HTML avec la nouvelle balise template qui est maintenant bien prise en charge par les navigateurs. Voici le nouveau HTML intégrant le template :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
<
template id
=
"
tableau-template
"
>
<
table class
=
"
table table-bordered
"
>
<
thead>
<
tr>
<
th v-for
=
"
value in colonnes
"
>
{{
value }}
<
/th
>
<
/tr
>
<
/thead
>
<
tbody>
<
tr v-for
=
"
ligne in lignes
"
>
<
td v-for
=
"
value in ligne
"
>
{{
value }}
<
/td
>
<
/tr
>
<
/tbody
>
<
/table
>
<
/template
>
<
div id
=
"
tuto
"
>
<
tableau
:colonnes
=
"
colonnes
"
:lignes
=
"
personnes
"
>
<
/tableau
>
<
/div
>
Remarquez qu'on a un identifiant (tableau-template) qui va permettre de référencer ce template dans le composant :
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 :
Cette fois le code est vraiment propre.
VI-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 ce tutoriel.
Voici le code complet de la page :
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.
<!
DOCTYPE html
>
<
html lang
=
"
fr
"
>
<
head>
<
meta charset
=
"
utf-8
"
>
<
meta http-equiv
=
"
X-UA-Compatible
"
content
=
"
IE=edge
"
>
<
meta name
=
"
viewport
"
content
=
"
width=device-width, initial-scale=1
"
>
<
title>
Test vue.js<
/title
>
<
link rel
=
"
stylesheet
"
href
=
"
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css
"
>
<
link rel
=
"
stylesheet
"
href
=
"
https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css
"
>
<
/head
>
<
body>
<
div class
=
"
container
"
>
<
br>
<
template id
=
"
panier-template
"
>
<
div class
=
"
panel panel-primary
"
>
<
div class
=
"
panel-heading
"
>
Panier<
/div
>
<
table class
=
"
table table-bordered table-striped
"
>
<
thead>
<
tr>
<
th class
=
"
col-sm-4
"
>
Article<
/th
>
<
th class
=
"
col-sm-2
"
>
Quantité<
/th
>
<
th class
=
"
col-sm-2
"
>
Prix<
/th
>
<
th class
=
"
col-sm-2
"
>
Total<
/th
>
<
th class
=
"
col-sm-1
"
></th
>
<
th class
=
"
col-sm-1
"
></th
>
<
/tr
>
<
/thead
>
<
tbody>
<
tr v-for
=
"
(item, index) in panier
"
>
<
td>
{{
item.
article }}
<
/td
>
<
td>
{{
item.
quantite }}
<
/td
>
<
td>
{{
item.
prix }}
€<
/td
>
<
td>
{{
(item.
quantite *
item.
prix).
toFixed(2
) }}
€<
/td
>
<
td><button class
=
"
btn btn-info btn-block
"
@click
=
"
modifier(index)
"
><i class
=
"
fa fa-edit fa-lg
"
></i
></button
></td
>
<
td><button class
=
"
btn btn-danger btn-block
"
@click
=
"
supprimer(index)
"
><i class
=
"
fa fa-trash-o fa-lg
"
></i
></button
></td
>
<
/tr
>
<
tr>
<
td colspan
=
"
3
"
></td
>
<
td><strong>
{{
total }}
€<
/strong
></td
>
<
td colspan
=
"
2
"
></td
>
<
/tr
>
<
tr>
<
td><input type
=
"
text
"
class
=
"
form-control
"
v-model
=
"
input.article
"
ref
=
"
modif
"
placeholder
=
"
Article
"
></td
>
<
td><input type
=
"
text
"
class
=
"
form-control
"
v-model
=
"
input.quantite
"
placeholder
=
"
Quantité
"
></td
>
<
td><input type
=
"
text
"
class
=
"
form-control
"
v-model
=
"
input.prix
"
placeholder
=
"
Prix
"
></td
>
<
td colspan
=
"
3
"
><button class
=
"
btn btn-primary btn-block
"
v-on:click
=
"
ajouter()
"
>
Ajouter<
/button
></td
>
<
/tr
>
<
/tbody
>
<
/table
>
<
/div
>
<
/template
>
<
div id
=
"
tuto
"
>
<
panier :panier
=
"
panier
"
></panier
>
<
/div
>
<
/div
>
<script src
=
"
https://unpkg.com/vue@2.0.3/dist/vue.js
"
></
script>
<script>
Vue.component
(
'
panier
'
,
{
props
:
[
'
panier
'
],
template
:
'
#panier-template
'
,
data
:
function
(
) {
return
{
input
:
{
article
:
''
,
quantite
:
0
,
prix
:
0
}
}
},
computed
:
{
total
:
function
(
) {
var
total =
0
;
this
.
panier.forEach
(
function
(
el) {
total +=
el.
prix *
el.
quantite;
}
);
return
total.toFixed
(
2
);
}
},
methods
:
{
ajouter
:
function
(
) {
this
.
panier.push
(
this
.
input);
this
.
input =
{
article
:
''
,
quantite
:
0
,
prix
:
0
};
},
modifier
:
function
(
index) {
this
.
input =
this
.
panier[
index];
this
.
panier.splice
(
index,
1
);
this
.
$refs.
modif.focus
(
);
},
supprimer
:
function
(
index) {
this
.
panier.splice
(
index,
1
);
},
}
}
);
new
Vue
({
el
:
'
#tuto
'
,
data
:
{
panier
:
[
{
article
:
"
Cahier
"
,
quantite
:
2
,
prix
:
'
5.30
'
},
{
article
:
"
Crayon
"
,
quantite
:
4
,
prix
:
'
1.10
'
},
{
article
:
"
Gomme
"
,
quantite
:
1
,
prix
:
'
3.25
'
}
],
}
}
);
</
script>
<
/body
>
<
/html
>
Cela donne évidemment les mêmes rendu et fonctionnement que lors du chapitre précédent :
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 :
Ça permet d'afficher correctement les données :
Ce tutoriel 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.
VI-H. Vue devtools▲
Pour déboguer les applications Vue.js, il existe un plugin pour Chrome. Lorsque vous l'avez installé vous disposez d'un nouveau bouton dans la fenêtre des outils de développement :
Vous pouvez ainsi inspecter vos composants, voilà ce que ça donne pour le panier :
C'est un outil qui devient très intéressant lorsque vous avez de nombreux composants !
VI-I. En résumé▲
- Vue.js permet la création de composants pour enrichir le HTML.
- Un composant peut utiliser toutes les options existantes (mis à part el).
- Il est possible de passer des données à un composant.
- Un composant peut hériter des données de son parent.
- On peut manipuler des listes avec un composant.
- On peut ajouter un outil de débogage pour Vue.js à Chrome.