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

Plus loin

bestmomo (Laravel)

Vue.js est une bibliothèque qui permet de créer facilement des interfaces Web interactives. Ce tutoriel est la suite d'une série de cours dont l'objectif est de vous apprendre à installer, paramétrer et utiliser ce framework.

Commentez

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

- Auteur : bestmomo (Laravel)

- Parution : 26 novembre 2017

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

Sommaire

- I. On s'organise !

- I-A. Webpack

- I-B. Installer vue-loader

- I-C. État des lieux

- I-D. Le panier encore revisité

- I-D-1. index.html

- I-D-2. Le composant Panier

- I-D-3. Le composant Editeur

- I-D-4. Le composant principal

- I-E. Jouer avec les templates et les styles

- I-E-1. Les templates

- I-E-2. Le style

- I-F. La production

- II. Avec Laravel

- II-A. État des lieux

- II-B. Installation

- II-C. Le panier

- II-D. On lance Gulp

- III. Vue-resource (1/2)

- III-A. Installation de l'intendance

- III-A-1. Laravel

- III-A-2. La base de données

- III-A-3. Route et contrôleur

- III-A-4. npm

- III-B. L'application

- III-B-1. Les données du composant

- III-C. Le chargement de l'application

- III-D. Supprimer un utilisateur

- III-E. Ajouter un utilisateur

- III-F. Modifier un utilisateur

- III-G. En conclusion

- IV. Vue-resource (2/2)

- IV-A. Un composant pour les messages

- IV-B. La pagination

- IV-B-1. Le template

- IV-B-2. Repérage de l'index de l'utilisateur

- IV-C. Conclusion

- V. Pagination avancée

- V-A. Côté Laravel

- V-B. Quelle pagination ?

- V-C. Le composant principal

- V-C-1. La liste déroulante

- V-C-2. L'intégration de la pagination

- V-D. Le composant de pagination

- V-D-1. Les propriétés

- V-D-2. Le template et les boutons

- V-D-3. La logique

- V-D-4. Un peu d'observation

- V-D-5. Le style

- V-E. Conclusion

I - On s'organise !

Dans tous les exemples qu'on a vus dans précédent tutoriel on a placé le code JavaScript sur la page, on pourrait évidemment le mettre dans un fichier spécifique, ce qui ne changerait rien au principe. Tant qu'on n'a pas trop de code, c'est parfait, simple et léger, mais quand le code s'accumule on aurait envie de pouvoir l'organiser plus efficacement.

D'autre part on voudrait aussi des fois effectuer d'autres actions : utiliser Babel pour coder en ES6, utiliser Pug dans notre template, utiliser Sass pour notre style, minifier notre JavaScript en production…

Le développement rapide du codage côté client a vu l'apparition d'une multitude d'outils : Grunt, Gulp, Browserify, Webpack, Lint… On finit par s'y perdre ! On a des tasks runners, des bundlers, et autres… Il est grand temps qu'une normalisation voie le jour…

Je ne vais pas clarifier tout ça dans ce tutoriel, ce n'est pas le but. On va juste voir un plugin bien pratique de vue.js : vue-loader. Ce plugin permet :

- d'écrire chaque composant dans un fichier avec le template, le script et le style ;

- d'écrire le code JavaScript en ES6 ;

- d'utiliser Webpack avec simplicité ;

- d'offrir un serveur de développement avec mise à jour en temps réel des modifications ;

- de générer les fichiers de productions…

Le fondement de tout ça c'est Webpack, alors pour ceux qui ne connaissent pas je vais en parler un peu.

I-A - Webpack

Webpack est un module bundler (je ne trouve pas de traduction satisfaisante). Le but premier est de pouvoir organiser le code (css, images, polices, JavaScript…) en modules pour simplifier le développement. Il a tendance à détrôner Grunt et Gulp dans cette tâche.

En gros, on crée des modules interdépendants, Webpack traite tout ça pour les transformer en fichiers statiques tout prêts pour le Web :

Webpack est capable de charger à peu près n'importe quoi à partir du moment où on lui explique comment faire (en pratique il faut un loader dédié). En plus il est assez intelligent pour pouvoir séparer le résultat en plusieurs paquets pour améliorer les performances.

Le seul reproche qu'on peut faire à Webpack c'est qu'il n'est pas vraiment facile à prendre en main, mais une fois qu'on le connait bien on peut vraiment tout faire !

Dans le contexte de ce tutoriel, on va voir que le plugin vue-loader a bien préparé le terrain et que l'utilisation en est grandement facilitée. Si vous voulez vraiment vous lancer dans Webpack, il y a le tutoriel officiel qui est bien fait.

I-B - Installer vue-loader

On va installer un outil en ligne de commande (vue-cli) pour pouvoir ensuite utiliser vue-loader.

Mais à la base il vous faut node.js. Si vous ne l'avez pas, alors commencez par l'installer globalement, il vous servira pour bien d'autres choses !

Le gestionnaire de dépendances de node.js est npm. C'est l'équivalent de composer, mais pour JavaScript. Lui aussi il vous le faut, mais vous pouvez l'installer en même temps que node.js.

Une fois que vous avez tout ça, vous pouvez installer le plugin avec ces commandes dans votre console :

1. npm install -g vue-cli (1)

2. vue init webpack-simple test (2)

3. cd test (3)

4. npm install (4)

5. npm run dev (5)

Voyons cela de plus près :

- (1) on commence par installer le plugin avec npm ;

- (2) on initialise le plugin dans le dossier test, acceptez les valeurs par défaut ;

- (3) on se positionne dans le dossier test ;

- (4) on installe toutes les dépendances (ça peut être long, ça crée un dossier node_modules bien garni), vous devriez vous retrouver avec tout ça :

- (5) on lance le serveur de développement (en localhost:8080).

Si tout se passe bien, votre navigateur préféré s'ouvre avec ça :

Il ne vous reste plus qu'à coder votre application !

Nota : j'utilise ici le template simplifié webpack-simple, mais il existe aussi la version complète webpack avec plein de choses détaillées sur la page github.

I-C - État des lieux

Pour fonctionner, Webpack a besoin d'un fichier de configuration webpack.config.js. Vous en trouvez un à la racine avec ce code :

1. var path = require('path')

2. var webpack = require('webpack')

4. module.exports = {

5. entry: './src/main.js',

6. output: {

7. path: path.resolve(__dirname, './dist'),

8. publicPath: '/dist/',

9. filename: 'build.js'

10. },

11. module: {

12. rules: [

13. {

14. test: /\.vue$/,

15. loader: 'vue',

16. options: {

17. // vue-loader options go here

18. }

19. },

20. {

21. test: /\.js$/,

22. loader: 'babel',

23. exclude: /node_modules/

24. },

25. {

26. test: /\.(png|jpg|gif|svg)$/,

27. loader: 'file',

28. options: {

29. name: '[name].[ext]?[hash]'

30. }

31. }

32. ]

33. },

34. resolve: {

35. alias: {

36. 'vue$': 'vue/dist/vue'

37. }

38. },

39. devServer: {

40. historyApiFallback: true,

41. noInfo: true

42. },

43. devtool: '#eval-source-map'

44. }

46. if (process.env.NODE_ENV === 'production') {

47. module.exports.devtool = '#source-map'

48. // http://vue-loader.vuejs.org/en/workflow/production.html

49. module.exports.plugins = (module.exports.plugins || []).concat([

50. new webpack.DefinePlugin({

51. 'process.env': {

52. NODE_ENV: '"production"'

53. }

54. }),

55. new webpack.optimize.UglifyJsPlugin({

56. compress: {

57. warnings: false

58. }

59. }),

60. new webpack.LoaderOptionsPlugin({

61. minimize: true

62. })

63. ])

64. }

On ne va pas détailler tout ça, mais au moins le minimum :

- entry : là on définit le fichier qui est le point d'entrée de l'application : src/main.js ;

- output : là on définit le fichier de sortie, ça ne sert pas pour le développement, mais pour la distribution : dist/build.js ;

- module : là on met les modules dont on va avoir besoin, les loaders, on a par défaut vue, babel et file.

Pour le moment on va se contenter de ça.

Les fichiers de développement de notre application sont dans le dossier src (source) :

Un fichier qui contient un composant a comme suffixe vue, ici on n'en a qu'un : App.vue. Voici son code :

1. <template>

2. <div id="app">

3. <img src="./assets/logo.png">

4. <h1>{{ msg }}</h1>

5. <h2>Essential Links</h2>

6. <ul>

7. <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>

8. <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>

9. <li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>

10. <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>

11. </ul>

12. <h2>Ecosystem</h2>

13. <ul>

14. <li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>

15. <li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>

16. <li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>

17. <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>

18. </ul>

19. </div>

20. </template>

22. <script>

23. export default {

24. name: 'app',

25. data () {

26. return {

27. msg: 'Welcome to Your Vue.js App'

28. }

29. }

30. }

31. </script>

33. <style>

34. #app {

35. font-family: 'Avenir', Helvetica, Arial, sans-serif;

36. -webkit-font-smoothing: antialiased;

37. -moz-osx-font-smoothing: grayscale;

38. text-align: center;

39. color: #2c3e50;

40. margin-top: 60px;

41. }

43. h1, h2 {

44. font-weight: normal;

45. }

47. ul {

48. list-style-type: none;

49. padding: 0;

50. }

52. li {

53. display: inline-block;

54. margin: 0 10px;

55. }

57. a {

58. color: #42b983;

59. }

60. </style>

On a trois blocs :

- template : pour le template HTML ;

- script : pour le code JavaScript, on peut utiliser ES6 parce que par défaut babel est activé, c'est pour ça qu'il est prévu un export dans le code du composant ;

