Vue.js

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

Dans les articles précédents on a vu l’essentiel de Vue.js mais cette librairie bénéficie de nombreuses ressources additonnelles. Vous en trouvez l’ensemble sur cette page avec en plus la références des tutoriels et autres. Dans cet article je vous propose de découvrir le plugin vue-resource qui simplifie la mise en place de requêtes Ajax.

Pour l’occasion on va utiliser Laravel pour d’une part fournir l’application et d’autre paert présenter une API. Comme sujet je ne vais pas faire dans l’originalité puisque je vais me contenter de gérer la table par défaut des utilisateurs.

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…

Installation de l’intendance

Laravel

On va donc avoir besoin d’une installation fraîche de Laravel. Comme je n’aime pas me compliquer la vie j’ai adopté Laragon et pour installer un Laravel c’est tout simple :

img39

Il suffit d’indiquer un nom et c’est parti !

img40Laragon ouvre la console, crée une base de données du même nom et lance l’installation, il n’y a plus qu’à attendre ! En bonus on bénéficie automatiquement d’un hôte virtuel vuejs.dev.

On va ensuite le franciser avec ce plugin en copiant le dossier du français :

img41

Et en mettant à jour la configuration (app/config/app.php) :

'locale' => 'fr',

Ca va nous servir pour les messages de la validation.

La base de données

Comme on va avoir besoin d’une base de données sans grande complexité on va utiliser sqlite.

Dans le fichier .env changez cette ligne :

DB_CONNECTION=sqlite

Et supprimez cette ligne :

DB_DATABASE=homestead

Ensuite dans la console créez le fichier :

touch database/database.sqlite

Vous devriez trouver le fichier ici :

img42

Pour les migrations supprimez celle-ci que nous n’utiliserons pas :

img43

On ne conserve donc que celle de la table users.

Vous pouvez lancer la migration :

php artisan migrate

On va aussi avoir besoin de remplir un peu cette table. Laravel offre des factories bien pratiques et celui des utilisateurs est déjà présent par défaut. On va donc l’utiliser pour créer 10 utilisateurs, on va modifier ce fichier :

img45

Avec ce code :

public function run()
{
    factory(App\User::class, 10)->create();;
}

Vous pouvez alors lancer la population :

php artisan db:seed

Si tout se passe bien vous devriez avoir 10 utilisateurs :

img46

Pour info j’utilise une extension de Firefox pour cette visualisation et d’une manière générale pour gérer simplement les bases sqlite.

Route et contrôleur

Il ne reste plus maintenant qu’à créer un contrôleur et les routes pour charger l’application et présenter l’API :

php artisan make:controller UserController

img47

Changez ainsi le code :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\User;
use Illuminate\Validation\Rule;

class UserController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return User::select('id', 'name', 'email')->get();
    }

    /**
     * Send app.
     *
     * @return \Illuminate\Http\Response
     */
    public function app()
    {
        return view('users');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $this->validate($request, [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
        ]);

        $input = array_merge($request->all(), ['password' => bcrypt('secret')]);
        
        User::create($input);
        
        return response()->json();       
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $this->validate($request, [
            'name' => 'required|max:255',
            'email' => ['required', 'email', 'max:255',
             Rule::unique('users')->ignore($id)],
        ]);

        User::find($id)->update($request->all());

        return response()->json(); 
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        User::find($id)->delete();

        return response()->json();
    }
}

Et ajoutez ces routes dans routes/web.php :

Route::get('app', 'UserController@app');

Route::resource('users', 'UserController', ['except' => [
    'create', 'show', 'edit'
]]);

Vous pouvez supprimer la route présente dans le fichier routes/web.php.

Je place les routes dans le fichier web parce qu’on va avoir besoin de la session.

Vous devriez avoir ces routes :

img48

En détaillant :

  • app : on va charger ici l’application
  • users (GET) : on charge tous les utilisateurs
  • users (POST) : on crée un utilisateur
  • users/{user} (PUT) : on modifie un utilisateur
  • users/{user} (DELETE) : on supprime un utilisateur

Je ne détaille pas le code du contrôleur qui est classique et ce n’est pas le sujet de cet article.

On ne va mettre en place aucune authentification pour simplifier.

npm

Par défaut on a ce fichier package.json :

{
  "private": true,
  "scripts": {
    "prod": "gulp --production",
    "dev": "gulp watch"
  },
  "devDependencies": {
    "bootstrap-sass": "^3.3.7",
    "gulp": "^3.9.1",
    "jquery": "^3.1.0",
    "laravel-elixir": "^6.0.0-9",
    "laravel-elixir-vue-2": "^0.2.0",
    "laravel-elixir-webpack-official": "^1.0.2",
    "lodash": "^4.16.2",
    "vue": "^2.0.1",
    "vue-resource": "^1.0.3"
  }
}

Comme on y trouve que des choses qui vont nous servir (en particulier il y a déjà vue-resource) on installe ça :

npm install

