Tutoriel pour apprendre à utiliser le framework Vue.js version 2

Plus loin


précédentsommairesuivant

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 :

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

Image non disponible

Avec ce code :

 
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.
<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) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
<script>
import Message from './Message.vue'
 
export default {
  ...
  components: {
    Message
  }
}
</script>

Et au niveau du template :

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

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

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

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

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

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

 
Sélectionnez
changePage(index) {
  this.pagination.index = index
},

On a cet aspect par exemple avec la page 2 :

Image non disponible

Toujours dans le template dans la boucle pour afficher les utilisateurs :

 
Sélectionnez
<tr v-for="(user, index) in userShowed">

On utilise la propriété calculée userShowed :

 
Sélectionnez
1.
2.
3.
4.
userShowed: function() {
  let start = this.getStartPagination()
  return this.users.slice(start, start + this.pagination.number)
},

On utilise la méthode getStartPagination pour définir l'index de départ de la page :

 
Sélectionnez
getStartPagination() {
  return (this.pagination.index - 1) * this.pagination.number
}

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
edit: function(index) {
  ...
  this.save.index = index + this.getStartPagination()   (1)
  this.user = this.users[this.save.index]
  ...
  this.users.splice(this.save.index, 1)
  ...
},

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
del: function(index) {
  let that = this
  index = index + this.getStartPagination()
  ...
  }).then(function() {
    that.resource.delete({id: that.users[index].id}).then((response) => {
      ...       
},

Au passage je vous rappelle que vous disposez d'un superbe outil de développement dans Chrome :

Image non disponible

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.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2017 Vue.js. 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.