- style : pour le style CSS.

vue-loader sera chargé d'extraire ces blocs, de transformer le code avec les loaders prévus, et enfin de les assembler en un module qui pourra être pris en charge par Webpack.

On a vu que le point d'entrée est main.js :

1. import Vue from 'vue'

2. import App from './App.vue'

4. new Vue({

5. el: '#app',

6. render: h => h(App)

7. })

On importe Vue évidemment et aussi le seul composant App. Ne vous inquiétez pas de la syntaxe avec le render, on verra ça peut-être dans un autre chapitre.

Au niveau de index.html on a du classique :

1. <!DOCTYPE html>

2. <html lang="en">

3. <head>

4. <meta charset="utf-8">

5. <title>test</title>

6. </head>

7. <body>

8. <div id="app"></div>

9. <script src="/dist/build.js"></script>

10. </body>

11. </html>

I-D - Le panier encore revisité

Maintenant qu'on a toute l'intendance, on va à nouveau coder le panier qu'on a vu lors du précédent tutoriel, mais cette fois à la mode vue-loader.

On va conserver inchangé le fichier main.js.

I-D-1 - index.html

On va adapter index.html essentiellement pour intégrer Bootstrap :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <meta http-equiv="X-UA-Compatible" content="IE=edge">

6. <meta name="viewport" content="width=device-width, initial-scale=1">

7. <title>Test vue.js</title>

8. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

9. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">

10. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/united/bootstrap.min.css">

11. </head>

12. <body>

13. <div class="container">

14. <br>

15. <div id="app"></div>

16. </div>

17. <script src="/dist/build.js"></script>

18. </body>

19. </html>

I-D-2 - Le composant Panier

On va créer un fichier Panier.vue pour le composant du panier :

1. <template>

2. <div class="panel panel-primary">

3. <div class="panel-heading">Panier</div>

4. <table class="table table-bordered table-striped">

5. <thead>

6. <tr>

7. <th class="col-sm-4">Article</th>

8. <th class="col-sm-2">Quantité</th>

9. <th class="col-sm-2">Prix</th>

10. <th class="col-sm-2">Total</th>

11. <th class="col-sm-1"></th>

12. <th class="col-sm-1"></th>

13. </tr>

14. </thead>

15. <tbody>

16. <tr v-for="(item, index) in panier">

17. <td>{{ item.article }}</td>

18. <td>{{ item.quantite }}</td>

19. <td>{{ item.prix }} €</td>

20. <td>{{ (item.quantite * item.prix).toFixed(2) }} €</td>

21. <td><button class="btn btn-info btn-block" @click="modifier(index)"><i class="fa fa-edit fa-lg"></i></button></td>

22. <td><button class="btn btn-danger btn-block" @click="supprimer(index)"><i class="fa fa-trash-o fa-lg"></i></button></td>

23. </tr>

24. <tr>

25. <td colspan="3"></td>

26. <td><strong>{{ total }} €</strong></td>

27. <td colspan="2"></td>

28. </tr>

29. <editeur :article="article" @add="ajouter"></editeur>

30. </tbody>

31. </table>

32. </div>

33. </template>

35. <script>

36. import Editeur from './Editeur.vue'

38. export default {

39. props: ['panier'],

40. data: function () {

41. return {

42. article: { article: '', quantite: 0, prix: 0 }

43. }

44. },

45. computed: {

46. total: function () {

47. let total = 0;

48. for(let el of this.panier) {

49. total += el.prix * el.quantite

50. }

51. return total.toFixed(2)

52. }

53. },

54. methods: {

55. modifier: function(index) {

56. this.article = this.panier[index]

57. this.panier.splice(index, 1)

58. },

59. supprimer: function(index) {

60. this.panier.splice(index, 1)

61. },

62. ajouter: function(input) {

63. this.panier.push(input)

64. this.article = { article: '', quantite: 0, prix: 0 }

65. }

66. },

67. components: {

68. Editeur

69. }

70. }

71. </script>

Je n'ai rien changé au template.

Pour le JavaScript on sait qu'il faudra importer le composant Editeur :

1. import Editeur from './Editeur.vue'

3. ...

5. components: {

6. Editeur

7. }

D'autre part je profite de la mise à disposition de ES6 pour voir si babel fonctionne bien :

1. let total = 0;

2. for(let el of this.panier) {

3. total += el.prix * el.quantite

4. }

I-D-3 - Le composant Editeur

De la même manière, j'ai créé le fichier Editeur.vue pour le composant Editeur :

1. <template>

2. <tr>

3. <td><input type="text" class="form-control" v-model="input.article" ref="modif" placeholder="Article"></td>

4. <td><input type="text" class="form-control" v-model="input.quantite" placeholder="Quantité"></td>

5. <td><input type="text" class="form-control" v-model="input.prix" placeholder="Prix"></td>

6. <td colspan="3"><button class="btn btn-primary btn-block" @click="ajouter()">Ajouter</button></td>

7. </tr>

8. </template>

10. <script>

11. export default {

12. props: ['article'],

13. computed: {

14. input: function() {

15. return this.article;

16. }

17. },

18. methods: {

19. ajouter: function() {

20. this.$emit('add', this.input);

21. }

22. }

23. }

24. </script>

Là je n'ai rien changé.

I-D-4 - Le composant principal

Pour terminer, il faut modifier le composant App.vue :

1. <template>

2. <div id="app">

3. <panier :panier="panier"></panier>

4. </div>

5. </template>

7. <script>

8. import Panier from './Panier.vue'

10. export default {

11. name: 'application',

12. data () {

13. return {

14. panier: [

15. { article: "Cahier", quantite: 2, prix: '5.30' },

16. { article: "Crayon", quantite: 4, prix: '1.10' },

17. { article: "Gomme", quantite: 1, prix: '3.25' }

18. ]

19. }

20. },

21. components: {

22. Panier

23. }

24. }

25. </script>

On importe le composant Panier.

À la fin vous devez avoir ces fichiers :

Et normalement le panier à l'écran :

