IV. Vue-resource (2/2)▲
Dans le précédent chapitrechapitreVue-resource (1/2), j'ai montré comment utiliser le plugin vue-resource pour générer facilement des requêtes Ajax avec une application de gestion d'utilisateurs. Dans le présent chapitre, on va améliorer cette application en prévoyant d'une part une pagination simplifiée, d'autre part un composant spécifique pour les messages.
On va prendre l'application telle qu'on l'a laissée précédemment.
Pour vous faciliter la vie et si vous n'avez pas le courage de suivre tout le processus vous pouvez télécharger ici le code complet. Il suffit de l'installer…
IV-A. Un composant pour les messages▲
Dans le template tel qu'on l'a construit on a trois messages différents :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<div
class
=
"ui positive message"
v-show
=
"success"
>
<i
class
=
"close icon"
@
click
=
"closeSuccess()"
></i>
<div
class
=
"header"
>
Serveur mis à jour avec succès !
</div>
</div>
<div
class
=
"ui negative message"
v-show
=
"danger"
>
<i
class
=
"close icon"
@
click
=
"closeDanger()"
></i>
<div
class
=
"header"
>
Echec de la communication avec le serveur !
</div>
</div>
<div
class
=
"ui negative message"
v-show
=
"validation.name || validation.email"
>
<i
class
=
"close icon"
@
click
=
"closeValidation()"
></i>
<div
class
=
"header"
>
Il y a des erreurs dans la validation des données saisies :
</div>
<ul
class
=
"list"
>
<li>
{{
validation.name }}
</li>
<li>
{{
validation.email }}
</li>
</ul>
</div>
La structure HTML de chaque message est la même, ce qui diffère c'est :
- la classe pour l'aspect : positive ou negative ;
- le commutateur pour la directive v-show ;
- le texte du header ;
- la présence éventuelle d'une liste d'éléments.
On peut donc imaginer de construire un composant avec ces quatre propriétés. On va l'appeler Message :
Avec ce 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.
<template>
<div
class
=
"ui message"
:
class
=
"classType"
v-show
=
"show"
>
<i
class
=
"close icon"
@
click
=
"close()"
></i>
<div
class
=
"header"
>
{{
header }}
</div>
<ul
class
=
"list"
v-show
=
"showList"
>
<li
v-for
=
"element in list"
>
{{
element }}
</li>
</ul>
</div>
</template>
<script>
export default {
name
:
'message'
,
props
:
[
'type'
,
'header'
,
'show'
,
'list'
],
computed
:
{
classType
:
function(
) {
return {
positive
:
this.
type ==
'positive'
,
negative
:
this.
type ==
'negative'
}
},
showList
:
function(
) {
return this.
list !==
undefined
}
},
methods
:
{
close
:
function(
) {
this.
$emit
(
'close'
)
}
}
}
</script>
On a les quatre propriétés (props) :
- type : pour définir la classe ;
- header : pour le texte du header ;
- show : pour l'affichage ;
- list : pour la liste éventuelle.
D'autre part on émet un événement (close) à destination du parent si on clique sur le bouton de fermeture du message.
L'ensemble du code correspond à des choses qu'on a vues lors des précédents chapitres.
Il nous faut intégrer ce composant dans le composant parent (App) :
Et au niveau du template :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<message
type
=
"positive"
header
=
"Serveur mis à jour avec succès !"
:
show
=
"success"
@
close
=
"closeSuccess"
>
</message>
<message
type
=
"negative"
header
=
"Echec de la communication avec le serveur !"
:
show
=
"danger"
@
close
=
"closeDanger"
>
</message>
<message
type
=
"negative"
header
=
"Il y a des erreurs dans la validation des données saisies :"
:
list
=
"[ validation.name, validation.email ]"
:
show
=
"validation.name != '' || validation.email != ''"
@
close
=
"closeValidation"
>
</message>
On obtient ainsi un code plus lisible et ce composant est réutilisable pour une autre application.
Remarque : on a souvent le choix entre utiliser une propriété calculée (computed) et charger un peu le code au niveau du template, c'est selon ses goûts !
IV-B. La pagination▲
Comme notre application charge l'ensemble des utilisateurs dès son lancement, on va ajouter une pagination. On pourrait adopter une autre stratégie et utiliser une requête pour chaque page, ce qui serait judicieux s'il y avait énormément d'utilisateurs et que le chargement complet dès le départ ne soit pas réaliste. Sur le fond ça ne changerait pas grand-chose au codage…
Pour la pagination on va devoir déterminer le nombre d'utilisateurs par page, on va aussi devoir gérer un index pour savoir où on en est.
Voici le nouveau code complet du composant :
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.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
<template>
<div
class
=
"ui raised container segment"
>
<message
type
=
"positive"
header
=
"Serveur mis à jour avec succès !"
:
show
=
"success"
@
close
=
"closeSuccess"
>
</message>
<message
type
=
"negative"
header
=
"Echec de la communication avec le serveur !"
:
show
=
"danger"
@
close
=
"closeDanger"
>
</message>
<message
type
=
"negative"
header
=
"Il y a des erreurs dans la validation des données saisies :"
:
list
=
"[ validation.name, validation.email ]"
:
show
=
"validation.name != '' || validation.email != ''"
@
close
=
"closeValidation"
>
</message>
<table
class
=
"ui celled table"
>
<caption><h1>
Liste des utilisateurs</h1></caption>
<thead>
<tr>
<th>
Nom</th>
<th>
Email</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr
v-for
=
"(user, index) in userShowed"
>
<td>
{{
user.name }}
</td>
<td>
{{
user.email }}
</td>
<td>
<button
class
=
"fluid ui orange button"
:
class
=
"{ disabled: edition }"
data-tooltip
=
"Modifier cet utilisateur"
data-position
=
"top center"
@
click
=
"edit(index)"
>
<i
class
=
"edit icon"
></i>
</button>
</td>
<td>
<button
class
=
"fluid ui red button"
:
class
=
"{ disabled: edition }"
data-tooltip
=
"Supprimer cet utilisateur"
data-position
=
"top center"
@
click
=
"del(index)"
>
<i
class
=
"remove user icon"
></i>
</button>
</td>
</tr>
<tr
class
=
"ui form"
>
<td>
<div
class
=
"ui field"
:
class
=
"{ error: validation.name }"
>
<input
type
=
"text"
v-model
=
"user.name"
placeholder
=
"Nom"
>
</div>
</td>
<td>
<div
class
=
"ui field"
:
class
=
"{ error: validation.email }"
>
<input
type
=
"email"
class
=
"form-control"
v-model
=
"user.email"
placeholder
=
"Email"
>
</div>
</td>
<td
colspan
=
"2"
v-if
=
"!edition"
>
<button
class
=
"fluid ui blue button"
data-tooltip
=
"Ajouter un utilisateur"
data-position
=
"top center"
@
click
=
"add()"
>
<i
class
=
"add user icon"
></i>
</button>
</td>
<td
v-if
=
"edition"
>
<button
class
=
"fluid ui blue button"
data-tooltip
=
"Mettre à jour cet utilisateur"
data-position
=
"top center"
@
click
=
"update()"
>
<i
class
=
"add user icon"
></i>
</button>
</td>
<td
v-if
=
"edition"
>
<button
class
=
"fluid ui violet button"
data-tooltip
=
"Annuler la modification"
data-position
=
"top center"
@
click
=
"undo()"
>
<i
class
=
"undo icon"
></i>
</button>
</td>
</tr>
</tbody>
</table>
<div
class
=
"ui pagination menu"
v-if
=
"paginationEnabled"
>
<a
v-for
=
"n in pagesNumber"
class
=
"item"
:
class
=
"{ active: pagination.index == n }"
@
click
=
"changePage(n)"
>
{{
n }}
</a>
</div>
</div>
</template>
<script>
import Message from './Message.vue'
export default {
name
:
'application'
,
resource
:
null,
data
(
) {
return {
users
:
[],
user
:
{
name
:
''
,
email
:
''
},
save
:
{
index
:
0
,
user
:
{}
},
success
:
false,
danger
:
false,
edition
:
false,
validation
:
{
name
:
''
,
email
:
''
},
pagination
:
{
index
:
1
,
number
:
4
}
}
},
computed
:
{
userShowed
:
function(
) {
let start =
this.getStartPagination
(
)
return this.
users.slice
(
start,
start +
this.
pagination.
number)
},
pagesNumber
:
function(
) {
return Math.ceil
(
this.
users.
length /
this.
pagination.
number)
},
paginationEnabled
:
function(
) {
return this.
users.
length >
this.
pagination.
number &&
!
this.
edition
}
},
mounted
:
function(
) {
this.
resource =
this.
$resource
(
'/users{/id}'
)
this.
resource.get
(
).then
((
response) =>
{
this.
users =
response.
body
}, (
response) =>
{
this.
danger =
true
}
)
},
methods
:
{
add
:
function(
) {
this.resetMessages
(
)
this.
resource.save
(
this.
user).then
((
response) =>
{
this.
success =
true
this.
users.push
(
this.
user)
this.
user =
{
name
:
''
,
email
:
''
}
}, (
response) =>
{
this.setValidation
(
response)
}
);
},
update
:
function(
) {
this.resetMessages
(
)
this.
resource.update
({
id
:
this.
user.
id},
this.
user).then
((
response) =>
{
this.
success =
true
this.
edition =
false
this.
users.splice
(
this.
save.
index,
0
,
this.
user)
this.
user =
{
name
:
''
,
email
:
''
}
}, (
response) =>
{
this.setValidation
(
response)
}
);
},
del
:
function(
index) {
let that =
this
index =
index +
this.getStartPagination
(
)
this.resetMessages
(
)
this.
$swal
({
title
:
'Vous êtes sûr de vous ?'
,
text
:
"Il n'y aura aucun retour en arrière possible !"
,
type
:
'warning'
,
showCancelButton
:
true,
confirmButtonColor
:
'#3085d6'
,
cancelButtonColor
:
'#d33'
,
confirmButtonText
:
'Oui supprimer !'
,
cancelButtonText
:
'Non, surtout pas !'
,
}
).then
(
function(
) {
that.
resource.delete
({
id
:
that.
users[
index].
id}
).then
((
response) =>
{
that.
success =
true
that.
users.splice
(
index,
1
)
}, (
response) =>
{
that.
danger =
true
}
);
}
).done
(
)
},
edit
:
function(
index) {
this.resetMessages
(
)
this.
save.
index =
index +
this.getStartPagination
(
)
this.
user =
this.
users[
this.
save.
index]
this.
save.
user =
JSON.parse
(
JSON.stringify
(
this.
user))
this.
users.splice
(
this.
save.
index,
1
)
this.
edition =
true
},
undo
:
function(
) {
this.
users.splice
(
this.
save.
index,
0
,
this.
save.
user)
this.
user =
{
name
:
''
,
email
:
''
}
this.
edition =
false
},
resetMessages
:
function(
) {
this.
success =
false
this.
danger =
false
this.closeValidation
(
)
},
setValidation
:
function(
response) {
this.
validation.
name
=
response.
body.
name
?
response.
body.
name
[
0
]
:
''
this.
validation.
email =
response.
body.
email ?
response.
body.
email[
0
]
:
''
},
closeSuccess
:
function(
) {
this.
success =
false
},
closeDanger
:
function(
) {
this.
danger =
false
},
closeValidation
:
function(
) {
this.
validation =
{
name
:
''
,
email
:
''
}
},
changePage
(
index) {
this.
pagination.
index =
index
},
getStartPagination
(
) {
return (
this.
pagination.
index -
1
) *
this.
pagination.
number
}
},
components
:
{
Message
}
}
</script>
Voyons les modifications apportées.
Au niveau du data :
2.
3.
4.
5.
6.
7.
8.
data
(
) {
return {
...
save
:
{
index
:
0
,
user
:
{}
},
...
pagination
:
{
index
:
1
,
number
:
4
}
}
},
On a une propriété save avec la sauvegarde de l'index de l'utilisateur. Dans la précédente version, on se contentait de rajouter un utilisateur modifié en fin de tableau. Avec la pagination on va être plus précis et remettre l'utilisateur exactement à la même place pour qu'il reste sur la même page.
On a une propriété pagination avec l'index de la page en cours (index) et le nombre d'utilisateurs par page (number).
IV-B-1. Le template▲
Dans le template, on a ce code :
2.
3.
4.
5.
<div
class
=
"ui pagination menu"
v-if
=
"paginationEnabled"
>
<a
v-for
=
"n in pagesNumber"
class
=
"item"
:
class
=
"{ active: pagination.index == n }"
@
click
=
"changePage(n)"
>
{{
n }}
</a>
</div>
On fait apparaître (ou disparaître) avec v-if la pagination avec la propriété calculée paginationEnabled :
paginationEnabled
:
function(
) {
return this.
users.
length >
this.
pagination.
number &&
!
this.
edition
}
On a deux cas :
- le nombre d'utilisateurs dépasse le nombre par page ;
- on est en mode édition (en mode édition ce n'est pas vraiment le moment de changer de page !).
Le nombre de pages est donné par la propriété calculée pagesNumber :
pagesNumber
:
function(
) {
return Math.ceil
(
this.
users.
length /
this.
pagination.
number)
},
La page active est définie avec la classe active qui est en action si le numéro de la page (n) est égal à l'index de la pagination (pagination.index).
Enfin on installe une écoute de l'événement clic (@click) pour le changement de page avec la méthode changePage :
changePage
(
index) {
this.
pagination.
index =
index
},
On a cet aspect par exemple avec la page 2 :
Toujours dans le template dans la boucle pour afficher les utilisateurs :
<tr
v-for
=
"(user, index) in userShowed"
>
On utilise la propriété calculée userShowed :
On utilise la méthode getStartPagination pour définir l'index de départ de la page :
IV-B-2. Repérage de l'index de l'utilisateur▲
Avec la pagination on doit trouver l'index réel de l'utilisateur (et non pas celui dans la page) pour l'édition et la suppression.
Par exemple pour l'édition :
On mémorise l'index réel (1) en utilisant la méthode getStartPagination qu'on a déjà vue ci-dessus.
C'est la même chose quand on veut supprimer un utilisateur :
Au passage je vous rappelle que vous disposez d'un superbe outil de développement dans Chrome :
IV-C. Conclusion▲
On voit qu'il est facile de créer une pagination avec Vue.js. D'autre part il est judicieux de créer un composant enfant pour du code répétitif et/ou qu'on risque de réutiliser ailleurs.
On pourrait encore améliorer notre application en enrichissant la pagination (boutons avant/arrière, compression des pages si elles sont nombreuses…), en prévoyant une liste de sélection pour déterminer le nombre d'utilisateurs à afficher, ou encore prévoir un tri par colonne.
Si vous allez sur la page des ressources de Vue.js, vous allez trouver des composants tout prêts pour créer des tables plus élaborées que celle que je vous ai proposée dans ce chapitre. Par exemple le composant vue-smart-table semble vraiment intéressant, on trouve une démo ici. Il y a aussi vue-tables avec cette démo.