Si tout se passe bien vous obtenez toutes les dépendances dans ce dossier :

img49

Comme on va utiliser Sweet Alert on va installer aussi le plugin prévu pour Vue.js :

npm i vue-sweetalert --save-dev

L’application

Maintenant qu’on a mis en place toute l’intenance on va code l’application.

On va commencer par supprimer le composant prévu par défaut :

img50Et on va modifier ainsi le fichier resources/assets/je/app.js :

require('./bootstrap');

// Sweetalert
import VueSweetAlert from 'vue-sweetalert';
Vue.use(VueSweetAlert);

Vue.component('app', require('./components/App.vue'));

const app = new Vue({
    el: '#app'
});

J’ai ajouté Sweetalert et changé le nom du composant.

D’autre part on va enlever la compilation de sass dans gulpfile.js :

elixir(mix => {
    mix.webpack('app.js');
});

On va créer une vue resources/views/users.blade.php :

<!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://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css">
  </head>
  <body>
    <div class="container">
        <br>
        <div id="app">
          <app></app>
        </div>
    </div>
    <script>
      window.Laravel = {!! json_encode(['csrfToken' => csrf_token()]) !!};
    </script>
    <script src="/js/app.js"></script>
  </body>
</html>

Pour changer un peu je vais utiliser Semantic-ui que je trouve très élégant.

Le reste est classique mis à part la ligne pour le token qui est nécessaire pour l’intercepteur.

Il ne nous manque plus que le composant ! On l’appelle simplement :

img52

Avec ce code :

<template>
  <div class="ui raised container segment">
    <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>
    <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 users">
          <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>  
</template>

<script>
export default {
  name: 'application',
  resource: null,
  data () {
    return {
      users: [],
      user: { name: '', email: ''},
      userSave: {},
      success: false,
      danger: false,
      edition: false,
      validation: { name: '', email: ''}
    }
  },
  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.push(this.user)
        this.user = { name: '', email: '' }
      }, (response) => {
        this.setValidation(response)
      });
    },
    del: function(index) {
      let that = this
      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.user = this.users[index]
      this.userSave = JSON.parse(JSON.stringify(this.user))
      this.users.splice(index, 1)
      this.edition = true
    },
    undo: function() {
      this.users.push(this.userSave)
      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: ''}
    }
  }
}
</script>

On va détailler un peu tout ça mais on va déjà voir si ça fonctionne…

img53

On va ouvrir avec Chrome (on a vu dans un précédent article qu’on bénéficie d’un outil de développement spécial pour Vue.js) avec l’url vuejs.dev/app :

img54On obtient bien notre application avec les 10 utilisateurs.

Les données du composant

Regardons le data du composant :

data () {
  return {
    users: [],
    user: { name: '', email: ''},
    userSave: {},
    success: false,
    danger: false,
    edition: false,
    validation: { name: '', email: ''}
  }
},

On a :

  • users : tableau de tous  les utilisateurs, au départ vide
  • user : objet pour la liaison de données avec le formulaires
  • userSave : objet pour mémoriser les données de l’utilisateur en cas de modification (si on annule)
  • succes, danger, edition : des commutateurs pour l’état de l’application
  • validation : tableau pour les messages issus de la validation de Laravel

Le chargement de l’application

Lorsque l’application se charge elle passe par un certain nombre de stades avec des événements associés. On va utiliser l’événement mounted pour être sûr que le composant est prêt à afficher des informations :

mounted: function() {
  this.resource = this.$resource('/users{/id}')  (1)
  this.resource.get().then((response) => {   (2)
    this.users = response.body
  }, (response) => {
    this.danger = true
  })
},

On commence par créer et mémoriser la ressource (1). On va ainsi disposer de ces actions :

get: {method: 'GET'},
save: {method: 'POST'},
query: {method: 'GET'},
update: {method: 'PUT'},
remove: {method: 'DELETE'},
delete: {method: 'DELETE'}

Ensuite on envoie une requête get (2). Si ça aboutit on remplit le tableau users et les données s’affichent automatiquement :

<tr v-for="(user, index) in users">
  <td>{{ user.name }}</td>
  <td>{{ user.email }}</td>
  ...
</tr>

Au niveau de Laravel on a une simple requête Eloquent :

public function index()
{
    return User::select('id', 'name', 'email')->get();
}

Si ça n’aboutit pas on met à true la propriété danger et avec une directive v-show on affiche un message :

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

img55

Supprimer un utilisateur

On dispose de boutons pour supprimer un utilisateur :

img56Avec ce code :

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

On voit qu’on intercepte le clic avec la méthode del en transmettant l’index :

del: function(index) {
  let that = this
  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()
},

Pour faire plus joli j’ai prévu l’utilisation de Sweetalert pour avoir un fenêtre de confirmation élégante :

img57

Cette fois on utilise l’action delete de la ressource :

that.resource.delete({id: that.users[index].id}).then((response) => {
  that.success = true
  that.users.splice(index, 1)
}, (response) => {
  that.danger = true 
});