Si ce n'est pas le cas, relisez l'article pour voir le code que vous avez raté ! Ou lisez les informations fournies par les outils de développement du navigateur (je rappelle qu'il y a une extension pour vue.js dans Chrome).

I-E - Jouer avec les templates et les styles

I-E-1 - Les templates

On va voir qu'on peut vraiment faire beaucoup de choses, par exemple utiliser un outil de template comme Pug. On va commencer par l'installer avec npm :

npm install pug --save-dev

Ensuite, changez ainsi le code du template du composant Editeur.vue :

1. <template lang="pug">

2. tr

3. td

4. input(type="text" class="form-control" v-model="input.article" ref="modif" placeholder="Article")

5. td

6. input(type="text" class="form-control" v-model="input.quantite" placeholder="Quantité")

7. td

8. input(type="text" class="form-control" v-model="input.prix" placeholder="Prix")

9. td(colspan="3")

10. button(class="btn btn-primary btn-block" @click="ajouter()") "Ajouter"

11. </template>

On déclare le système de template utilisé avec un attribut dans la balise template. Ensuite on l'utilise dans le code.

Si tout passe bien le bon code HTML doit être généré. Pratique non ?

I-E-2 - Le style

On peut aussi ajouter du style au composant Editeur.vue. Là aussi on peut utiliser un préprocesseur comme Sass. On va d'abord l'installer :

npm install sass-loader node-sass --save-dev

Et ensuite, ajouter un bloc de style :

1. <style lang="sass">

2. $fond: lightblue;

3. .bleu {

4. background-color: $fond !important;

5. }

6. </style>

Il faut quand même préciser où agit la classe dans le template :

tr(class="bleu")

Avec ce résultat :

Le CSS a été correctement généré !

I-F - La production

Le but c'est quand même de générer les fichiers de production. Là c'est tout simple :

npm run build

Voyons le dossier dist :

Si vous ouvrez le fichier build.js vous verrez que le code a été « uglifié » et minimisé. Mais maintenant est-ce que ça fonctionne ? Il suffit d'ouvrir index.html dans votre navigateur. Il faudra peut-être changer la référence du fichier build.js si vous n'êtes pas avec un hôte virtuel :

<script src="dist/build.js"></script>

Normalement tout fonctionne bien, même le style ajouté à l'éditeur, le tout dans un fichier de 580 Ko !

Si vous voulez voir le code produit sans la compression, commentez ces lignes dans webpack.config.js :

1. new webpack.optimize.UglifyJsPlugin({

2. compress: {

3. warnings: false

4. }

5. }),

6. new webpack.LoaderOptionsPlugin({

7. minimize: true

8. })

Si vous voulez voir ce que ça donne, je l'ai mis en ligne ici.

II - Avec Laravel

Comme ce tutoriel est essentiellement consacré à Laravel il me faut évoquer l'utilisation de Vue.js 2 avec Laravel 5.3 puisque cette version a été enrichie de tout ce qu'il faut pour sceller ce mariage. J'en ai un peu parlé dans mon tutoriel sur les nouveautés de la version 5.3 je vais y revenir de façon plus précise maintenant en poursuivant l'exemple du panier dans le contexte de Laravel.

II-A - État des lieux

Dans l'article référencé ci-dessus, j'avais présenté l'infrastructure en place lorsqu'on installe Laravel 5.3 :

On a déjà :

- un composant vue.js 2 : Exemple.vue ;

- une application de base : app.js ;

- un fichier d'initialisation : bootstrap.js.

Avec Laravel on n'utilise pas directement Webpack - on pourrait si on le voulait -, mais plutôt Elixir qui est lui-même une sur couche qui simplifie l'utilisation de Gulp.

Si on regarde le fichier de configuration de Gulp (gulpfile.js) :

1. const elixir = require('laravel-elixir');

3. require('laravel-elixir-vue-2');

5. /*

6. |--------------------------------------------------------------------------

7. | Elixir Asset Management

8. |--------------------------------------------------------------------------

9. |

10. | Elixir provides a clean, fluent API for defining some basic Gulp tasks

11. | for your Laravel application. By default, we are compiling the Sass

12. | file for our application, as well as publishing vendor resources.

13. |

14. */

16. elixir(mix => {

17. mix.sass('app.scss')

18. .webpack('app.js');

19. });

On voit qu'on lance une tâche Webpack en référençant le fichier app.js qui est l'entrée de l'application. C'est donc Elixir qui se charge de tout de façon invisible (on n'a pas de fichier webpack.config.js, mais on peut en créer un au besoin).

Concernant npm on a ce fichier package.json :

1. {

2. "private": true,

3. "scripts": {

4. "prod": "gulp --production",

5. "dev": "gulp watch"

6. },

7. "devDependencies": {

8. "bootstrap-sass": "^3.3.7",

9. "gulp": "^3.9.1",

10. "jquery": "^3.1.0",

11. "laravel-elixir": "^6.0.0-11",

12. "laravel-elixir-vue-2": "^0.2.0",

13. "laravel-elixir-webpack-official": "^1.0.2",

14. "lodash": "^4.16.2",

15. "vue": "^2.0.1",

16. "vue-resource": "^1.0.3"

17. }

18. }

On voit qu'on charge vue ainsi que le plugin vue-resource dont je parlerai dans un autre chapitre.

On dispose aussi de deux scripts :

- dev : pour le développement avec une observation des modifications (watch) ;

- prod : pour la production en compressant les fichiers.

II-B - Installation

La première chose à faire est évidemment d'utiliser npm pour charger toutes les dépendances :

npm install

Ce qui crée le dossier node_modules avec tous les fichiers :

On sait qu'on aura besoin du système de template pug pour le panier, alors on l'ajoute :

npm install pug

On a ainsi tout ce qu'il faut au niveau des dépendances !

En faisant mes essais, je me suis rendu compte que la transformation du code ES6 en ES5 avec Elixir n'était pas assurée par Babel, mais par Bublé. Cela a certaines conséquences parce que ce n'est pas l'ensemble de ES6 qui est pris en charge, ce qui est justifié dans la documentation :

Bublé limits itself to ES features that can be compiled to compact, performant ES5

Vous disposez d'ailleurs d'une page avec les fonctionnalités non supportées. En particulier on y trouve le for…of que j'ai utilisé précédemment pour justement tester la transformation effectuée par Babel. C'est d'ailleurs comme ça que je m'en suis rendu compte, parce que je tombais sur une erreur et que j'ai dû un peu fouiller parce que rien n'est indiqué dans la documentation de Laravel…

On peut tout de même changer les options de fonctionnement de Bublé, mais pas d'une façon qui m'a semblé suffisamment claire et adaptable à la situation. On peut évidemment utiliser Babel, mais le loader est codé en dur dans Elixir…

Pour le présent chapitre, j'ai pris la configuration par défaut et enlevé le for…of du code, ce qui était le plus simple !

II-C - Le panier

Maintenant pour mettre en place le panier c'est tout simple. On crée une route pour le tester :

Route::get('test', function () {
return view('test');
});

La vue test.blade.php :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <meta http-equiv="X-UA-Compatible" content="IE=edge">

6. <meta name="viewport" content="width=device-width, initial-scale=1">

7. <title>Test vue.js</title>

8. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

9. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">

10. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/united/bootstrap.min.css">

11. </head>

12. <body>

13. <div class="container">

14. <br>

15. <div id="app">

16. <app></app>

17. </div>

18. </div>

19. <script src="/js/app.js"></script>

20. </body>

21. </html>

On a :

- un identifiant app pour servir de référence à Vue.js ;

- une balise app pour notre application ;

- le chargement du fichier JavaScript de l'application /js/app.js.

Dans app.js on change le composant :

1. require('./bootstrap');

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

5. const app = new Vue({

6. el: '#app'

7. });

On met en place nos trois composants et on supprime Exemple.vue :

Pour rappel voici le code de App.vue :

1. <template>

2. <div id="app">

3. <panier :panier="panier"></panier>

4. </div>

5. </template>

7. <script>

8. import Panier from './Panier.vue'

10. export default {

11. name: 'application',

12. data () {

13. return {

14. panier: [

15. { article: "Cahier", quantite: 2, prix: '5.30' },

16. { article: "Crayon", quantite: 4, prix: '1.10' },

17. { article: "Gomme", quantite: 1, prix: '3.25' }

18. ]

19. }

20. },

21. components: {

22. Panier

23. }

24. }

25. </script>

Le code de Panier.vue :

1. <template>

2. <div class="panel panel-primary">

3. <div class="panel-heading">Panier</div>

4. <table class="table table-bordered table-striped">

5. <thead>

6. <tr>

7. <th class="col-sm-4">Article</th>

8. <th class="col-sm-2">Quantité</th>

9. <th class="col-sm-2">Prix</th>

10. <th class="col-sm-2">Total</th>

11. <th class="col-sm-1"></th>

12. <th class="col-sm-1"></th>

13. </tr>

14. </thead>

15. <tbody>

16. <tr v-for="(item, index) in panier">

17. <td>{{ item.article }}</td>

18. <td>{{ item.quantite }}</td>

19. <td>{{ item.prix }} €</td>

20. <td>{{ (item.quantite * item.prix).toFixed(2) }} €</td>

21. <td><button class="btn btn-info btn-block" @click="modifier(index)"><i class="fa fa-edit fa-lg"></i></button></td>

22. <td><button class="btn btn-danger btn-block" @click="supprimer(index)"><i class="fa fa-trash-o fa-lg"></i></button></td>

23. </tr>

24. <tr>

25. <td colspan="3"></td>

26. <td><strong>{{ total }} €</strong></td>

27. <td colspan="2"></td>

28. </tr>

29. <editeur :article="article" @add="ajouter"></editeur>

30. </tbody>

31. </table>

32. </div>

33. </template>

35. <script>

36. import Editeur from './Editeur.vue'

38. export default {

39. props: ['panier'],

40. data: function () {

41. return {

42. article: { article: '', quantite: 0, prix: 0 }

43. }

44. },

45. computed: {

46. total: function () {

47. let total = 0;

48. this.panier.forEach(function(el) {

49. total += el.prix * el.quantite;

50. });

51. return total.toFixed(2)

52. }

53. },

54. methods: {

55. modifier: function(index) {

56. this.article = this.panier[index]

57. this.panier.splice(index, 1)

58. },

59. supprimer: function(index) {

60. this.panier.splice(index, 1)

61. },

62. ajouter: function(input) {

63. this.panier.push(input)

64. this.article = { article: '', quantite: 0, prix: 0 }

65. }

66. },

67. components: {

68. Editeur

69. }

70. }

71. </script>

C'est là que j'ai supprimé le for…of.

Et Editeur.vue :

1. <template lang="pug">

2. tr(class="bleu")

3. td

4. input(type="text" class="form-control" v-model="input.article" ref="modif" placeholder="Article")

5. td

6. input(type="text" class="form-control" v-model="input.quantite" placeholder="Quantité")

7. td

8. input(type="text" class="form-control" v-model="input.prix" placeholder="Prix")

9. td(colspan="3")

10. button(class="btn btn-primary btn-block" @click="ajouter()") "Ajouter"

11. </template>

13. <script>

14. export default {

15. props: ['article'],

16. computed: {

17. input: function() {

18. return this.article;

19. }

20. },

21. methods: {

22. ajouter: function() {

23. this.$emit('add', this.input);

24. }

25. }

26. }

27. </script>

29. <style lang="sass">

30. $fond: lightblue;

31. .bleu {

32. background-color: $fond !important;

33. }

34. </style>

II-D - On lance Gulp

Il ne reste plus qu'à voir si ça fonctionne :

Apparemment tout se passe bien en mode développement. On retrouve bien le panier :

Et on est en mode watch pour que tous les changements soient pris en compte.

Et voilà ce que ça donne en mode production :

Cette fois le fichier /js/app.js est minifié et le panier fonctionne encore !

On voit donc que la mise en œuvre de Vue.js2 avec Laravel 5.3 est assez facile.

Reste toutefois à voir s'il est aussi facile de modifier la configuration selon les besoins. Je n'en ai pas l'expérience parce que je n'ai eu aucun projet qui justifie ce genre de mise en œuvre.

III - Vue-resource (1/2)

Dans le tutoriel précédent, on a vu l'essentiel de Vue.js, mais cette bibliothèque bénéficie de nombreuses ressources additionnelles. Vous en trouvez l'ensemble sur cette page avec en plus la référence des tutoriels et autres. Dans ce chapitre 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 part 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…

III-A - Installation de l'intendance

III-A-1 - 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 :

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

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

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

'locale' => 'fr',

Ça va nous servir pour les messages de la validation.

III-A-2 - 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 :

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

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 dix utilisateurs, on va modifier ce fichier :

Avec ce code :

1. public function run()

2. {

3. factory(App\User::class, 10)->create();;

4. }

Vous pouvez alors lancer la population :

php artisan db:seed

Si tout se passe bien, vous devriez avoir dix utilisateurs :

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.

III-A-3 - 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

Changez ainsi le code :

1. <?php

3. namespace App\Http\Controllers;

5. use Illuminate\Http\Request;

7. use App\User;

8. use Illuminate\Validation\Rule;

10. class UserController extends Controller

11. {

12. /**

13. * Display a listing of the resource.

14. *

15. * @return \Illuminate\Http\Response

16. */

17. public function index()

18. {

19. return User::select('id', 'name', 'email')->get();

20. }

22. /**

23. * Send app.

24. *

25. * @return \Illuminate\Http\Response

26. */

27. public function app()

28. {

29. return view('users');

30. }

32. /**

33. * Store a newly created resource in storage.

34. *

35. * @param  \Illuminate\Http\Request  $request

36. * @return \Illuminate\Http\Response

37. */

38. public function store(Request $request)

39. {

40. $this->validate($request, [

41. 'name' => 'required|max:255',

42. 'email' => 'required|email|max:255|unique:users',

43. ]);

45. $input = array_merge($request->all(), ['password' => bcrypt('secret')]);

47. User::create($input);

49. return response()->json();

50. }

52. /**

53. * Update the specified resource in storage.

54. *

55. * @param  \Illuminate\Http\Request  $request

56. * @param  int  $id

57. * @return \Illuminate\Http\Response

58. */

59. public function update(Request $request, $id)

60. {

61. $this->validate($request, [

62. 'name' => 'required|max:255',

63. 'email' => ['required', 'email', 'max:255',

64. Rule::unique('users')->ignore($id)],

65. ]);

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

69. return response()->json();

70. }

72. /**

73. * Remove the specified resource from storage.

74. *

75. * @param  int  $id

76. * @return \Illuminate\Http\Response

77. */

78. public function destroy($id)

79. {

80. User::find($id)->delete();

82. return response()->json();

83. }

84. }

Et ajoutez ces routes dans routes/web.php :

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

3. Route::resource('users', 'UserController', ['except' => [

4. 'create', 'show', 'edit'

5. ]]);

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 :

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 ce chapitre.

On ne va mettre en place aucune authentification pour simplifier.

III-A-4 - npm

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

1. {

2. "private": true,

3. "scripts": {

4. "prod": "gulp --production",

5. "dev": "gulp watch"

6. },

7. "devDependencies": {

8. "bootstrap-sass": "^3.3.7",

9. "gulp": "^3.9.1",

10. "jquery": "^3.1.0",

11. "laravel-elixir": "^6.0.0-9",

12. "laravel-elixir-vue-2": "^0.2.0",

13. "laravel-elixir-webpack-official": "^1.0.2",

14. "lodash": "^4.16.2",

15. "vue": "^2.0.1",

16. "vue-resource": "^1.0.3"

17. }

18. }

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 :

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

npm i vue-sweetalert --save-dev

III-B - L'application

Maintenant qu'on a mis en place toute l'intendance, on va coder l'application.

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

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

1. require('./bootstrap');

3. // Sweetalert

4. import VueSweetAlert from 'vue-sweetalert';

5. Vue.use(VueSweetAlert);

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

9. const app = new Vue({

10. el: '#app'

11. });

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 :

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <meta http-equiv="X-UA-Compatible" content="IE=edge">

6. <meta name="viewport" content="width=device-width, initial-scale=1">

7. <title>Test vue.js</title>

8. <link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css">

9. </head>

10. <body>

11. <div class="container">

12. <br>

13. <div id="app">

14. <app></app>

15. </div>

16. </div>

17. <script>

18. window.Laravel = {!! json_encode(['csrfToken' => csrf_token()]) !!};

19. </script>

20. <script src="/js/app.js"></script>

21. </body>

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

Avec ce code :

1. <template>

2. <div class="ui raised container segment">

3. <div class="ui positive message" v-show="success">

4. <i class="close icon" @click="closeSuccess()"></i>

5. <div class="header">

6. Serveur mis à jour avec succès !

7. </div>

8. </div>

9. <div class="ui negative message" v-show="danger">

10. <i class="close icon" @click="closeDanger()"></i>

11. <div class="header">

12. Echec de la communication avec le serveur !

13. </div>

14. </div>

15. <div class="ui negative message" v-show="validation.name || validation.email">

16. <i class="close icon" @click="closeValidation()"></i>

17. <div class="header">

18. Il y a des erreurs dans la validation des données saisies :

19. </div>

20. <ul class="list">

21. <li>{{ validation.name }}</li>

22. <li>{{ validation.email }}</li>

23. </ul>

24. </div>

25. <table class="ui celled table">

26. <caption><h1>Liste des utilisateurs</h1></caption>

27. <thead>

28. <tr>

29. <th>Nom</th>

30. <th>Email</th>

31. <th></th>

32. <th></th>

33. </tr>

34. </thead>

35. <tbody>

36. <tr v-for="(user, index) in users">

37. <td>{{ user.name }}</td>

38. <td>{{ user.email }}</td>

39. <td>

40. <button class="fluid ui orange button" :class="{ disabled: edition }" data-tooltip="Modifier cet utilisateur" data-position="top center" @click="edit(index)">

41. <i class="edit icon"></i>

42. </button>

43. </td>

44. <td>

45. <button class="fluid ui red button" :class="{ disabled: edition }" data-tooltip="Supprimer cet utilisateur" data-position="top center" @click="del(index)">

46. <i class="remove user icon"></i>

47. </button>

48. </td>

49. </tr>

50. <tr class="ui form">

51. <td>

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

53. <input type="text"v-model="user.name" placeholder="Nom">

54. </div>

55. </td>

56. <td>

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

58. <input type="email" class="form-control" v-model="user.email" placeholder="Email">

59. </div>

60. </td>

61. <td colspan="2" v-if="!edition">

62. <button class="fluid ui blue button" data-tooltip="Ajouter un utilisateur" data-position="top center" @click="add()">

63. <i class="add user icon"></i>

64. </button>

65. </td>

66. <td v-if="edition">

67. <button class="fluid ui blue button" data-tooltip="Mettre à jour cet utilisateur" data-position="top center" @click="update()">

68. <i class="add user icon"></i>

69. </button>

70. </td>

71. <td v-if="edition">

72. <button class="fluid ui violet button" data-tooltip="Annuler la modification" data-position="top center" @click="undo()">

73. <i class="undo icon"></i>

74. </button>

75. </td>

76. </tr>

77. </tbody>

78. </table>

79. </div>

80. </template>

82. <script>

83. export default {

84. name: 'application',

85. resource: null,

86. data () {

87. return {

88. users: [],

89. user: { name: '', email: ''},

90. userSave: {},

91. success: false,

92. danger: false,

93. edition: false,

94. validation: { name: '', email: ''}

95. }

96. },

97. mounted: function() {

98. this.resource = this.$resource('/users{/id}')

99. this.resource.get().then((response) => {

100. this.users = response.body

101. }, (response) => {

102. this.danger = true

103. })

104. },

105. methods: {

106. add: function() {

107. this.resetMessages()

108. this.resource.save(this.user).then((response) => {

109. this.success = true

110. this.users.push(this.user)

111. this.user = { name: '', email: '' }

112. }, (response) => {

113. this.setValidation(response)

114. });

115. },

116. update: function() {

117. this.resetMessages()

118. this.resource.update({id: this.user.id}, this.user).then((response) => {

119. this.success = true

120. this.edition = false

121. this.users.push(this.user)

122. this.user = { name: '', email: '' }

123. }, (response) => {

124. this.setValidation(response)

125. });

126. },

127. del: function(index) {

128. let that = this

129. this.resetMessages()

130. this.$swal({

131. title: 'Vous êtes sûr de vous ?',

132. text: "Il n'y aura aucun retour en arrière possible !",

133. type: 'warning',

134. showCancelButton: true,

135. confirmButtonColor: '#3085d6',

136. cancelButtonColor: '#d33',

137. confirmButtonText: 'Oui supprimer !',

138. cancelButtonText: 'Non, surtout pas !',

139. }).then(function() {

140. that.resource.delete({id: that.users[index].id}).then((response) => {

141. that.success = true

142. that.users.splice(index, 1)

143. }, (response) => {

144. that.danger = true

145. });

146. }).done()

147. },

148. edit: function(index) {

149. this.resetMessages()

150. this.user = this.users[index]

151. this.userSave = JSON.parse(JSON.stringify(this.user))

152. this.users.splice(index, 1)

153. this.edition = true

154. },

155. undo: function() {

156. this.users.push(this.userSave)

157. this.user = { name: '', email: '' }

158. this.edition = false

159. },

160. resetMessages: function() {

161. this.success = false

162. this.danger = false

163. this.closeValidation()

164. },

165. setValidation: function(response) {

166. this.validation.name = response.body.name ? response.body.name[0] : ''

167. this.validation.email = response.body.email ? response.body.email[0] : ''

168. },

169. closeSuccess: function() {

170. this.success = false

171. },

172. closeDanger: function() {

173. this.danger = false

174. },

175. closeValidation: function() {

176. this.validation = { name: '', email: ''}

177. }

178. }

179. }

180. </script>

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

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

On obtient bien notre application avec les dix utilisateurs.

III-B-1 - Les données du composant

Regardons le data du composant :

1. data () {

2. return {

3. users: [],

4. user: { name: '', email: ''},

5. userSave: {},

6. success: false,

7. danger: false,

8. edition: false,

9. validation: { name: '', email: ''}

10. }

11. },

On a :

- users : tableau de tous les utilisateurs, au départ vide ;

- user : objet pour la liaison de données avec le formulaire ;

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

III-C - 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 :

1. mounted: function() {

2. this.resource = this.$resource('/users{/id}')  (1)

3. this.resource.get().then((response) => {   (2)

4. this.users = response.body

5. }, (response) => {

6. this.danger = true

7. })

8. },

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

1. get: {method: 'GET'},

2. save: {method: 'POST'},

3. query: {method: 'GET'},

4. update: {method: 'PUT'},

5. remove: {method: 'DELETE'},

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

1. <tr v-for="(user, index) in users">

2. <td>{{ user.name }}</td>

3. <td>{{ user.email }}</td>

4. ...

5. </tr>

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

1. public function index()

2. {

3. return User::select('id', 'name', 'email')->get();

4. }

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

1. <div class="ui negative message" v-show="danger">

2. <i class="close icon" @click="closeDanger()"></i>

3. <div class="header">

4. Echec de la communication avec le serveur !

5. </div>

6. </div>

III-D - Supprimer un utilisateur

On dispose de boutons pour supprimer un utilisateur :

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

1. del: function(index) {

2. let that = this

3. this.resetMessages()

4. this.$swal({

5. title: 'Vous êtes sûr de vous ?',

6. text: "Il n'y aura aucun retour en arrière possible !",

7. type: 'warning',

8. showCancelButton: true,

9. confirmButtonColor: '#3085d6',

10. cancelButtonColor: '#d33',

11. confirmButtonText: 'Oui supprimer !',

12. cancelButtonText: 'Non, surtout pas !',

13. }).then(function() {

14. that.resource.delete({id: that.users[index].id}).then((response) => {

15. that.success = true

16. that.users.splice(index, 1)

17. }, (response) => {

18. that.danger = true

19. });

20. }).done()

21. },

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

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

1. that.resource.delete({id: that.users[index].id}).then((response) => {

2. that.success = true

3. that.users.splice(index, 1)

4. }, (response) => {

5. that.danger = true

6. });

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

1. <div class="ui positive message" v-show="success">

2. <i class="close icon" @click="closeSuccess()"></i>

3. <div class="header">

4. Serveur mis à jour avec succès !

5. </div>

6. </div>

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

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

III-E - Ajouter un utilisateur

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

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

1. <tr class="ui form">

2. <td>

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

4. <input type="text" v-model="user.name" placeholder="Nom">

5. </div>

6. </td>

7. <td>

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

9. <input type="email" class="form-control" v-model="user.email" placeholder="Email">

10. </div>

11. </td>

12. <td colspan="2" v-if="!edition">

13. <button class="fluid ui blue button" data-tooltip="Ajouter un utilisateur" data-position="top center" @click="add()">

14. <i class="add user icon"></i>

15. </button>

16. </td>

17. <td v-if="edition">

18. ...

19. </td>

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

1. add: function() {

2. this.resetMessages()   (1)

3. this.resource.save(this.user).then((response) => {    (2)

4. this.success = true   (3)

5. this.users.push(this.user)   (4)

6. this.user = { name: '', email: '' }   (5)

7. }, (response) => {

8. this.setValidation(response)   (6)

9. });

10. },

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

1. setValidation: function(response) {

2. this.validation.name = response.body.name ? response.body.name[0] : ''

3. this.validation.email = response.body.email ? response.body.email[0] : ''

4. },

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

Et d'afficher un message en haut de la liste avec les textes des erreurs :

1. <div class="ui negative message" v-show="validation.name || validation.email">

2. <i class="close icon" @click="closeValidation()"></i>

3. <div class="header">

4. Il y a des erreurs dans la validation des données saisies :

5. </div>

6. <ul class="list">

7. <li>{{ validation.name }}</li>

8. <li>{{ validation.email }}</li>

9. </ul>

10. </div>

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

1. public function store(Request $request)

2. {

3. $this->validate($request, [

4. 'name' => 'required|max:255',

5. 'email' => 'required|email|max:255|unique:users',

6. ]);

8. $input = array_merge($request->all(), ['password' => bcrypt('secret')]);

10. User::create($input);

12. return response()->json();

13. }

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

III-F - Modifier un utilisateur

La modification d'un utilisateur ressemble beaucoup à l'ajout avec quelques contraintes supplémentaires. En effet il faut transfé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>

On appelle la méthode edit :

1. edit: function(index) {

2. this.resetMessages()   (1)

3. this.user = this.users[index]   (2)

4. this.userSave = JSON.parse(JSON.stringify(this.user))   (3)

5. this.users.splice(index, 1)   (4)

6. this.edition = true   (5)

7. },

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 :

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 :

1. undo: function() {

2. this.users.push(this.userSave)   (1)

3. this.user = { name: '', email: '' }   (2)

4. this.edition = false   (3)

5. },

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 :

1. update: function() {

2. this.resetMessages()

3. this.resource.update({id: this.user.id}, this.user).then((response) => {

4. this.success = true

5. this.edition = false

6. this.users.push(this.user)

7. this.user = { name: '', email: '' }

8. }, (response) => {

9. this.setValidation(response)

10. });

11. },

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 :

1. public function update(Request $request, $id)

2. {

3. $this->validate($request, [

4. 'name' => 'required|max:255',

5. 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($id)],

6. ]);

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

10. return response()->json();

11. }

III-G - 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 œuvre 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 chapitre je montrerai comment ajouter facilement la pagination et on en profitera aussi pour créer un composant pour les messages puisqu'on en a trois différents dans notre application.

IV - Vue-resource (2/2)

Dans le précédent chapitrechapitre, 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 :

1. <div class="ui positive message" v-show="success">

2. <i class="close icon" @click="closeSuccess()"></i>

3. <div class="header">

4. Serveur mis à jour avec succès !

5. </div>

6. </div>

7. <div class="ui negative message" v-show="danger">

8. <i class="close icon" @click="closeDanger()"></i>

9. <div class="header">

10. Echec de la communication avec le serveur !

11. </div>

12. </div>

13. <div class="ui negative message" v-show="validation.name || validation.email">

14. <i class="close icon" @click="closeValidation()"></i>

15. <div class="header">

16. Il y a des erreurs dans la validation des données saisies :

17. </div>

18. <ul class="list">

19. <li>{{ validation.name }}</li>

20. <li>{{ validation.email }}</li>

21. </ul>

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

1. <template>

2. <div class="ui message" :class="classType" v-show="show">

3. <i class="close icon" @click="close()"></i>

4. <div class="header">

5. {{ header }}

6. </div>

7. <ul class="list" v-show="showList">

8. <li v-for="element in list" >{{ element }}</li>

9. </ul>

10. </div>

11. </template>

13. <script>

14. export default {

15. name: 'message',

16. props: ['type', 'header', 'show', 'list'],

17. computed: {

18. classType: function() {

19. return {

20. positive: this.type == 'positive',

21. negative: this.type == 'negative'

22. }

23. },

24. showList: function() {

25. return this.list !== undefined

26. }

27. },

28. methods: {

29. close: function() {

30. this.$emit('close')

31. }

32. }

33. }

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

1. <script>

2. import Message from './Message.vue'

4. export default {

5. ...

6. components: {

7. Message

8. }

9. }

10. </script>

Et au niveau du template :

1. <message

2. type="positive"

3. header="Serveur mis à jour avec succès !"

4. :show="success"

5. @close="closeSuccess">

6. </message>

7. <message

8. type="negative"

9. header="Echec de la communication avec le serveur !"

10. :show="danger"

11. @close="closeDanger">

12. </message>

13. <message

14. type="negative"

15. header="Il y a des erreurs dans la validation des données saisies :"

16. :list="[ validation.name, validation.email ]"

17. :show="validation.name != '' || validation.email != ''"

18. @close="closeValidation">

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

1. <template>

2. <div class="ui raised container segment">

3. <message

4. type="positive"

5. header="Serveur mis à jour avec succès !"

6. :show="success"

7. @close="closeSuccess">

8. </message>

9. <message

10. type="negative"

11. header="Echec de la communication avec le serveur !"

12. :show="danger"

13. @close="closeDanger">

14. </message>

15. <message

16. type="negative"

17. header="Il y a des erreurs dans la validation des données saisies :"

18. :list="[ validation.name, validation.email ]"

19. :show="validation.name != '' || validation.email != ''"

20. @close="closeValidation">

21. </message>

22. <table class="ui celled table">

23. <caption><h1>Liste des utilisateurs</h1></caption>

24. <thead>

25. <tr>

26. <th>Nom</th>

27. <th>Email</th>

28. <th></th>

29. <th></th>

30. </tr>

31. </thead>

32. <tbody>

33. <tr v-for="(user, index) in userShowed">

34. <td>{{ user.name }}</td>

35. <td>{{ user.email }}</td>

36. <td>

37. <button class="fluid ui orange button" :class="{ disabled: edition }" data-tooltip="Modifier cet utilisateur" data-position="top center" @click="edit(index)">

38. <i class="edit icon"></i>

39. </button>

40. </td>

41. <td>

42. <button class="fluid ui red button" :class="{ disabled: edition }" data-tooltip="Supprimer cet utilisateur" data-position="top center" @click="del(index)">

43. <i class="remove user icon"></i>

44. </button>

45. </td>

46. </tr>

47. <tr class="ui form">

48. <td>

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

50. <input type="text" v-model="user.name" placeholder="Nom">

51. </div>

52. </td>

53. <td>

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

55. <input type="email" class="form-control" v-model="user.email" placeholder="Email">

56. </div>

57. </td>

58. <td colspan="2" v-if="!edition">

59. <button class="fluid ui blue button" data-tooltip="Ajouter un utilisateur" data-position="top center" @click="add()">

60. <i class="add user icon"></i>

61. </button>

62. </td>

63. <td v-if="edition">

64. <button class="fluid ui blue button" data-tooltip="Mettre à jour cet utilisateur" data-position="top center" @click="update()">

65. <i class="add user icon"></i>

66. </button>

67. </td>

68. <td v-if="edition">

69. <button class="fluid ui violet button" data-tooltip="Annuler la modification" data-position="top center" @click="undo()">

70. <i class="undo icon"></i>

71. </button>

72. </td>

73. </tr>

74. </tbody>

75. </table>

76. <div class="ui pagination menu" v-if="paginationEnabled">

77. <a v-for="n in pagesNumber" class="item" :class="{ active: pagination.index == n }" @click="changePage(n)">

78. {{ n }}

79. </a>

80. </div>

81. </div>

82. </template>

84. <script>

85. import Message from './Message.vue'

87. export default {

88. name: 'application',

89. resource: null,

90. data () {

91. return {

92. users: [],

93. user: { name: '', email: '' },

94. save: { index: 0, user: {} },

95. success: false,

96. danger: false,

97. edition: false,

98. validation: { name: '', email: '' },

99. pagination: { index: 1, number: 4 }

100. }

101. },

102. computed: {

103. userShowed: function() {

104. let start = this.getStartPagination()

105. return this.users.slice(start, start + this.pagination.number)

106. },

107. pagesNumber: function() {

108. return Math.ceil(this.users.length / this.pagination.number)

109. },

110. paginationEnabled: function() {

111. return this.users.length > this.pagination.number && !this.edition

112. }

113. },

114. mounted: function() {

115. this.resource = this.$resource('/users{/id}')

116. this.resource.get().then((response) => {

117. this.users = response.body

118. }, (response) => {

119. this.danger = true

120. })

121. },

122. methods: {

123. add: function() {

124. this.resetMessages()

125. this.resource.save(this.user).then((response) => {

126. this.success = true

127. this.users.push(this.user)

128. this.user = { name: '', email: '' }

129. }, (response) => {

130. this.setValidation(response)

131. });

132. },

133. update: function() {

134. this.resetMessages()

135. this.resource.update({id: this.user.id}, this.user).then((response) => {

136. this.success = true

137. this.edition = false

138. this.users.splice(this.save.index, 0, this.user)

139. this.user = { name: '', email: '' }

140. }, (response) => {

141. this.setValidation(response)

142. });

143. },

144. del: function(index) {

145. let that = this

146. index = index + this.getStartPagination()

147. this.resetMessages()

148. this.$swal({

149. title: 'Vous êtes sûr de vous ?',

150. text: "Il n'y aura aucun retour en arrière possible !",

151. type: 'warning',

152. showCancelButton: true,

153. confirmButtonColor: '#3085d6',

154. cancelButtonColor: '#d33',

155. confirmButtonText: 'Oui supprimer !',

156. cancelButtonText: 'Non, surtout pas !',

157. }).then(function() {

158. that.resource.delete({id: that.users[index].id}).then((response) => {

159. that.success = true

160. that.users.splice(index, 1)

161. }, (response) => {

162. that.danger = true

163. });

164. }).done()

165. },

166. edit: function(index) {

167. this.resetMessages()

168. this.save.index = index + this.getStartPagination()

169. this.user = this.users[this.save.index]

170. this.save.user = JSON.parse(JSON.stringify(this.user))

171. this.users.splice(this.save.index, 1)

172. this.edition = true

173. },

174. undo: function() {

175. this.users.splice(this.save.index, 0, this.save.user)

176. this.user = { name: '', email: '' }

177. this.edition = false

178. },

179. resetMessages: function() {

180. this.success = false

181. this.danger = false

182. this.closeValidation()

183. },

184. setValidation: function(response) {

185. this.validation.name = response.body.name ? response.body.name[0] : ''

186. this.validation.email = response.body.email ? response.body.email[0] : ''

187. },

188. closeSuccess: function() {

189. this.success = false

190. },

191. closeDanger: function() {

192. this.danger = false

193. },

194. closeValidation: function() {

195. this.validation = { name: '', email: ''}

196. },

197. changePage(index) {

198. this.pagination.index = index

199. },

200. getStartPagination() {

201. return (this.pagination.index - 1) * this.pagination.number

202. }

203. },

204. components: {

205. Message

206. }

207. }

208. </script>

Voyons les modifications apportées.

Au niveau du data :

1. data () {

2. return {

3. ...

4. save: { index: 0, user: {} },

5. ...

6. pagination: { index: 1, number: 4 }

7. }

8. },

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 :

1. <div class="ui pagination menu" v-if="paginationEnabled">

2. <a v-for="n in pagesNumber" class="item" :class="{ active: pagination.index == n }" @click="changePage(n)">

3. {{ n }}

4. </a>

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

1. userShowed: function() {

2. let start = this.getStartPagination()

3. return this.users.slice(start, start + this.pagination.number)

4. },

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
}

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 :

