VII. Les composants (2/2)▲
J'ai commencé à présenter les composants dans le précédent chapitre. On a vu qu'ils constituent un aspect important de Vue.js et qu'il sont faciles à créer. On a également vu qu'ils sont organisés hiérarchiquement et qu'on peut passer des informations (props) du parent à l'enfant.
On va continuer notre exploration des composants dans le présent chapitre en voyant la communication inverse : de l'enfant vers le parent. On va en profiter pour évoquer aussi quelques autres éléments.
VII-A. La communication entre composants▲
Les composants étant isolés, il faut prévoir une procédure pour qu'ils échangent des informations. On a vu les props dans le précédent article, c'est la communication descendante, du parent vers l'enfant. De façon symétrique on aura des événements pour la communication ascendante : de l'enfant vers le parent :
VII-A-1. Compléments sur les props▲
Pour les props il faut tenir compte d'une contrainte : si la valeur change au niveau du parent ce changement est répercuté à l'enfant, mais l'inverse n'est pas vrai ! En effet on pourrait avoir des comportements douteux si un enfant pouvait modifier des valeurs chez un parent…
Donc il ne faut pas modifier la valeur d'un prop dans un composant !
Si on a besoin de changer la valeur d'un prop dans un enfant il faut donc d'abord créer une propriété locale ou calculée. On verra ça dans l'exemple en fin d'article.
Il est aussi possible d'effectuer une validation des props pour être sûr de recevoir des données correctes. On peut obliger une valeur à être présente, prévoir une valeur par défaut, un traitement personnalisé… Cette possibilité n'est judicieuse que si vous avez l'intention de créer un composant public. On a ces types disponibles :
- String ;
- Number ;
- Boolean ;
- Function ;
- Object ;
- Array.
Voici un résumé qui s'inspire de l'exemple de la documentation :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
Vue.component
(
'mon_composant'
,
{
props
:
{
prop1
:
Number,
// type unique
prop2
:
[
Object,
Array],
// types multiples
prop3
:
{
type
:
Boolean,
required
:
true // valeur booléenne requise
},
prop4
:
{
type
:
String,
default:
'chat'
// une valeur par défaut
},
prop5
:
{
type
:
Object,
default:
function (
) {
return {
animal
:
'chat'
}
// une fonction pour construire la valeur par défaut
}
},
prop6
:
{
validator
:
function (
value) {
return value <
4
// une validation personnalisée
}
}
}
}
)
VII-B. Les événements▲
Un composant peut :
- émettre un événement : $emit(nom de l'événement) ;
- écouter un événement : $on(nom de l'événement) ou v-on dans le template pour un parent.
On va considérer un petit exemple pour illustrer ça : on a un nombre auquel on peut ajouter ou retirer des chiffres. On va pour cela utiliser deux boutons : un pour ajouter un chiffre, et un autre pour enlever un chiffre. On va créer un composant bouton pour réaliser ça. Voici le code complet de l'exemple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
<!
DOCTYPE html
>
<
html lang
=
"
fr
"
>
<
head>
<
meta charset
=
"
utf-8
"
>
<
meta http-equiv
=
"
X-UA-Compatible
"
content
=
"
IE=edge
"
>
<
meta name
=
"
viewport
"
content
=
"
width=device-width, initial-scale=1
"
>
<
title>
Test vue.js<
/title
>
<
link rel
=
"
stylesheet
"
href
=
"
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css
"
>
<
/head
>
<
body>
<
div class
=
"
container
"
>
<
br>
<
div id
=
"
tuto
"
>
<
bouton titre
=
"
Ajoute chiffre
"
type
=
'
ajoute
'
@ajoute
=
"
ajoute
"
></bouton
>
<
bouton titre
=
"
Enlève chiffre
"
type
=
'
enleve
'
@enleve
=
"
enleve
"
></bouton
>
<
h1>
{{
nombre }}
<
/h1
>
<
/div
>
<
/div
>
<script src
=
"
https://unpkg.com/vue@2.0.3/dist/vue.js
"
></
script>
<script>
Vue.component
(
'
bouton
'
,
{
props
:
[
'
titre
'
,
'
type
'
],
template
:
'
<a class="btn btn-info" href="#" @click="action">
{{
titre }}
</a>
'
,
methods
:
{
action
:
function
(
) {
this
.
$emit
(
this
.
type);
}
}
}
)
new
Vue
({
el
:
'
#tuto
'
,
data
:
{
nombre
:
'
0
'
},
methods
:
{
ajoute
:
function
(
) {
this
.
nombre +=
Math
.floor
(
Math
.random
(
) *
10
);
},
enleve
:
function
(
) {
this
.
nombre =
this
.
nombre.slice
(
0
,
-
1
);
}
}
}
);
</
script>
<
/body
>
<
/html
>
Au départ on a cet aspect :
On ajoute ou retire un chiffre en cliquant sur le bouton correspondant :
Bon ! mon exemple ne sert à rien, je l'avoue, sauf à illustrer le sujet de ce tutoriel !
VII-B-1. Le composant▲
Voici le code du composant bouton :
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 :
VII-B-2. 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 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 :
Vous voyez qu'il est simple de faire communiquer ainsi des composants hiérarchiques.
VII-C. Contrôle de saisie et événement▲
On peut aussi imaginer avoir un composant avec un contrôle de saisie, par exemple une zone de texte en liaison avec un composant parent. Par exemple, on peut compléter l'exemple ci-dessus en ajoutant un composant avec une zone de texte reliée à la valeur du nombre. Voilà l'exemple modifié en conséquence :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
<!
DOCTYPE html
>
<
html lang
=
"
fr
"
>
<
head>
<
meta charset
=
"
utf-8
"
>
<
meta http-equiv
=
"
X-UA-Compatible
"
content
=
"
IE=edge
"
>
<
meta name
=
"
viewport
"
content
=
"
width=device-width, initial-scale=1
"
>
<
title>
Test vue.js<
/title
>
<
link rel
=
"
stylesheet
"
href
=
"
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css
"
>
<
/head
>
<
body>
<
div class
=
"
container
"
>
<
br>
<
div id
=
"
tuto
"
>
<
bouton titre
=
"
Ajoute chiffre
"
type
=
'
ajoute
'
@ajoute
=
"
ajoute
"
></bouton
>
<
bouton titre
=
"
Enlève chiffre
"
type
=
'
enleve
'
@enleve
=
"
enleve
"
></bouton
>
<
saisie v-model
=
"
nombre
"
></saisie
>
<
h1>
{{
nombre }}
<
/h1
>
<
/div
>
<
/div
>
<script src
=
"
https://unpkg.com/vue@2.0.3/dist/vue.js
"
></
script>
<script>
Vue.component
(
'
bouton
'
,
{
props
:
[
'
titre
'
,
'
type
'
],
template
:
'
<a class="btn btn-info" href="#" @click="action">
{{
titre }}
</a>
'
,
methods
:
{
action
:
function
(
) {
this
.
$emit
(
this
.
type);
}
}
}
)
Vue.component
(
'
saisie
'
,
{
template
:
'
<input @input="onInput">
'
,
methods
:
{
onInput
:
function
(
event
) {
this
.
$emit
(
'
input
'
,
event
.
target.
value);
}
}
}
)
new
Vue
({
el
:
'
#tuto
'
,
data
:
{
nombre
:
'
0
'
},
methods
:
{
ajoute
:
function
(
) {
this
.
nombre +=
Math
.floor
(
Math
.random
(
) *
10
);
},
enleve
:
function
(
) {
this
.
nombre =
this
.
nombre.slice
(
0
,
-
1
);
}
}
}
);
</
script>
<
/body
>
<
/html
>
Voici le composant pour la zone de saisie :
On intercepte l'événement input, et avec la méthode onInput on émet l'événement input à destination du parent avec la valeur actuelle.
Il suffit d'insérer le composant en prévoyant la liaison de données :
Maintenant la zone de saisie reflète la valeur du nombre et permet de le modifier :
On peut se demander quel est l'intérêt de procéder ainsi ; dans la documentation est évoquée la possibilité des types personnalisés de zones de saisie…
Il y aurait encore des choses à dire sur les composants, mais vous avez déjà acquis l'essentiel. Pour le reste je vous renvoie à la documentation.
VII-D. Le panier revisité▲
Pour terminer ce tutoriel on va reprendre l'exemple du panier, en ajoutant un composant pour l'édition et l'ajout des articles. Voilà le nouveau code :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
<!
DOCTYPE html
>
<
html lang
=
"
fr
"
>
<
head>
<
meta charset
=
"
utf-8
"
>
<
meta http-equiv
=
"
X-UA-Compatible
"
content
=
"
IE=edge
"
>
<
meta name
=
"
viewport
"
content
=
"
width=device-width, initial-scale=1
"
>
<
title>
Test vue.js<
/title
>
<
link rel
=
"
stylesheet
"
href
=
"
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css
"
>
<
link rel
=
"
stylesheet
"
href
=
"
https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css
"
>
<
link rel
=
"
stylesheet
"
href
=
"
https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/united/bootstrap.min.css
"
>
<
/head
>
<
body>
<
div class
=
"
container
"
>
<
br>
<script type
=
"
text/x-template
"
id
=
"
panier-template
"
>
<div class
=
"
panel panel-primary
"
>
<div class
=
"
panel-heading
"
>Panier</div>
<table class
=
"
table table-bordered table-striped
"
>
<thead>
<tr>
<th class
=
"
col-sm-4
"
>Article</th>
<th class
=
"
col-sm-2
"
>Quantité</th>
<th class
=
"
col-sm-2
"
>Prix</th>
<th class
=
"
col-sm-2
"
>Total</th>
<th class
=
"
col-sm-1
"
></th>
<th class
=
"
col-sm-1
"
></th>
</tr>
</thead>
<tbody>
<tr v-for
=
"
(item, index) in panier
"
>
<td>{{
item.
article }}
</td>
<td>{{
item.
quantite }}
</td>
<td>{{
item.
prix }}
€</td>
<td>{{
(item.
quantite *
item.
prix).
toFixed(2
) }}
€</td>
<td><button class
=
"
btn btn-info btn-block
"
@click
=
"
modifier(index)
"
><i class
=
"
fa fa-edit fa-lg
"
></i></button></td>
<td><button class
=
"
btn btn-danger btn-block
"
@click
=
"
supprimer(index)
"
><i class
=
"
fa fa-trash-o fa-lg
"
></i></button></td>
</tr>
<tr>
<td colspan
=
"
3
"
></td>
<td><strong>{{
total }}
€</strong></td>
<td colspan
=
"
2
"
></td>
</tr>
<editeur :article
=
"
article
"
@add
=
"
ajouter
"
></editeur>
</tbody>
</table>
</div>
</script>
<script type
=
"
text/x-template
"
id
=
"
editeur-template
"
>
<tr>
<td><input type
=
"
text
"
class
=
"
form-control
"
v-model
=
"
input.article
"
ref
=
"
modif
"
placeholder
=
"
Article
"
></td>
<td><input type
=
"
text
"
class
=
"
form-control
"
v-model
=
"
input.quantite
"
placeholder
=
"
Quantité
"
></td>
<td><input type
=
"
text
"
class
=
"
form-control
"
v-model
=
"
input.prix
"
placeholder
=
"
Prix
"
></td>
<td colspan
=
"
3
"
><button class
=
"
btn btn-primary btn-block
"
@click
=
"
ajouter()
"
>Ajouter</button></td>
</tr>
</script>
<
div id
=
"
tuto
"
>
<
panier :panier
=
"
panier
"
></panier
>
<
/div
>
<
/div
>
<script src
=
"
https://unpkg.com/vue@2.0.3/dist/vue.js
"
></
script>
<script>
Vue.component
(
'
panier
'
,
{
props
:
[
'
panier
'
],
template
:
'
#panier-template
'
,
data
:
function
(
) {
return
{
article
:
{
article
:
''
,
quantite
:
0
,
prix
:
0
}
}
},
computed
:
{
total
:
function
(
) {
var
total =
0
;
this
.
panier.forEach
(
function
(
el) {
total +=
el.
prix *
el.
quantite;
}
);
return
total.toFixed
(
2
);
}
},
methods
:
{
modifier
:
function
(
index) {
this
.
article =
this
.
panier[
index];
this
.
panier.splice
(
index,
1
);
},
supprimer
:
function
(
index) {
this
.
panier.splice
(
index,
1
);
},
ajouter
:
function
(
input) {
this
.
panier.push
(
input);
this
.
article =
{
article
:
''
,
quantite
:
0
,
prix
:
0
};
}
},
components
:
{
'
editeur
'
:
{
props
:
[
'
article
'
],
template
:
'
#editeur-template
'
,
computed
:
{
input
:
function
(
) {
return
this
.
article;
}
},
methods
:
{
ajouter
:
function
(
) {
this
.
$emit
(
'
add
'
,
this
.
input);
}
}
}
}
}
);
new
Vue
({
el
:
'
#tuto
'
,
data
:
{
panier
:
[
{
article
:
"
Cahier
"
,
quantite
:
2
,
prix
:
'
5.30
'
},
{
article
:
"
Crayon
"
,
quantite
:
4
,
prix
:
'
1.10
'
},
{
article
:
"
Gomme
"
,
quantite
:
1
,
prix
:
'
3.25
'
}
],
}
}
);
</
script>
<
/body
>
<
/html
>
L'aspect est le même, j'ai juste changé le thème pour varier un peu l'apparence :
On a toujours le composant panier :
Et dans celui-ci, on a le composant editeur :
Le composant editeur est déclaré dans le composant panier :
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.
Le composant editeur est ainsi intégré dans le template du panier :
Pour le reste on retrouve ce qu'on a précédemment mis en œuvre.
Pour la déclaration des templates, j'ai changé de système parce que le fait d'avoir deux balises template sur la même page ne semble pas fonctionner correctement. Je suis donc passé par une balise script de type text/x-template qui ne pose aucun souci.
VII-E. En résumé▲
- Un composant enfant ne doit pas modifier la valeur d'un prop.
- On peut valider le type d'un prop.
- Un composant enfant envoie des informations à son parent par des événements.