Vue.js2 : vue-resource (2/2)

Dans le précédent article j’ai montré comment utiliser le plugin vue-resource pour générer facilement des requêtes Ajax avec une apllication de gestion d’utilisateurs. Dans le présent article 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 au précédent article.

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…

Un composant pour les messages

Dans le template tel qu’on l’a construit on a 3 messages différents :

<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 struture 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 quatres propriétés. On va l’appeller Message :

img64

Avec ce code :

<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 4 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 articles.

Il nous faut intégrer ce composant dans le composant parent (App) :

<script>
import Message from './Message.vue'

export default {
  ...
  components: {
    Message
  }
}
</script>

Et au niveau du template :

<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 !

La pagination

Comme notre application charge l’ensemble des utilisateur 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 :

<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 modification apportées…

Au niveau du data :

data () {
  return {
    ...
    save: { index: 0, user: {} },
    ...
    pagination: { index: 1, number: 4 }
  }
},

On a une propriété save avec la sauvarde 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).

Le template

Dans le template on a ce code :

<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 c’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 :

img65

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 :

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 :

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

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 :

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 :

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 :

img66

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 cet article. 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.

Laisser un commentaire