1. edit: function(index) {

2. ...

3. this.save.index = index + this.getStartPagination()   (1)

4. this.user = this.users[this.save.index]

5. ...

6. this.users.splice(this.save.index, 1)

7. ...

8. },

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 :

1. del: function(index) {

2. let that = this

3. index = index + this.getStartPagination()

4. ...

5. }).then(function() {

6. that.resource.delete({id: that.users[index].id}).then((response) => {

7. ...

8. },

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.

V - Pagination avancée

Dans le précédent chapitrechapitre, on a vu un exemple de pagination très simplifié. Je vous propose dans le présent chapitre de poursuivre l'exemple en construisant un composant de pagination digne de ce nom qui soit à la fois pratique, complet et esthétique.

On va donc partir de la situation telle qu'on l'a laissée précédemment en transformant la pagination en composant indépendant.

Je vous ai mis un zip contenant tout le code final ici.

V-A - Côté Laravel

Du côté de Laravel on va garder le même code, mais on va ajouter des utilisateurs. Dans le seeder on va en prévoir 1000 :

1. <?php

3. use Illuminate\Database\Seeder;

5. class DatabaseSeeder extends Seeder

6. {

7. /**

8. * Run the database seeds.

9. *

10. * @return void

11. */

12. public function run()

13. {

14. factory(App\User::class, 1000)->create();;

15. }

16. }

Alors régénérez votre base de données (on l'a prévue en sqlite) :

php artisan migrate:reset
php atisan migrate --seed

Vous devriez avoir ainsi 1000 utilisateurs :

V-B - Quelle pagination ?

Il existe de nombreuses façons de créer une pagination, plus ou moins judicieuses. J'aime bien cet article, même s'il est en anglais. En résumé il préconise :

- des zones cliquables confortables ;

- éviter les soulignements ;

- identifier la page active ;

- bien espacer les liens ;

- prévoir des liens « précédent » et « suivant » ;

- prévoir des liens « premier » et ‘ »dernier » séparés.

S'ensuit une galerie de bonnes et mauvaises paginations.

On va essayer dans ce tutoriel de respecter ces préconisations en y ajoutant la possibilité de déterminer le nombre d'enregistrements par page.

On va partir du principe qu'on affiche par défaut 10 enregistrements, comme on en a 1000 ça nous fait 100 pages. Au départ on affiche la page 1 et la pagination va se présenter ainsi :

On a :

- la page active (1) mise en évidence ;

- une troncature ;

- les deux dernières pages ;

- un bouton de changement de page vers le haut ;

- un bouton de saut pour 10 pages vers le haut.

Si maintenant on affiche la page 5 :

On a :

- un bouton de changement de page vers le bas ;

- deux pages avant la troncature ;

- une troncature ;

- un bouton de changement de page vers le haut ;

- un bouton de saut pour 10 pages vers le haut.

Si maintenant on affiche la page 15 :

Cette fois on a :

- un bouton de saut pour 10 pages vers le bas ;

- un bouton de changement de page vers le bas ;

- les deux premières pages ;

- une troncature ;

- la page active encadrée de deux pages vers le bas et vers le haut ;

- une troncature ;

- les dernières pages ;

- un bouton de changement de page vers le haut ;

- un bouton de saut pour 10 pages vers le haut.

Prenons un dernier exemple avec la page 96 :

Là on a :

- un bouton de saut pour 10 pages vers le bas ;

- un bouton de changement de page vers le bas ;

- les deux premières pages ;

- une troncature ;

- la page active encadrée avec deux pages vers le bas ;

- les dernières pages ;

- un bouton de changement de page vers le haut.

Je pense que vous avez compris les principes mis en œuvre. Il me semble que c'est une façon efficace de gérer une pagination.

Un dernier cas concerne la réduction sur petit support, dans ce cas le plus simple est de ne conserver que les boutons de déplacement :

Pour compléter tout ça, on va prévoir une liste déroulante pour choisir le nombre d'enregistrements par page :

V-C - Le composant principal

Voici le nouveau code du composant principal (App) :

1. <template>

2. <div class="ui raised container segment">

3. <message

4. type="positive"

5. header="Serveur mis à jour avec succès !"

6. :show="success"

7. @close="closeSuccess">

8. </message>

9. <message

10. type="negative"

11. header="Echec de la communication avec le serveur !"

12. :show="danger"

13. @close="closeDanger">

14. </message>

15. <message

16. type="negative"

17. header="Il y a des erreurs dans la validation des données saisies :"

18. :list="[ validation.name, validation.email ]"

19. :show="validation.name != '' || validation.email != ''"

20. @close="closeValidation">

21. </message>

22. <div class="ui three column grid">

23. <div class="row">

24. <div class="column"></div>

25. <div class="column"><h1>Liste des utilisateurs</h1></div>

26. <div class="column right aligned" v-show="!edition">

27. <select v-model="paginationSelect" class="ui menu dropdown">

28. <option value="5">5 lignes</option>

29. <option value="10">10 lignes</option>

30. <option value="20">20 lignes</option>

31. <option value="50">50 lignes</option>

32. </select>

33. </div>

34. </div>

35. <div class="row" v-if="users.length == 0" style="height: 100px">

36. <div class="ui active inverted centered dimmer">

37. <div class="ui text loader">Chargement</div>

38. </div>

39. <p></p>

40. </div>

41. </div>

42. <table class="ui celled table" v-if="users.length > 0">

43. <thead>

44. <tr>

45. <th>Nom</th>

46. <th>Email</th>

47. <th></th>

48. <th></th>

49. </tr>

50. </thead>

51. <tbody>

52. <tr v-for="(user, index) in userShowed">

53. <td>{{ user.name }}</td>

54. <td>{{ user.email }}</td>

55. <td>

56. <button class="fluid ui orange button" :class="{ disabled: edition }" data-tooltip="Modifier cet utilisateur" data-position="top center" @click="edit(index)">

57. <i class="edit icon"></i>

58. </button>

59. </td>

60. <td>

61. <button class="fluid ui red button" :class="{ disabled: edition }" data-tooltip="Supprimer cet utilisateur" data-position="top center" @click="del(index)">

62. <i class="remove user icon"></i>

63. </button>

64. </td>

65. </tr>

66. <tr class="ui form">

67. <td>

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

69. <input type="text" v-model="user.name" placeholder="Nom">

70. </div>

71. </td>

72. <td>

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

74. <input type="email" class="form-control" v-model="user.email" placeholder="Email">

75. </div>

76. </td>

77. <td colspan="2" v-if="!edition">

78. <button class="fluid ui blue button" data-tooltip="Ajouter un utilisateur" data-position="top center" @click="add()">

79. <i class="add user icon"></i>

80. </button>

81. </td>

82. <td v-if="edition">

83. <button class="fluid ui blue button" data-tooltip="Mettre à jour cet utilisateur" data-position="top center" @click="update()">

84. <i class="add user icon"></i>

85. </button>

86. </td>

87. <td v-if="edition">

88. <button class="fluid ui violet button" data-tooltip="Annuler la modification" data-position="top center" @click="undo()">

89. <i class="undo icon"></i>

90. </button>

91. </td>

92. </tr>

93. </tbody>

94. </table>

95. <pagination :records="maxUsers" :number-per-page="paginationNumber" :hide="edition" @changepage="changePage"></pagination>

96. </div>

97. </template>

99. <script>

100. import Message from './Message.vue'

101. import Pagination from './Pagination.vue'

103. export default {

104. name: 'application',

105. resource: null,

106. data() {

107. return {

108. users: [],

109. userShowed: [],

110. user: { name: '', email: '' },

111. save: { index: 0, user: {} },

112. success: false,

113. danger: false,

114. edition: false,

115. validation: { name: '', email: '' },

116. paginationIndex: 1,

117. paginationNumber: 10,

118. paginationSelect: 10

119. }

120. },

121. computed: {

122. maxUsers() {

123. return this.users.length

124. }

125. },

126. watch: {

127. paginationSelect() {

128. this.paginationNumber = _.toInteger(this.paginationSelect)

129. this.paginationIndex = 1

130. this.refreshPage()

131. }

132. },

133. mounted() {

134. this.resource = this.$resource('/users{/id}')

135. this.resource.get().then((response) => {

136. this.users = response.body

137. this.userShowed = this.users.slice(0, this.paginationNumber)

138. }, (response) => {

139. this.danger = true

140. })

141. },

142. methods: {

143. add() {

144. this.resetMessages()

145. this.resource.save(this.user).then((response) => {

146. this.success = true

147. this.users.push(this.user)

148. this.user = { name: '', email: '' }

149. }, (response) => {

150. this.setValidation(response)

151. });

152. },

153. update() {

154. this.resetMessages()

155. this.resource.update({id: this.user.id}, this.user).then((response) => {

156. this.success = true

157. this.userShowed.splice(this.save.index, 0, this.user)

158. this.users[this.save.index + this.getStartPagination] = _.clone(this.user)

159. this.edition = false

160. this.user = { name: '', email: '' }

161. }, (response) => {

162. this.setValidation(response)

163. });

164. },

165. del(index) {

166. let that = this

167. index = index + this.getStartPagination()

168. this.resetMessages()

169. this.$swal({

170. title: 'Vous êtes sûr de vous ?',

171. text: "Il n'y aura aucun retour en arrière possible !",

172. type: 'warning',

173. showCancelButton: true,

174. confirmButtonColor: '#3085d6',

175. cancelButtonColor: '#d33',

176. confirmButtonText: 'Oui supprimer !',

177. cancelButtonText: 'Non, surtout pas !',

178. }).then(function() {

179. that.resource.delete({id: that.users[index].id}).then((response) => {

180. that.success = true

181. that.users.splice(index, 1)

182. that.changePage(1)

183. }, (response) => {

184. that.danger = true

185. });

186. }).done()

187. },

188. edit(index) {

189. this.resetMessages()

190. this.save.index = index

191. this.user = this.userShowed[index]

192. this.save.user = _.clone(this.user)

193. this.edition = true

194. this.userShowed.splice(index, 1)

195. },

196. undo() {

197. this.userShowed.splice(this.save.index, 0, this.save.user)

198. this.user = { name: '', email: '' }

199. this.edition = false

200. },

201. resetMessages() {

202. this.success = false

203. this.danger = false

204. this.closeValidation()

205. },

206. setValidation(response) {

207. this.validation.name = response.body.name ? response.body.name[0] : ''

208. this.validation.email = response.body.email ? response.body.email[0] : ''

209. },

210. closeSuccess() {

211. this.success = false

212. },

213. closeDanger() {

214. this.danger = false

215. },

216. closeValidation() {

217. this.validation = { name: '', email: ''}

218. },

219. changePage(index) {

220. this.paginationIndex = index

221. this.refreshPage()

222. },

223. refreshPage() {

224. let start = this.getStartPagination()

225. this.userShowed = this.users.slice(start, start + this.paginationNumber)

226. },

227. getStartPagination() {

228. return (this.paginationIndex - 1) * this.paginationNumber

229. }

230. },

231. components: {

232. Message,

233. Pagination

234. }

235. }

236. </script>

On retrouve l'essentiel de ce qu'on avait déjà mis en place, mais avec quelques modifications…

V-C-1 - La liste déroulante

Dans le template, on a ajouté la liste déroulante pour le nombre d'enregistrements par page :

1. <div class="column right aligned" v-show="!edition">

2. <select v-model="paginationSelect" class="ui menu dropdown">

3. <option value="5">5 lignes</option>

4. <option value="10">10 lignes</option>

5. <option value="20">20 lignes</option>

6. <option value="50">50 lignes</option>

7. </select>

8. </div>

On cache la liste en mode édition avec la directive v-show.

On voit une liaison de données avec la directive v-model. On va retrouver la valeur dans le data avec une valeur par défaut de 10 :

1. data() {

2. return {

3. ...

4. paginationSelect: 10

5. }

6. },

On surveille les changements dans la liste avec une propriété dont je ne vous ai pas encore parlé (watch) :

1. watch: {

2. paginationSelect() {

3. this.paginationNumber = _.toInteger(this.paginationSelect)

4. this.paginationIndex = 1

5. this.refreshPage()

6. }

7. },

Avec Vue.js il y a deux façons de réagir à un changement de valeur : les propriétés calculées (computed properties) et les observateurs (watchers). Selon les cas, il est plus judicieux d'utiliser l'un ou l'autre. Les observateurs sont intéressants quand on veut accomplir une action spécifique comme c'est le cas ici. Quand la valeur change dans la liste déroulante :

- on transforme la valeur en integer (dans la liste on a des chaînes de caractères) ;

- on réinitialise la page actuelle à 1 ;

- on rafraîchit l'affichage.

Remarquez que j'utilise Lodash parce qu'il est déjà par défaut chargé par Elixir, alors autant s'en servir !

V-C-2 - L'intégration de la pagination

Dans le template, on va évidemment aussi trouver le composant de la pagination :

<pagination :records="maxUsers" :number-per-page="paginationNumber" :hide="edition" @changepage="changePage"></pagination>

Et on le déclare dans le JavaScript :

1. components: {

2. ...

3. Pagination

4. }

On voit qu'on va transmettre des valeurs pour trois propriétés :

- records : le nombre total d'enregistrements donné par la propriété calculée maxUsers ;

- number-per-page : le nombre d'enregistrements par page donné par la liste déroulante ;

- hide : l'effacement de la pagination qui servira lorsqu'on veut éditer un utilisateur.

D'autre part on écoute l'événement changePage qui va nous indiquer le changement de page. En cas de changement de page, on actualise l'index et on rafraîchit la page :

1. changePage(index) {

2. this.paginationIndex = index

3. this.refreshPage()

4. },

Cet index nous est nécessaire pour repérer l'utilisateur pour les actions de suppression et édition. On peut en effet déterminer où commence la page actuelle :

getStartPagination() {
return (this.paginationIndex - 1) * this.paginationNumber
}

Le rafraîchissement de la page se fait avec cette fonction :

1. refreshPage() {

2. let start = this.getStartPagination()

3. this.userShowed = this.users.slice(start, start + this.paginationNumber)

4. },

Avec ça on est parés au niveau du composant principal…

V-D - Le composant de pagination

c'est dans le composant de pagination que se trouve toute la logique correspondante ainsi que le code HTML et CSS. Voici le code complet du composant :

1. <template>

2. <div id="pagination" v-if="paginationEnabled">

3. <div class="ui pagination menu" v-show="chunkLeft" @click="bigLeft">

4. <a class="item"><i class="angle double left icon"></i></a>

5. </div>

6. <div class="ui pagination menu" v-show="buttonLeft" @click="left">

7. <a class="item"><i class="angle left icon"></i></a>

8. </div>

9. <div id="numbers" class="ui pagination menu">

10. <a v-for="(item, index) in pagination" class="item" :class="{ active: item.active, disabled: item.disabled }" @click="changePage(index)">

11. {{ item.text }}

12. </a>

13. </div>

14. <div class="ui pagination menu" v-show="buttonRight" @click="right">

15. <a class="item"><i class="angle right icon"></i></a>

16. </div>

17. <div class="ui pagination menu" v-show="chunkRight" @click="bigRight">

18. <a class="item"><i class="angle double right icon"></i></a>

19. </div>

20. </div>

21. </template>

23. <script>

24. export default {

25. name: 'pagination',

26. props: {

27. 'records': {

28. type: Number,

29. required: true

30. },

31. 'numberPerPage': {

32. type: Number,

33. required: false,

34. default: 10

35. },

36. 'chunk': {

37. type: Number,

38. required: false,

39. default: 10

40. },

41. 'hide': {

42. type: Boolean,

43. required: false,

44. default: false

45. }

46. },

47. data() {

48. return {

49. current: 1,

50. adjacent: 2,

51. pagination: []

52. }

53. },

54. computed: {

55. paginationEnabled() {

56. return this.records > this.numberPerPage && !this.hide

57. },

58. buttonLeft() {

59. return this.current > 1

60. },

61. buttonRight() {

62. return this.current < this.totalPages()

63. },

64. chunkLeft() {

65. return this.current >= this.chunk

66. },

67. chunkRight() {

68. return this.current <= this.totalPages() - this.chunk

69. }

70. },

71. watch: {

72. numberPerPage() {

73. this.current = 1

74. this.createPagination()

75. },

76. records() {

77. this.current = 1

78. this.createPagination()

79. },

80. current() {

81. this.createPagination()

82. }

83. },

84. methods: {

85. changePage(index) {

86. if(!this.pagination[index].disabled) {

87. this.current = this.pagination[index].text

88. this.$emit('changepage', this.current)

89. }

90. },

91. createPagination() {

92. let total = this.totalPages()

93. this.pagination = []

94. let encadrement = this.adjacent * 2

95. // Sans troncature

96. if(total < 7 + encadrement) {

97. this.addPages(..._.range(1, total + 1))

98. // Avec troncature

99. } else {

100. // Troncature à droite

101. if (this.current < 2 + encadrement) {

102. this.addPages(..._.range(1, 4 + encadrement))

103. this.addTroncature()

104. this.addLastPages(total)

105. }

106. // Deux troncatures

107. else if ((encadrement + 1 < this.current) && (this.current < total - encadrement)) {

108. this.addFirstPages()

109. this.addTroncature()

110. this.addPages(..._.range(this.current - this.adjacent, this.current + this.adjacent + 1))

111. this.addTroncature()

112. this.addLastPages(total)

113. }

114. // Troncature à gauche

115. else {

116. this.addFirstPages()

117. this.addTroncature()

118. this.addPages(..._.range(total - 2 - encadrement, total + 1))

119. }

120. }

121. },

122. addTroncature() {

123. this.pagination.push({ text: '...', active: false, disabled: true })

124. },

125. addFirstPages() {

126. this.addPages(1, 2)

127. },

128. addLastPages(total) {

129. this.addPages(total - 1, total)

130. },

131. addPages(...valeurs) {

132. let that = this

133. _.forEach(valeurs, function(valeur) {

134. that.pagination.push({ text: valeur, active: that.current == valeur, disabled: false })

135. });

136. },

137. totalPages() {

138. return _.ceil(this.records / this.numberPerPage)

139. },

140. left() {

141. this.$emit('changepage', --this.current)

142. },

143. right() {

144. this.$emit('changepage', ++this.current)

145. },

146. bigLeft() {

147. this.current -= this.chunk

148. this.$emit('changepage', this.current)

149. },

150. bigRight() {

151. this.current += this.chunk

152. this.$emit('changepage', this.current)

153. }

154. }

155. }

156. </script>

158. <style>

159. .ui.pagination.menu .item {

160. background-color: rgba(33,133,208,.2);

161. }

162. .ui.pagination.menu .active.item {

163. background-color: rgba(33,133,208,.5);

164. }

165. @media screen and (max-width: 768px) {

166. #numbers { display: none; }

167. }

168. #pagination {

169. display:flex;

170. justify-content:center

171. }

172. </style>

V-D-1 - Les propriétés

Pour faire les choses correctement, j'ai prévu une validation au niveau des propriétés (props) :

1. props: {

2. 'records': {

3. type: Number,

4. required: true

5. },

6. 'numberPerPage': {

7. type: Number,

8. required: false,

9. default: 10

10. },

11. 'chunk': {

12. type: Number,

13. required: false,

14. default: 10

15. },

16. 'hide': {

17. type: Boolean,

18. required: false,

19. default: false

20. }

21. },

On contrôle le type de donnée, le fait que la valeur soit obligatoire ou pas et on prévoit une valeur par défaut le cas échéant.

J'ai prévu une propriété optionnelle chunk qui correspond au déplacement de plusieurs pages en avant ou en arrière avec une valeur par défaut de 10.

V-D-2 - Le template et les boutons

Au niveau du template, on a déjà une condition globale :

<div id="pagination" v-if="paginationEnabled">

La directive v-if permet d'effacer la pagination dans deux cas :

1. computed: {

2. paginationEnabled() {

3. return this.records > this.numberPerPage && !this.hide

4. },

- si le nombre d'enregistrements est inférieur ou égal au nombre d'enregistrements par page ;

- si on est en mode édition.

Pour les boutons de déplacement, on a une logique simple. Si on prend par exemple celui d'une page à gauche :

<div class="ui pagination menu" v-show="buttonLeft" @click="left">
<a class="item"><i class="angle left icon"></i></a>
</div>

Une directive v-show permet de le faire apparaître uniquement quand c'est nécessaire :

buttonLeft() {
return this.current > 1
},

Très logiquement c'est quand on en est au moins à la page 2. D'autre part si on clique sur le bouton on a un événement left :

left() {
this.$emit('changepage', --this.current)
},

On émet l'événement changePage pour le composant parent en lui transmettant le nouvel index.

Pour les boutons des pages on a une simple directive v-for :

<a v-for="(item, index) in pagination" class="item" :class="{ active: item.active, disabled: item.disabled }" @click="changePage(index)">
{{ item.text }}
</a>

V-D-3 - La logique

On a ces données :

1. data() {

2. return {

3. current: 1,

4. adjacent: 2,

5. pagination: []

6. }

7. },

- la page courante current ;

- le nombre de pages adjacentes à afficher adjacent fixée à 2 ;

- un tableau contenant les informations pour les boutons des pages pagination.

Le tableau pagination est rempli avec la méthode createPagination aidée par quelques autres méthodes secondaires :

1. createPagination() {

2. let total = this.totalPages()

3. this.pagination = []

4. let encadrement = this.adjacent * 2

5. // Sans troncature

6. if(total < 7 + encadrement) {

7. this.addPages(..._.range(1, total + 1))

8. // Avec troncature

9. } else {

10. // Troncature à droite

11. if (this.current < 2 + encadrement) {

12. this.addPages(..._.range(1, 4 + encadrement))

13. this.addTroncature()

14. this.addLastPages(total)

15. }

16. // Deux troncatures

17. else if ((encadrement + 1 < this.current) && (this.current < total - encadrement)) {

18. this.addFirstPages()

19. this.addTroncature()

20. this.addPages(..._.range(this.current - this.adjacent, this.current + this.adjacent + 1))

21. this.addTroncature()

22. this.addLastPages(total)

23. }

24. // Troncature à gauche

25. else {

26. this.addFirstPages()

27. this.addTroncature()

28. this.addPages(..._.range(total - 2 - encadrement, total + 1))

29. }

30. }

31. },

32. addTroncature() {

33. this.pagination.push({ text: '...', active: false, disabled: true })

34. },

35. addFirstPages() {

36. this.addPages(1, 2)

37. },

38. addLastPages(total) {

39. this.addPages(total - 1, total)

40. },

41. addPages(...valeurs) {

42. let that = this

43. _.forEach(valeurs, function(valeur) {

44. that.pagination.push({ text: valeur, active: that.current == valeur, disabled: false })

45. });

46. },

47. totalPages() {

48. return _.ceil(this.records / this.numberPerPage)

49. },

C'est la principale méthode du composant. Je ne vais pas entrer dans le détail du fonctionnement, j'ai ajouté quelques commentaires pour s'y retrouver.

J'ai profité de la compilation en ES6 pour utiliser le paramètre du reste et l'opérateur de décomposition qui rendent la syntaxe concise, surtout associée à Lodash.

V-D-4 - Un peu d'observation

J'ai aussi prévu d'observer (watch) tous les changements qui justifient un recalcul de la pagination :

1. watch: {

2. numberPerPage() {

3. this.current = 1

4. this.createPagination()

5. },

6. records() {

7. this.current = 1

8. this.createPagination()

9. },

10. current() {

11. this.createPagination()

12. }

13. },

- si le nombre d'enregistrements par page (numberPerPage) change ;

- si le nombre total d'enregistrements (records) change ;

- si la page courante (current) change.

V-D-5 - Le style

J'ai aussi prévu un peu de style :

1. <style>

2. .ui.pagination.menu .item {

3. background-color: rgba(33,133,208,.2);

4. }

5. .ui.pagination.menu .active.item {

6. background-color: rgba(33,133,208,.5);

7. }

8. @media screen and (max-width: 768px) {

9. #numbers { display: none; }

10. }

11. #pagination {

12. display:flex;

13. justify-content:center

14. }

15. </style>

On a ainsi un peu de couleur, un petit effet responsive et un centrage.

V-E - Conclusion

On a ainsi une application bien organisée avec deux composants réutilisables : un pour les messages et l'autre pour la pagination.