En cas de succès on met à true la propriété success pour faire apparaître un message :

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

img58

On supprime les informations du tableau de sutilisateur et il disparaît de la liste.

En cas d’échec c’est la même chose qu’on a vue ci-dessus.

Ajouter un utilisateur

Pour ajouter un utilisateur on dispose d’un formulaire au bas de la liste :

img59

Le code est un peu chargé parce que le même formulaire sert pour la modification :

<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">
    ...
  </td>
</tr>

On passe d’une situation à l’autre avec une directive v-if et la propriété edition.

On crée la liaison des données avec des directives v-model.

On prévoit une classe error en cas d’erreur de validation qui nous est donnée par l’objet validation.

Le bouton active la méthode add :

add: function() {
    this.resetMessages()   (1)
    this.resource.save(this.user).then((response) => {    (2)
    this.success = true   (3)
    this.users.push(this.user)   (4)
    this.user = { name: '', email: '' }   (5)
  }, (response) => {
    this.setValidation(response)   (6)
  });
},

On commence par supprimer les messages éventuels d’erreur de validation (1).

On utilise l’action save de la ressource (2).

En cas de réussite on affiche le message de réussite (3), on ajoute l’utilisateur dans le tableau (4), on efface les données saisies dans les contrôles du formulaire (5).

En cas de souci de validation on appelle la méthode setValidation (6) :

setValidation: function(response) {
  this.validation.name = response.body.name ? response.body.name[0] : ''
  this.validation.email = response.body.email ? response.body.email[0] : ''
},

On informe l’objet validation avec les messages de validation reçus. ce qui va avoir pour effet de mettre en rouge le contrôle correspondant :

<div class="ui field" :class="{ error: validation.name }">

img60Et d’afficher un message en haut de la liste avec les textes des erreurs :

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

img61

Au niveau de Laravel on a prévu ces règles :

public function store(Request $request)
{
    $this->validate($request, [
        'name' => 'required|max:255',
        'email' => 'required|email|max:255|unique:users',
    ]);

    $input = array_merge($request->all(), ['password' => bcrypt('secret')]);
    
    User::create($input);
    
    return response()->json();       
}

On voit aussi qu’on affecte automatiquement un mot de passe pour faire plaisir à la base.

Modifier un utilisateur

La modification d’un utilisateur ressemble beaucoup à l’ajout avec quelques contraintes supplémentaires. En effet il faut tranférer les données dans le formulaire, empêcher de faire une autre action, prévoir un bouton d’annulation.

On dispose d’un bouton de modification pour chaque utilisateur :

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

img62

On appelle la méthode edit :

edit: function(index) {
  this.resetMessages()   (1)
  this.user = this.users[index]   (2)
  this.userSave = JSON.parse(JSON.stringify(this.user))   (3)
  this.users.splice(index, 1)   (4)
  this.edition = true   (5)
},

On supprime les messages de validation éventuels (1).

On transfère les données dans le formulaire (2).

On clone les données pour le cas de l’annulation (3).

On supprime les données de la liste (4).

On passe en mode édition en informant la propriété edition (5).

On a donc les informations dans le formulaire, deux boutons et les autres boutons tous inhibés :

img63

L’inhibition des boutons se fait en ajoutant la classe disabled avec la propriété edition :

:class="{ disabled: edition }"

Si on utilise le bouton d’annulation on appelle la méthode undo :

undo: function() {
  this.users.push(this.userSave)   (1)
  this.user = { name: '', email: '' }   (2)
  this.edition = false   (3)
},

On rétablit l’utilisateur dans le tableau (1).

On efface les données du formulaire (2).

On enlève le mode édition (3).

Si on utilise le bouton d’envoi on active la méthode update :

update: function() {
    this.resetMessages()
    this.resource.update({id: this.user.id}, this.user).then((response) => {
    this.success = true
    this.edition = false
    this.users.push(this.user)
    this.user = { name: '', email: '' }
  }, (response) => {
    this.setValidation(response)
  });
},

Là c’est comme dans le cas de l’ajout à part qu’on utilise l’action update de la ressource.

On a la même validation.

Au niveau de Laravel on a ce code classique :

public function update(Request $request, $id)
{
    $this->validate($request, [
        'name' => 'required|max:255',
        'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($id)],
    ]);

    User::find($id)->update($request->all());

    return response()->json(); 
}

En conclusion

Mon application d’exemple n’est pas très réaliste avec mon API accessible par tout le monde mais elle montre comment mettre en oeuvre les plugins vue-resource et vue-sweetalert. D’autre part s’il y a beaucoup d’utilisateurs il serait judicieux de faire une pagination. Alors dans le prochain article je montrerai comment ajouter facilement la pagination et on en profitera aussi pour créer un composant pour les messages puisqu’on en a 3 différents dans notre application.

 

Print Friendly, PDF & Email

Un commentaire

Laisser un commentaire