INTRODUCTION AU FRAMEWORK VUE.JS PAR L’EXEMPLE

Auteur

Serge Tahé, octobre 2019, https://sergetahe.com

Téléchargements

Téléchargement du PDF du cours

Téléchargement des exemples du cours (rar)

Présentation du cours

Ce document fait partie d’une série de quatre articles :

  1. [Introduction au langage PHP7 par l’exemple] ;
  2. [Introduction au langage ECMASCRIPT 6 par l’exemple] ;
  3. [Introduction au framework VUE.JS par l’exemple]. C’est le document présent ;
  4. [Introduction au framework NUXT.JS par l’exemple] ;

Ce sont tous des documents pour débutants. Les articles ont une suite logique mais sont faiblement couplés :

  • le document [1] présente le langage PHP 7. Le lecteur seulement intéressé par le langage PHP et pas par le langage Javascript des articles suivants s’arrêtera là ;
  • les documents [2-4] visent tous à construire un client Javascript au serveur de calcul de l’impôt développé dans le document [1] ;
  • les frameworks Javascript [vue.js] et [nuxt.js] des articles 3 et 4 nécessitent de connaître le Javascript des dernières versions d’ECMASCRIPT, celles de la version 6. Le document [2] est donc destiné à ceux qui ne connaissent pas cette version de JavaScript. Il fait référence au serveur de calcul de l’impôt construit dans le document [1]. Le lecteur de [2] aura alors parfois besoin de se référer au document [1] ;
  • une fois ECMASCRIPT 6 maîtrisé, on peut aborder le framework VUE.JS qui permet de construire des clients Javascript s’exécutant dans un navigateur en mode SPA (Single Page Application). C’est le document [3]. Il fait référence à la fois au serveur de calcul de l’impôt construit dans le document [1] et au code du client JavaScript autonome construit en [2]. Le lecteur de [3] aura alors parfois besoin de se référer aux documents [1] et [2] ;
  • une fois VUE.JS maîtrisé, on peut aborder le framework NUXT.JS qui permet de construire des clients Javascript s’exécutant dans un navigateur en mode SSR (Server Side Rendered). Il fait référence à la fois au serveur de calcul de l’impôt construit dans le document [1], au code du client Javascript autonome construit en [2] ainsi qu’à l’application [vue.js] développée dans le document [3]. Le lecteur de [4] aura alors parfois besoin de se référer aux documents [1] [2] et [3] ;

La dernière version du serveur de calcul de l’impôt développée dans le document [1] (cf document |https://tahe.developpez.com/tutoriels-cours/php7|) peut être améliorée de diverses manières :

  • la version écrite est centrée sur le serveur. La tendance est désormais (sept 2019) au client / serveur :
    • le serveur fonctionne en service jSON ;
    • une page statique ou non est le point d’entrée de l’application web. Cette page contient du HTML /CSS mais aussi du JavaScript ;
    • les autres pages de l’application web sont obtenues dynamiquement par le JavaScript :
      • la page HTML peut être obtenue par assemblage de fragments statiques ou non, fournis par le même serveur qui a fourni la page d’entrée, ou bien construites par le Javascript du client ;
      • ces différentes pages affichent des données qui sont demandées au service jSON ;

Ainsi le travail est réparti sur le client et le serveur. Le serveur ainsi déchargé peut servir davantage d’utilisateurs.

L’architecture correspondant à ce modèle est le suivant :

image0

JS : JavaScript

Le navigateur est client :

  • d’un service de pages ou fragments statiques ou non (non représenté ci-dessus) ;
  • d’un service de données jSON ;

Le code JavaScript est donc un client jSON et à ce titre peut être organisé en couches [UI, métier, dao] (UI : User Interface) comme l’ont été nos clients jSON écrits en PHP. Au final, le navigateur ne charge qu’une unique page, la page d’accueil. Toutes les autres sont obtenues et construites par le JavaScript. On appelle ce type d’application SPA : Single Page Application ou encore APU : Application à Page Unique.

Ce type d’application fait également partie des applications dites AJAX : Asynchronous Javascript And XM :

  • Asynchronous : parce que les appels du client Javascript au serveur jSON sont asynchrones ;
  • XML : parce que XML était la technologie utilisée avant l’avènement du jSON. On a cependant gardé l’acronyme AJAX ;

Nous allons étudier une telle architecture dans ce document. Côté client, nous utiliserons le framework Javascript [Vue.js] [https://vuejs.org/] pour écrire le client Javascript du serveur jSON PHP que nous avons écrit dans le document [1].

[Vue.js] est un framework JavaScript. Nous avons présenté le langage Javascript dans le document [2]. Nous ne ferons pas une étude exhaustive du framework [vue.js]. Nous nous contenterons de présenter les concepts qui seront utilisés ensuite dans l’écriture d’un client [Vue.js] pour la version jSON de la version 14 du serveur de calcul de l’impôt (cf |https://tahe.developpez.com/tutoriels-cours/php7|).

Les scripts de ce document sont commentés et leur exécution console reproduite. Des explications supplémentaires sont parfois fournies. Le document nécessite une lecture active : pour comprendre un script, il faut à la fois lire son code, ses commentaires et ses résultats d’exécution.

Les exemples du document sont disponibles |ici|.

L’application serveur PHP 7 peut être testée |ici|.

Serge Tahé, octobre 2019

Création d’un environnement de travail

Nous reprenons l’environnement de travail détaillé dans le document [2] :

  • Visual Studio Code (VSCode) pour écrire les codes JavaScript ;
  • [node.js] pour les exécuter ;
  • [npm] pour télécharger et installer les bibliothèques JavaScript dont nous aurons besoin ;

Nous créons un environnement de travail dans [VSCode] :

image0

  • en [1-5], nous ouvrons un dossier [vuejs] vide dans [VSCode] ;

image1

  • en [8-10], on installe la dépendance [@vue/cli] qui va nous permettre d’initialiser un projet [vue.js]. Cette dépendance amène un grand nombre de packages (plusieurs centaines) ;

Dans le même terminal, on tape ensuite la commande [vue create .] qui demande à créer un projet [vue.js] dans le dossier courant (.) :

image2

  • en [13], commence une série de questions qui servent à configurer le projet ;

image3

  • une fois toutes les questions répondues, de nouveaux packages sont téléchargés et un projet généré dans le dossier courant [14].

Regardons ce qui a été généré. Le fichier [package.json] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 {
   "name": "vuejs",
   "version": "0.1.0",
   "private": true,
   "scripts": {
     "serve": "vue-cli-service serve vuejs-20/main.js",
     "build": "vue-cli-service build vuejs-20/main.js",
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
     "axios": "^0.19.0",
     "bootstrap": "^4.3.1",
     "bootstrap-vue": "^2.0.2",
     "core-js": "^2.6.5",
     "vue": "^2.6.10",
     "vue-router": "^3.1.3",
     "vuex": "^3.1.1"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^3.11.0",
     "@vue/cli-plugin-eslint": "^3.11.0",
     "@vue/cli-service": "^3.11.0",
     "babel-eslint": "^10.0.1",
     "eslint": "^5.16.0",
     "eslint-plugin-vue": "^5.0.0",
     "vue-template-compiler": "^2.6.10"
   },
   "eslintConfig": {
     "root": true,
     "env": {
       "node": true
     },
     "extends": [
       "plugin:vue/essential",
       "eslint:recommended"
     ],
     "rules": {},
     "parserOptions": {
       "parser": "babel-eslint"
     }
   },
   "postcss": {
     "plugins": {
       "autoprefixer": {}
     }
   },
   "browserslist": [
     "> 1%",
     "last 2 versions"
   ]
 }

Commentaires

  • lignes 14-22 : dans les dépendances nécessaires au développement on voit des références aux deux outils [eslint, babel] déjà utilisés dans les deux chapitres précédents. S’y ajoutent des plugins de ces deux outils destinés à leur utilisation au sein de [vue.js] ;
  • ligne 34 : c’est le package [babel-eslint] qui opèrera la transpilation ES6 -> ES5 des codes jS ;
  • lignes 5-9 : trois tâches [npm] ont été créées :
    • [build] : sert à construire la version compilée du projet prête à entrer en production ;
    • [serve] : exécute le projet sur un serveur web. C’est avec cet outil que sont faits les tests lors du développement. Comme avec [webpack-dev-server], une modification d’un code source du projet provoque automatiquement la recompilation du projet et son rechargement par le serveur web ;
    • [lint] : sert à analyser les codes jS et délivre des rapports. Nous n’utiliserons pas cet outil ici ;

Un fichier [README.md] a été généré avec le contenu suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 # vuejs

 ## Project setup
 ```
 npm install
 ```

 ### Compiles and hot-reloads for development
 ```
 npm run serve
 ```

 ### Compiles and minifies for production
 ```
 npm run build
 ```

 ### Run your tests
 ```
 npm run test
 ```

 ### Lints and fixes files
 ```
 npm run lint
 ```

 ### Customize configuration
 See [Configuration Reference](https://cli.vuejs.org/config/).

Ce fichier résume les commandes à utiliser pour gérer le projet.

Nous savons que dans [VSCode], les tâches [npm] sont proposées à l’exécution :

image4

  • en [1-3], nous exécutons la commande [serve] qui va compiler, puis exécuter le projet [4-5] ;

A l’URL [http://localhost:8080], nous obtenons la page suivante :

image5

Nous expliquerons un peu plus loin ce qui a amené à cette page.

Continuons à configurer notre environnement de travail :

image6

  • en [2] ci-dessus, nous voyons des indicateurs [git]. [git] est un gestionnaire de code source permettant de gérer des versions successives de celui-ci et de les partager entre développeurs. Nous allons désactiver cet outil pour le projet ;
  • en [3-5], nous allons dans les propriétés du projet ;

image7

  • en [9-10], on désactive l’utilisation de [git] dans le projet ;

Nous allons écrire divers tests pour montrer le fonctionnement de [vue.js]. Nous ne voulons cependant pas créer à chaque fois un nouveau projet car il faudrait alors à chaque fois générer un dossier [node_modules] alors que celui-ci fait plusieurs centaines de méga-octets. Revenons sur les tâches [npm] du fichier [package.json] :

1
2
3
4
5
   "scripts": {
     "serve": "vue-cli-service serve vuejs-00/main.js",
     "build": "vue-cli-service build vuejs-00/main.js",
     "lint": "vue-cli-service lint"
   },
  • ligne 2 : la commande [serve] utilise par défaut :
    • le fichier [public/index.html] ;
    • associé au fichier [src/main.js] ;

Ligne 2, il est possible de préciser à la commande [serve], le point d’entrée du projet, par exemple :

1
 "serve": "vue-cli-service serve vuejs-00/main.js",

Essayons :

image8

  • en [1], le dossier [src] a été renommé en [vuejs-00] ;
  • en [2-3], on a modifié la commande [serve] ;
  • en [4-6], on exécute le projet ;

On obtient le même résultat que précédemment :

image9

Pour nos tests, nous procéderons donc ainsi :

  • écriture de code dans un dossier [vuejs-xx] du projet ;
  • test de ce projet avec la commande [vue-cli-service serve vuejs-xx/main.js] dans le fichier [package.json] ;

Lorsque le serveur de développement est lancé, toute modification d’un des fichiers du projet provoque une recompilation. Pour cette raison, nous inhibons le mode [Auto Save] de [VSCode]. En effet, nous ne voulons pas de recompilation dès qu’on tape des caractères dans un des fichiers du projet. Nous ne voulons de recompilation qu’à certains moments :

image10

  • en [2], l’option [Auto Save] ne doit pas être cochée ;

projet [vuejs-01] : les bases

Pour expliquer le code exécuté dans [vuejs-00] nous allons le simplifier dans [vuejs-01]. Nous dupliquons le dossier [vuejs-00] dans [vuejs-01] :

image0

Le projet [vuejs-01] comprend essentiellement quatre fichiers :

  • [main.js] [2] est le point d’entrée du projet ;
  • [App.vue, HelloWorld.vue] [3-4] sont des composants [Vue.js], comprenant de façon facultative les éléments suivants :
    • [<template>…</template>] : du code HTML ;
    • [<script>…</script>] : le code Javascript associé au code HTML ;
    • [<style>…</style>] : le style CSS associé au code HTML ;
  • [public/index.html] [5] : le document HTML visualisé par la commande [npm run serve] ;

Le fichier [public/index.html] affiché à l’exécution du projet est celui-ci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <!DOCTYPE html>
 <html lang="en">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <title>vuejs</title>
   </head>
   <body>
     <noscript>
       <strong>We're sorry but vuejs doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
     </noscript>
     <div id="app"></div>
     <!-- built files will be auto injected -->
   </body>
 </html>

Ce fichier HTML n’affiche donc rien statiquement. Il n’y a ici pas de code HTML. L’affichage est dynamique : le code jS du projet va générer du HTML qui va remplacer entièrement la balise [<div id=’app’>] de la ligne 14. Le code HTML généré par le code jS du projet et inséré à la place de la balise [<div>] de la ligne 14 provient des balises [template] des composants [vue.js], les fichiers ayant le suffixe [.vue].

Le code HTML est inséré dynamiquement ligne 14 par le script [vuejs-01/main.js] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   render: h => h(App),
 }).$mount('#app')

Commentaires

  • ligne 2 : l’objet [Vue] est fourni par le framework [vue.js] ;
  • ligne 3 : l’objet [App] est fourni par le fichier [vuejs-01/App.vue] ;
  • ligne 6 : configuration de l’objet [Vue] ;
  • lignes 9-11 : ce sont les lignes qui :
    • génèrent le code HTML de l’application. Ligne 10, c’est le fichier [App.vue] qui le génère ;
    • chargent le code HTML généré ligne 10 dans la section [<div id=’app’></div>] du fichier [public/index.html] ;

Tout projet [Vue.js] peut conserver le fichier [index.html] tel quel.

Le fichier [App.vue] du projet initial [vuejs-00] est simplifié de la façon suivante dans le projet [vuejs-01] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <template>
   <div id="myApp">
     <img alt="Vue logo" src="./assets/logo.png" />
     <HelloWorld msg="Notre première application Vue.js" />
   </div>
 </template>

 <script>
 import HelloWorld from "./components/HelloWorld.vue";

 export default {
   name: "app",
   components: {
     HelloWorld
   }
 };
 </script>

Commentaires

  • un fragment [.vue] comprend au plus trois sections :
    • [<template>…</template>] : du code HTML ;
    • [<script>…</script>] : le code Javascript associé au code HTML ;
    • [<style>…</style>] : le style CSS associé au code HTML ;
Ici, nous n’avons pas de section [style].
  • lignes 1-6 : le code HTML du fragment (page, composant, vue, …) ;
  • lignes 2-5 : la section [template] ne peut contenir qu’un élément. On met en général une section [div] qui englobe tout le HTML du fragment. On peut mettre également une balise <template> ;
  • ligne 3 : une image ;

image1

  • ligne 4 : un composant nommé [HelloWorld]. Le principe de [Vue.js] est de construire des pages web à l’aide de fragments définis dans des fichiers [.vue] comme ici [App.vue]. Ce composant est défini par le fichier [HelloWorld.vue] défini ligne 9 du script jS associé ;
  • ligne 4 : un composant peut accepter des paramètres. Le paramètre est ici l’attribut [msg] ;
  • lignes 8-17 : le script jS du fragment (ou composant) ;
  • ligne 9 : pour pouvoir utiliser le composant [HelloWorld] dans le composant [App], il faut importer sa définition dans la partie [script] ;
  • lignes 11-16 : le script définit un objet et l’exporte afin de le rendre disponible à l’extérieur ;
  • ligne 12 : l’attribut [name] : définit le nom du composant exporté ;
  • lignes 13-15 : l’attribut [components] liste les composants utilisés par le composant [App]. Ils sont exportés avec lui ;

Ligne 9, il n’y a pas obligation que le composant [HelloWorld] porte le même nom que le fichier qui le définit. On pourrait l’importer en tant que [X] et l’exporter en tant que composant [Bonjour] :

image2

  • ligne 14 : le composant [X] est exporté sous le nom [Bonjour]. Il est alors utilisé sous ce nom, ligne 4 ;

La première version est la version la plus courante, aussi définirons-nous nos composants de cette façon ;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <template>
   <div id="myApp">
     <img alt="Vue logo" src="./assets/logo.png" />
     <HelloWorld msg="Notre première application Vue.js" />
   </div>
 </template>

 <script>
 import HelloWorld from "./components/HelloWorld.vue";

 export default {
   name: "app",
   components: {
     HelloWorld
   }
 };
 </script>

La ligne 14 est un raccourci pour le code [HelloWorld : HelloWorld] : le composant [HelloWorld] (à droite, importé ligne 9) est exporté sous le nom [HelloWorld] (à gauche).

image3

Nous simplifions le composant [HelloWorld.vue] de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 <template>
   <div>
     <h1>{{ msg }}</h1>
   </div>
 </template>

 <script>
 export default {
   name: "HelloWorld",
   props: {
     msg: String
   }
 };
 </script>

Commentaires

  • le composant [HelloWorld] a la même structure de fichier que le composant principal [App] ;
  • ligne 3 : on a ici une évaluation d’expression Javascript, ici l’expression [msg] ;
  • lignes 10-12 : définissent les propriétés du composant, plus exactement ses paramètres. Lorsque le composant [App] a instancié un composant [HelloWorld], il l’a fait avec la syntaxe suivante :
1
 <HelloWorld msg="Notre première application Vue.js" />
Le composant [HelloWorld] est instancié en donnant une valeur au paramètre (attribut) [msg]. Si on suit le [template] du composant [HelloWorld], celui-ci devient :
1
2
3
 <div>
     <h1>Notre première application Vue.js</h1>
 </div>
  • lignes 7-14 : les propriétés du composant définies sous la forme d’un objet qui est exporté ;
    • ligne 9 : le composant est exporté sous le nom [HelloWorld] ;
    • lignes 10-12 : ses paramètres sont définis par la propriété [props] ;

Au final, si on rassemble les templates des deux composants [App, HelloWorld] utilisés, le fichier [index.html] affiché sera le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 <!DOCTYPE html>
 <html lang="en">
 <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width,initial-scale=1.0">
   <link rel="icon" href="<%= BASE_URL %>favicon.ico">
   <title>vuejs</title>
 </head>
 <body>
   <noscript>
     <strong>We're sorry but vuejs doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
   </noscript>
   <div id="myApp">
     <img alt="Vue logo" src="./assets/logo.png" />
     <div>
       <h1>Notre première application Vue.js</h1>
     </div>
   </div></body>
 </html>

Nous lançons l’application en modifiant la commande [serve] [1] du fichier [package.json] :

image4

La page affichée est alors [2].

Maintenant regardons le code de cette page :

image5

  • en [1], faire [clic droit] ;
  • en [2], le code source de la page. On voit que c’est le code du fichier initial [index.html] et ce n’est pas ça qui a été affiché. C’est bien la page [index.html] qui a été chargée initialement. Ensuite, dynamiquement, du code Javascript a modifié cette page, mais cela ne nous est pas montré ;

Lorsque les pages sont générées dynamiquement par du Javascript, l’option [2] ne sert à rien. Il faut aller dans les outils du navigateur (F12 sur Firefox) pour voir le code de la page actuellement affichée :

image6

  • en [1], l’inspecteur du DOM (Document Object Model) du document affiché ;
  • en [2], ce que contient réellement ce DOM ;
  • [3-4], des outils que nous utiliserons pour afficher les objets Javascript utilisés par le framework [Vue.js] ;
  • [4] est une extension (ici Firefox) pour déboguer des applications [Vue.js] :
    • pour Firefox : [https://addons.mozilla.org/fr/firefox/addon/vue-js-devtools/];
    • pour Chrome : [https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd];

Examinons l’onglet [Vue] [4] :

image7

La vue [1-4] nous montre la structure [Vue.js] du document : la racine du document [2] (index.html) comprend le composant [App] (3) qui lui même comprend le composant [HelloWorld] (4). Cliquer sur [4] fait apparaître les propriétés du composant [HelloWorld] [5].

On voit en [4] (à droite), l’indicateur [$vm0]. C’est le nom de la variable qu’on peut utiliser dans la console JavaScript [6] pour désigner l’objet [HelloWorld]. Faisons-le :

image8

  • en [2], on fait évaluer l’expression [$vm0], ce qui a pour effet d’afficher sa structure. Normalement nous n’aurons pas à utiliser directement cette structure ;

Terminons en montrant la capacité de [hot reload] de la commande [serve] utilisée pour exécuter le projet :

  • dans [App.vue], modifiez le message affiché par [HelloWorld] :

image9

  • en [1], on modifie le message affiché ;
  • en [2-3], la page est automatiquement mise en jour sans intervention de notre part ;

Nous allons maintenant créer divers projets [vuejs-xx] pour illustrer les points importants de [Vue.js]. Par ‘importants’, il faut entendre ‘que nous allons utiliser dans le client [vue.js] du serveur de calcul de l’impôt’. D’autres points ‘importants’ seront passés sous silence s’ils ne sont pas utilisés dans le client. Ce n’est donc pas une présentation exhaustive de [vue.js] qui sera faite.

projet [vuejs-02] : utilisation du framework CSS Bootstrap

Le projet [vuejs-02] présente l’utilisation de Bootstrap dans un projet [vue.js]. C’est le framework CSS qui sera utilisé dans tous nos projets. Nous utiliserons une variation de Bootstrap appelée [BootstrapVue] [https://bootstrap-vue.js.org/].

L’arborescence du projet sera la suivante :

image0

Note : ci-dessus le dossier [vuejs] a été renommée [cours] [1] dans la suite du document.

Installation du framework [BootstrapVue]

[BootstrapVue] est un framework qu’on ajoute au projet avec l’outil [npm] :

image1

  • en [1], c’est donc deux frameworks qu’on installe : [Bootstrap] et sa variante [BootstrapVue] ;
  • en [2], les deux dépendances apparaissent dans le fichier [package.json] ;

Le script [main.js]

Le script principal [main.js] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   render: h => h(App),
 }).$mount('#app')
  • ligne 2 : import du framework [Vue] ;
  • ligne 3 : import de la vue principale ;
  • ligne 6 : import du framework [BootstrapVue] ;
  • ligne 7 : ce framework est conçu comme un plugin du framework [Vue]. La ligne 7 inclut ce plugin dans le framework [Vue] ;
  • lignes 10-11 : import des fichiers CSS des frameworks [Bootstrap] et [BootstrapVue] ;
  • les lignes 5-11 sont donc entièrement consacrées à l’utilisation de [BootstrapVue]. Le reste du code est identique à ce qu’on avait vu au paragraphe précédent ;

Le composant [App.vue]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 <template>
   <b-container>
     <b-card>
       <!-- Bootstrap Jumbotron -->
       <b-jumbotron>
         <!-- ligne -->
         <b-row>
           <!-- colonne de largeur 4 -->
           <b-col cols="4">
             <img src="./assets/logo.jpg" alt="Cerisier en fleurs" />
           </b-col>
           <!-- colonne de largeur 8 -->
           <b-col cols="8">
             <h1>Calculez votre impôt</h1>
           </b-col>
         </b-row>
       </b-jumbotron>
       <HelloBootstrap msg="Hello Bootstrap !" />
     </b-card>
   </b-container>
 </template>


 <script>
   import HelloBootstrap from "./components/HelloBootstrap.vue";

   export default {
     name: "app",
     components: {
       HelloBootstrap
     }
   };
 </script>

Commentaires

  • lignes 1-21 : toutes les balises <b-xx> sont des balises du framework [BootstrapVue] ;
  • lignes 2, 20 : la balise <b-container> définit un conteneur Bootstrap. A l’intérieur de ce conteneur, on va pouvoir définir des lignes avec la balise <b-row> et des colonnes avec la balise <b-col> ;
  • lignes 3, 19 : la balise <b-card> définit une ‘carte’ Bootstrap. Cela se matérialise visuellement par un rectangle avec une bordure ;
  • lignes 5, 17 : la balise <b-jumbotron> permet de mettre en avant une partie de la page, ici une image et un texte. On l’utilisera dans nos divers projets comme identification visuelle du projet ;
  • ligne 7 : la balise <b-row> définit une ligne ;
  • lignes 9-11 : la balise <b-col> définit une colonne de la ligne précédente. Bootstrap attribue 12 colonnes à chaque ligne. L’attribut [cols=’4’] indique que la colonne <b-col> va occuper 4 de ces 12 colonnes ;
  • ligne 10 : une image
  • lignes 13-15 : une colonne qui va occuper 8 des 12 colonnes de la ligne. on y met un texte ;
  • ligne 18 : utilisation d’un composant appelé [HelloBootstrap] avec une propriété nommée [msg] ;
  • lignes 24-33 : la partie <script> du composant ;
  • lignes 29-31 : le composant [HelloBootstrap] utilisée ligne 18 est exporté. Pour être connu, il doit être importé ligne 25 ;

Le résultat est le suivant :

image2

  • en [1], la balise <b-card> ;
  • en [2], la balise <jumbotron> ;
  • en [3], l’image sur 4 colonnes ;
  • en [4], le texte sur 8 colonnes ;

Le composant [HelloBootstrap]

[HelloBootstrap] est le composant suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 <template>
   <div>
     <!-- message sur fond vert -->
     <b-col cols="12">
       <b-alert show variant="success" align="center">
         <h4>[vuejs-02] : bootstrap</h4>
       </b-alert>
     </b-col>
     <!-- message sur fond jaune -->
     <b-col cols="12">
       <b-alert show variant="warning" align="center">
         <h4>{{msg}}</h4>
       </b-alert>
     </b-col>
   </div>
 </template>

 <script>
   export default {
     name: "HelloBootstrap",
     props: {
       msg: String
     }
   };
 </script>

Commentaires

  • ligne 3 : la balise <b-alert show> affiche un rectangle de couleur dans lequel on met en général un texte (ligne 6). L’attribut [variant] permet de sélectionner un type d’alerte. Chaque type d’alerte a une couleur de fond différente. La couleur de la variante [success] est le vert. L’attribut [align] permet d’aligner le texte de l’alerte (gauche, droite, centré). On notera que l’attribut [show] est obligatoire pour afficher l’alerte. Sans cet attribut, l’alerte n’est pas visible ;
  • les valeurs possibles de [variant] :
    • [primary] : bleu ;
    • [secondary] : gris ;
    • [success]:vert ;
    • [danger] : rouge léger ;
    • [warning] : jaune ;
    • [info] : turquoise ;
    • [light] : pas de couleur de fond ;
    • [dark] : gris un peu plus foncé que [secondary] ;
  • ligne 12 : [msg] est un paramètre du composant [HelloBootstrap] (lignes 21-23) ;

Le rendu visuel est le suivant :

image3

  • [1] : balise <b-alert show variant=’success’> ;
  • [2] : balise <b-alert show variant=’warning’> ;

Exécution du projet

Pour exécuter le projet, on modifie d’abord le fichier [package.json] :

image4

  • en [3], on modifie le script exécuté par la commande [serve] [2] du fichier package.json [1] ;
  • en [4], on exécute le projet ;

Note : dans tout ce qui suit on utilisera les balises du framework BootstrapVue, des balises de la forme <b-qqchose>. Ce n’est pas obligatoire. On peut utiliser les balises originelles du framework Bootstrap. Elles sont fonctionnelles dans les templates de [Vue.js]. Aussi le développeur habitué aux balises Bootstrap peut continuer à les utiliser.

projet [vuejs-03] : gestion des événements

Le projet [vuejs-03] introduit deux concepts :

  • la gestion d’un événement [clic] sur un bouton ;
  • la directive [v-if] qui permet d’afficher un bloc HTML de façon conditionnelle ;

L’arborescence du projet est la suivante :

image0

Le script principal [main.js]

Le script [main.js] reste inchangé :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   render: h => h(App),
 }).$mount('#app')

Le composant principal [App.vue]

Le composant principal [App.vue] utilise le composant [ClickOnMe] au lieu du composant [HelloBootstrap] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 <template>
   <b-container>
     <b-card>
     <!-- Bootstrap Jumbotron -->
     <b-jumbotron>
       <!-- ligne -->
       <b-row>
         <!-- colonne de largeur 4 -->
         <b-col cols="4">
           <img src="./assets/logo.jpg" alt="Cerisier en fleurs" />
         </b-col>
         <!-- colonne de largeur 8 -->
         <b-col cols="8">
           <h1>Calculez votre impôt</h1>
         </b-col>
       </b-row>
     </b-jumbotron>
     <!-- composant -->
     <ClickOnMe msg="Information..." />
     </b-card>
   </b-container>
 </template>


 <script>
 import ClickOnMe from "./components/ClickOnMe.vue";

 export default {
   name: "app",
   components: {
     ClickOnMe
   }
 };
 </script>

Le composant [ClickOnMe]

Le composant [ClickOnMe] introduit les nouveaux concepts :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 <template>
   <div>
     <!-- message sur fond vert -->
     <b-alert show variant="success" align="center">
       <h4>[vuejs-03] : événement @click, directive v-if, méthodes</h4>
     </b-alert>
     <!-- message sur fond jaune -->
     <b-alert show variant="warning" align="center" v-if="show">
       <h4>{{msg}}</h4>
     </b-alert>
     <!-- bouton bleu -->
     <b-button variant="primary" @click="changer">{{buttonTitle}}</b-button>
   </div>
 </template>

 <script>
   export default {
     name: "ClickOnMe",
     // paramètres du composant
     props: {
       msg: String
     },
     // attributs du composant
     data() {
       return {
         // titre du bouton
         buttonTitle: "Cacher",
         // contrôle l'affichage du message
         show: true
       };
     },
     // méthodes
     methods: {
       // montre / cache le message
       changer() {
         if (this.show) {
           // on cache le message
           this.show = false;
           this.buttonTitle = "Montrer";
         } else {
           // on montre le message
           this.show = true;
           this.buttonTitle = "Cacher";
         }
       }
     }
   };
 </script>

Commentaires

  • lignes 4-6 : une alerte verte Bootstrap. Le nombre de colonnes occupées n’est pas indiqué. Ce sont alors les 12 colonnes de Bootstrap qui sont utilisées ;
  • lignes 8-10 : une alerte jaune Bootstrap :
    • ligne 8 : la directive [v-if] de [Vue.js] contrôle la visibilité d’un bloc HTML. L’alerte est ici contrôlée par un booléen [show] (ligne 29). Si [show==true] alors l’alerte sera visible sinon elle ne le sera pas ;
    • ligne 9 : l’alerte affiche un message [msg] qui est une propriété (lignes 20-22) du composant ;
  • ligne 12 : un bouton de couleur bleue sur laquelle on clique pour cacher / montrer l’alerte [warning] ;
  • lignes 16-48 : le code jS du composant. Ce code règle le fonctionnement dynamique du composant :
  • lignes 20-22 : les propriétés du composant ;
  • lignes 24-31 : les attributs du composant ;

Quelle est la différence entre [propriétés] et [attributs] d’un composant, entre les champs [props] et [data] de l’objet exporté par le composant aux lignes 17-47 ?

  • comme nous l’avons déjà vu, les propriétés [props] d’un composant sont des paramètres du composant. Leurs valeurs sont fixées de l’extérieur du composant. Un composant A utilisant un composant B ayant les propriétés [prop1, prop2, …, propn] l’utilisera de la façon suivante : <B :prop1=’val1’ :prop2=’val2’ …> ;
  • l’objet rendu par la fonction [data] des lignes 24-31 représente l’état du composant ou attributs du composant. Cet état est manipulé par les méthodes du composant (lignes 33-46). Le <template> des lignes 1-14 utilise aussi bien des éléments [propriétés] que [attributs] :
    • les valeurs des propriétés sont fixées par un composant parent ;
    • les valeurs des attributs sont fixées initialement par la fonction [data] puis peuvent être modifiées par les méthodes ;
    • dans les deux cas, le rendu visuel réagit immédiatement aux changements d’une propriété (composant parent) ou d’un attribut (méthode du composant). On parle alors d’interface réactive ;
Dans le [template] d’un composant, rien ne diffère une propriété [prop] d’un attribut [data]. Pour savoir si une donnée dynamique du [template] doit être mise dans l’attribut [props] ou dans l’objet rendu par la fonction [data], il faut simplement se demander qui fixe la valeur de cette donnée :
  • si la réponse est le composant parent, alors on mettra la donnée dans l’attribut [props] ;
  • si la réponse est la méthode gérant tel événement du composant, alors on mettra la donnée dans l’objet rendu par la fonction [data] ;
Le [template] utilise ici les données dynamiques suivantes :
  • [show], ligne 8. Cette donnée est manipulée en interne par la méthode [changer] qui gère l’événement [click] sur le bouton de la ligne 12. C’est donc un attribut construit par la fonction [data] (ligne 29) ;
  • [msg], ligne 9. C’est un message fixé par le composant parent. On le met donc dans l’attribut [props] (ligne 21) ;
  • [buttonTitle] ligne 12. Cette donnée est manipulée en interne par la méthode [changer] qui gère l’événement [click] sur le bouton de la ligne 12. C’est donc un attribut construit par la fonction [data] (ligne 27) ;
  • les noms des attributs [name, props, data, methods] de l’objet exporté par le composant sont prédéfinis. On ne peut pas utiliser d’autres noms ;
  • ligne 12 : l’attribut [@click] du bouton sert à désigner la méthode qui doit réagir au clic sur le bouton. Cette méthode doit se trouver dans la propriété [methods] du composant ;
  • ligne 33 : l’attribut [methods] du composant réunit toutes les méthodes de celui-ci. La plupart du temps ce sont des fonctions qui réagissent à un événement du composant ;
  • lignes 35-46 : la méthode [changer] est appelée lorsque l’utilisateur clique sur le bouton :
    • si l’alerte [warning] est affichée alors elle est cachée et le texte du bouton devient [Montrer] (ligne 39) ;
    • si l’alerte [warning] est cachée alors elle est affichée et le texte du bouton devient [Cacher] (ligne 43) ;
    • pour afficher / cacher l’alerte [warning], on modifie la valeur du booléen [show] (lignes 38 et 42) ;
    • lorsqu’une méthode doit référencer l’attribut [attr] rendu par la fonction [data], on écrit [this.attr] (lignes 38 et 42). Cela signifie que les attributs de l’objet rendu par la fonction [data] sont des attributs directs du composant [this] ;

Exécution du projet

image1

Le résultat est le suivant :

image2

image3

projet [vuejs-04] : directives [v-model, v-bind], attributs calculés, formulaire de saisie

L’arborescence du projet [vuejs-04] est la suivante :

image0

Le script principal [main.js]

C’est le même que dans l’exemple précédent.

Le composant principal [App]

Le code de [App.vue] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
  <b-container>
    <b-card>
      <!-- message de présentation -->
      <b-row>
        <b-col cols="8">
          <b-alert show variant="success" align="center">
            <h4>[vuejs-04] : directives [v-model, v-bind], attributs calculés, formulaire de saisie</h4>
          </b-alert>
        </b-col>
      </b-row>
      <Form />
    </b-card>
  </b-container>
</template>


<script>
  import Form from "./components/Form.vue";

  export default {
    name: "app",
    components: {
      Form
    }
  };
</script>

Le composant [App.vue] utilise le nouveau composant [Form] (lignes 12, 19, 24).

Le composant [Form]

Le code du composant [Form] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
  <div>
    <!-- formulaire -->
    <b-form>
      <!-- éléments du formulaire -->
      <b-row>
        <b-col cols="4">
          <b-card bg-variant="light">
            <b-form-group label="Nombre d'enfants à charge" label-for="enfants">
              <b-input type="text"
                       id="enfants"
                       placeholder="Indiquez votre nombre d'enfants"
                       v-model="enfants"
                       v-bind:state="enfantsValide" />
              <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
            </b-form-group>
            <!-- bouton -->
            <b-button variant="primary" :disabled="formInvalide" @click="doSomething">Valider</b-button>
          </b-card>
        </b-col>
      </b-row>
    </b-form>
    <b-row class="mt-3">
      <b-col cols="4">
        <b-alert show variant="warning" align="center">
          <h4>enfants= {{enfants}}</h4>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

<!-- script -->
<script>
  export default {
    // nom
    name: "Form",
    // attributs statiques du composant
    data() {
      return {
        // nbre d'enfants
        enfants: ""
      };
    },

    // attributs calculés
    computed: {
      // attribut [formInvalide]
      formInvalide() {
        return !this.enfantsValide;
      },
      // attribut [enfantsInvalide]
      enfantsValide() {
        return Boolean(this.enfants.match(/^\s*\d+\s*$/));
      }
    },
    // méthodes
    methods: {
      doSomething() {
        // le nbre d'enfants est connu lorsque la validation a lieu
        alert("Nombre d'enfants : " + this.enfants);
      }
    }
  };
</script>

Rendu visuel

image1image2

Commentaires

  • lignes 4-32 : la balise <b-form> introduit un formulaire Bootstrap ;
  • ligne 6 : la balise <b-row> introduit une ligne dans le formulaire ;
  • ligne 7 : la balise <b-col cols=’4’> introduit une colonne s’étalant sur 4 colonnes Bootstrap ;
  • ligne 8 : la balise <b-card> [6] introduit une carte Bootstrap, une zone encadrée par une bordure ;
  • ligne 9 : la balise <b-form-group> introduit un groupe d’éléments du formulaire liés entre-eux. Ici un texte (attribut [label]) [1] est lié à une zone de saisie (attribut [label-for]). La valeur de [label-for] est la valeur du champ [id], ligne 12, de la zone de saisie ;
  • lignes 10-14 : la balise <b-input> [2] introduit une zone de saisie :
    • ligne 10 : [type=’text’] indique qu’on peut taper du texte dans la zone de saisie. On aurait pu écrire [type=’number’] avec des contraintes [min=’val1’ max=’val2’ step=’val3’] puisqu’on attend un nombre d’enfants. On a mis [type=’text’] afin de montrer comment vérifier la validité d’une saisie ;
    • ligne 12 : l’attribut [placeholder] [3] fixe le message affiché dans la zone de saisie tant que l’utilisateur n’a rien saisi ;
    • ligne 13 : la directive [v-model] associe, de façon bidirectionnelle la valeur saisie avec l’attribut [enfants], ligne 42, du composant :
      • lorsque la valeur saisie change, alors la valeur de l’attribut [enfants] change également ;
      • lorsque la valeur de l’attribut [enfants] change, alors la valeur saisie change également, ç-à-d que le contenu de [2] change ;
      • le point important à comprendre est que, grâce au mécanisme précédent, lorsque l’utilisateur clique sur le bouton [Valider] [5], l’attribut [enfants] de la ligne 42 a pour valeur, la valeur saisie en [2] ;
    • ligne 14 : la directive [v-bind] introduit une liaison entre d’un côté un attribut de la balise <b-input>, ici l’attribut [state] avec un attribut du composant, ici [enfantsValide], ligne 53. L’attribut [enfantsValide] est particulier en ce sens que c’est une fonction qui rend la valeur de l’attribut. On appelle, attribut calculé, ce type d’attribut. On trouve les attributs calculés dans la propriété [computed], ligne 47, du composant. Les attributs calculés s’utilisent de la même façon que les attributs statiques de la fonction [data] : On n’écrit pas, ligne 14, [v-bind:state=’enfantsValide()’] mais [v-bind:state=’enfantsValide’], sans les parenthèses. Aussi à la lecture du [template], on ne sait pas distinguer un attribut calculé d’un attribut statique. Il faut pour cela regarder le code du script du composant ;
    • ligne 14 : l’attribut [state] va fixer l’état valide / invalide de la valeur saisie : si [enfantsValide] rend la valeur [true], la valeur saisie est considérée comme valide, sinon comme invalide. La copie d’écran ci-dessus montre le composant [b-input] lorsque la fonction [enfantsValide] rend la valeur [false] ;
    • ligne 15 : la balise <b-form-invalid-feedback> [4] permet d’afficher un message lorsque la saisie en [2] est invalide. Son attribut [:state=’enfantsValide’] est identique à l’attribut [v-bind:state=’enfantsValide’] de la ligne 14. On peut omettre la directive [v-bind] mais il faut garder le signe [:]. Le message d’erreur s’affiche donc lorsque l’attribut [enfantsValide] vaut [false] ;
    • ligne 16 : fin du groupe d’éléments <b-group> ;
    • ligne 18 : le bouton [5] qui va permettre de valider la saisie :
      • il sera bleu [variant=’primary’] ;
      • [:disabled= »formInvalide »] : l’attribut [disabled] permet de valider / invalider le bouton. Cet attribut est lié (v-bind) à l’attribut calculé [formInvalide] de la ligne 49 ;
      • [@click= »doSomething »] : lorsque l’utilisateur cliquera sur le bouton, la méthode [doSomething], ligne 59, sera exécutée ;
    • lignes 19-22 : fermeture des différentes balises ouvertes ;
    • lignes 23-29 : une nouvelle ligne dans le [template]. [class=’mt-3’] signifie [margin (m) top (t) égale à 3 spacers]. [spacer] est une mesure d’espacement de Bootstrap. Cette classe génère l’espacement [8] dans la copie d’écran ci-dessus. Sans cette classe, la zone [7] est collée à la zone [1-6] ;
    • ligne 24 : une colonne occupant 4 colonnes Bootstrap ;
    • lignes 25-27 : une alerte [warning] affichant la valeur de l’attribut statique [enfants] (ligne 42). Comme cet attribut a une liaison bidirectionnelle avec la zone de saisie, dès que l’utilisateur modifie celle-ci, la valeur affichée dans l’alerte change également ;
    • lignes 34-65 : le code jS du composant ;
    • ligne 42 : l’unique attribut statique du composant ;
    • lignes 47-56 : les attributs calculés du composant ;
    • lignes 53-55 : la saisie est considérée valide si c’est un nombre entier positif éventuellement précédé / suivi par des ‘espaces’ ;
    • lignes 49-51 : le formulaire est considéré comme valide si la saisie du nombre d’enfants est valide. En général, un formulaire a plusieurs saisies et est considéré valide si celles-ci sont toutes valides ;
    • lignes 58-63 : les méthodes du composant qui réagissent aux événements de celui-ci. Ici, il n’y a qu’un événement : le [click] sur le bouton. On se contente d’afficher la valeur saisie pour montrer qu’on a bien accès à celle-ci ;

Exécution du projet

On modifie le fichier [package.json] et on exécute le projet :

image3

projet [vuejs-05] : directive [v-for]

Le projet [vuejs-05] présente la directive [v-for] :

image0

Le script principal [main.js]

Le code du script principal [main.js] est identique à celui du script [main.js] des projets précédents.

Le composant principal [App]

Le code du composant [App] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <template>
   <b-container>
     <b-card>
       <b-alert show variant="success" align="center">
         <h4>[vuejs-05] : attribut [v-for]</h4>
       </b-alert>
       <VFor />
     </b-card>
   </b-container>
 </template>


 <script>
   import VFor from "./components/VFor.vue";

   export default {
     name: "app",
     components: {
       VFor
     }
   };
 </script>
  • lignes 7, 14, 19 : le composant [App] utilise le composant [VFor] ;

Le composant [VFor]

Le rendu visuel sera le suivant :

image1

image2

Le code du composant [VFor] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
 <template>
   <div>
     <!-- une liste déroulante -->
     <b-dropdown id="dropdown" text="Options">
       <b-dropdown-item v-for="(option,index) in options"
                        :key="option.id"
                        @click="select(index)">{{option.text}}</b-dropdown-item>
     </b-dropdown>
     <!-- bouton -->
     <b-button class="ml-3"
               variant="primary"
               @click="generateErrors"
               v-if="!error">Générer une liste d'erreurs</b-button>
     <!-- alerte -->
     <b-alert show variant="danger" v-if="error" class="mt-3">
       Les erreurs suivantes se sont produites :
       <br />
       <ul>
         <li v-for="(erreur,index) in erreurs" :key="index">{{erreur}}</li>
       </ul>
     </b-alert>
   </div>
 </template>
 <!-- script -->
 <script>
   export default {
     name: "VFor",

     // propriétés statiques du composant
     data() {
       return {
         // liste des erreurs
         erreurs: [],
         // erreur ou pas
         error: false,
         // liste des options du menu
         options: [
           { text: "option 1", id: 1 },
           { text: "option 2", id: 2 },
           { text: "option 3", id: 3 }
         ]
       };
     },

     // méthodes
     methods: {
       // génération d'une liste d'erreurs
       generateErrors() {
         this.erreurs = ["erreur 1", "erreur 2", "erreur 3"];
         this.error = true;
       },
       // l'utilisateur a sélectionné une option
       select(index) {
         alert("Vous avez choisi : " + this.options[index].text);
       }
     }
   };
 </script>

Commentaires

  • ligne 4 : la balise <b-dropdown> sert à définir une liste déroulante [1] sous la forme d’un bouton qu’on clique pour voir les options de la liste [2]. [text=’Options’] définit le texte affiché sur le bouton [1] ;
  • lignes 5-7 : la balise <dropdown-item> définit un élément de la liste déroulante ;
  • ligne 5 : l’attribut [v-for] indique que la balise <dropdown-item> doit être répétée pour chaque élément [option] de l’attribut [options], lignes 37-41, du composant. [index] représente le n° de l’élément dans la liste [0, 1, …, n]. Le nom de l’élément [option] ainsi que celui de l’index sont libres. On aurait pu écrire [<b-dropdown-item v-for= »(o,i) in options » :key= »o.id » @click= »select(i) »>{{o.text}}</b-dropdown-item>] ;
  • ligne 6 : si on oublie l’attribut [key], ESLint émet un warning. La valeur de l’attribut [key] doit persister dans le temps. Aussi la valeur [index] de l’élément ne convient-elle pas. Car si cet élément est supprimé, les valeurs [index] de ceux qui sont derrière lui dans la liste vont être décrémentées de 1. Aussi ici, prend-on comme valeur de clé la valeur [option.id], lignes 38-40, qui ne changera pas en cas de suppression d’un élément. L’attribut [key] est un élément d’optimisation de régénération du DOM (Document Object Model) par [Vue.js] lorsque la liste doit être régénérée. Notez la notation [:key], car [key] a une valeur dynamique ;
  • ligne 7 : la méthode [select(index)], lignes 49-51, sera appelée lorsque l’utilisateur cliquera sur un élément de la liste ;
  • ligne 7 : le texte de l’option sera la valeur [option.text] définie aux lignes 37-41 ;
  • ligne 10 : le bouton [3]. [class=’ml-3] signifie margin (m) left (l) de trois spacers. [@click= »generateErrors »] indique que la méthode [generateErrors], lignes 45-48, sera exécutée lors d’un [click] sur le bouton. [v-if= »!error »] indique que l’affichage du bouton est conditionné à la valeur de l’attribut statique [error] de la ligne 35 ;
  • lignes 15-21 : une alerte de type [danger] [4] contrôlée elle-aussi par l’attribut statique [error] de la ligne 35. L’attribut [class=’mt-3’] (margin top 3 spacers) fixe l’espace entre cette alerte et l’élément qui est au-dessus de lui ;
  • ligne 27 : la balise HTML <br /> fait un saut de ligne ;
  • ligne 18 : début d’une liste non ordonnée [ul=unordered list] ;
  • ligne 19 : la balise <li> définit un élément de la liste <ul>. Là encore, on utilise une directive [v-for] pour générer la balise plusieurs fois. Autant de fois ici que le tableau [erreurs] de la ligne 33 aura d’éléments. On utilise ici l’attribut [:key=index]. On a dit précédemment que l’index des éléments d’une liste ne faisait pas un bon discriminant entre éléments de la liste car en cas de suppression d’un élément, tous les éléments qui suivent voient leur index changer. Ici ça n’a pas d’importance car les éléments de la liste d’erreurs ne sont pas susceptibles d’être supprimés ;
  • ligne 19 : l’élément sert à afficher l’élément [erreur] de la liste [erreurs] ;
  • lignes 30-43 : tous les éléments dynamiques du [template] sont des attributs du composant. Il n’y a ici aucune propriété de type [props] dont la valeur serait fixée par le composant parent ;
  • lignes 48-55 : la méthode [generateErrors] génère la liste d’erreurs à afficher par la balise <ul> des lignes 16-18. De plus, elle modifie l’attribut statique [error], à la fois pour afficher cette liste d’erreurs (ligne 15) et cacher le bouton de génération (ligne 13) ;

Exécution du projet

image3

projet [vuejs-06] : mise en page d’une vue avec des slots

image0

Le script principal [main.js]

Le script principal [main.js] reste inchangé.

Le composant principal [App]

Le code du composant [App] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <template>
   <div class="container">
     <b-card>
       <b-alert show variant="success" align="center">
         <h4>[vuejs-06] : mise en page avec des slots</h4>
       </b-alert>
       <Content />
     </b-card>
   </div>
 </template>


 <script>
   import Content from './components/Content'
   export default {
     name: "app",
     // composants utilisés
     components: {
       Content
     }
   };
 </script>

Le composant [App] introduit un nouveau composant appelé [Content] (lignes 7, 14, 19).

Le composant [Layout]

Le composant [Layout] sert à définir une mise en page des vues de l’application :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 <template>
   <!-- ligne -->
   <b-row>
     <!-- zone à trois colonnes -->
     <b-col cols="3" v-if="left">
       <slot name="slot-left" />
     </b-col>
     <!-- zone à neuf colonnes -->
     <b-col cols="9" v-if="right">
       <slot name="slot-right" />
     </b-col>
   </b-row>
 </template>

 <script>
   export default {
     name: "patron",
     // paramètres
     props: {
       left: {
         type: Boolean
       },
       right: {
         type: Boolean
       }
     }
   };
 </script>

Commentaires

  • le composant [Layout] définit une unique ligne (lignes 3-12). Celle-ci est divisée en deux colonnes :
    • une colonne composée de 3 colonnes Bootstrap (lignes 5-7) ;
    • une colonne composée de 9 colonnes Bootstrap (lignes 9-11) ;
  • ligne 5 : la présence de la colonne de gauche (lignes 5-7) est conditionnée par la valeur du paramètre [left], définie dans la propriété [props] du composant (lignes 20-22) ;
  • ligne 9 : la présence de la colonne de droite (lignes 9-11) est conditionnée par la valeur du paramètre [right], définie dans la propriété [props] du composant (lignes 23-25) ;
  • les valeurs des paramètres [left, right] doivent être fixées par un composant parent du composant [Layout] ;
  • ligne 6 : on ne fixe pas le contenu de la colonne de gauche. On donne simplement un nom à cette colonne avec la balise <slot>. Ici elle s’appelera [slot-left]. Un composant utilisant le composant [Content] devra indiquer ce qu’il veut mettre dans la zone appelée [slot-left] ;
  • ligne 10 : la colonne de droite s’appellera [slot-right] ;

Le composant [Right]

Le composant [Right] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <template>
   <!-- un message dans une alerte de type warning -->
   <b-alert show variant="warning" align="center">
     <h4>{{msg}}</h4>
   </b-alert>
 </template>

 <!-- script -->
 <script>
   export default {
     name: "droite",
     // paramètres
     props: {
       msg: String
     }
   };
 </script>
  • lignes 3-5 : le composant [Right] affiche un message dans une alerte de type [warning]. Ce message [msg] est défini comme étant un paramètre du composant, ligne 14. Le composant parent devra donc lui donner une valeur ;

Le composant [Left]

Le composant [Left] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <template>
   <!-- un message dans une alerte de type primary -->
   <b-alert show variant="primary" align="center">
     <h4>{{msg}}</h4>
   </b-alert>
 </template>

 <!-- script -->
 <script>
   export default {
     name: "gauche",
     // paramètres
     props: {
       msg: String
     }
   };
 </script>
  • lignes 3-5 : le composant [Left] affiche un message dans une alerte de type [primary]. Ce message [msg] est défini comme étant un paramètre du composant, ligne 14. Le composant parent devra donc lui donner une valeur ;

Le composant [Content]

On se rappelle que le composant [Content] est le composant affiché par la vue principale [App.vue] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 <template>
   <div>
     <!-- colonnes gauche et droite remplies -->
     <Layout :left="true" :right="true">
       <Right slot="slot-right" msg="slot [slot-right] présent et rempli" />
       <Left slot="slot-left" msg="slot [slot-left] présent et rempli" />
     </Layout>
     <!-- colonnes gauche basente. colonne droite remplie -->
     <Layout :left="false" :right="true">
       <Right slot="slot-right" msg="slot [slot-right] présent et rempli, slot [slot-left] absent" />
     </Layout>
     <!-- colonnes gauche remplie, colonne droite absente -->
     <Layout :left="true" :right="false">
       <Left slot="slot-left" msg="slot [slot-left] présent et rempli, slot [slot-right] absent" />
     </Layout>
     <!-- colonnes gauche présente mais pas remplie, colonne droite remplie -->
     <Layout :left="true" :right="true">
       <Right slot="slot-right" msg="slot [slot-right] présent et rempli, slot [slot-left] présent mais vide" />
     </Layout>
   </div>
 </template>

 <!-- script -->
 <script>
   import Layout from "./Layout";
   import Left from "./Left";
   import Right from "./Right";
   export default {
     name: "contenu",
     // composants
     components: {
       Layout,
       Left,
       Right
     }
   };
 </script>

Rendu visuel

image1

Commentaires

  • le composant [Layout] est utilisé 4 fois (lignes 4-7, 9-11, 13-15, 17-19). Si on se rappelle que le composant [Layout] définit 1 ligne, le [template] ci-dessus définit quatre lignes. On se rappelle également que la ligne du [Layout] définit deux colonnes :
    • une colonne appelée [slot-left] qui occupe les 3 colonnes Bootstrap de gauche ;
    • une colonne appelée [slot-Right] qui occupe les 9 colonnes Bootstrap de droite ;
  • lignes 4-7 : définit une ligne [1] où le composant [Left] occupe la colonne [slot-left] et le composant [Right] la colonne [slot-right] ;
  • lignes 9-11 : définit une ligne [2] où la colonne [slot-left] n’est pas affichée et le composant [Right] occupe la colonne [slot-right] ;
  • lignes 13-15 : définit une ligne [3] où la colonne [slot-right] n’est pas affichée et le composant [Left] occupe la colonne [slot-left] ;
  • lignes 17-19 : définit une ligne [4] où la colonne [slot-left] est affichée [:left=’true’] mais pas remplie et le composant [Right] occupe la colonne [slot-right] ;

Au final, le composant [Layout] a servi de mise en page au composant [Content].

projet [vuejs-07] : remontée d’événements dans la hiérarchie des composants

L’arborescence du projet est la suivante :

image0

Le script principal [main.js]

Le script principal [main.js] reste inchangé.

Le composant principal [App]

Le code du composant [App] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 <template>
   <div class="container">
     <b-card>
       <b-alert show variant="success" align="center">
         <h4>[vuejs-07] : remontée d'événements dans la hiérarchie des composants</h4>
       </b-alert>
       <Component1 />
     </b-card>
   </div>
 </template>

 <script>
   import Component1 from './components/Component1'
   export default {
     name: "app",
     components: {
       Component1
     }
   };
 </script>

Le composant [App] utilise un nouveau composant [Component1] (lignes 7, 13, 17).

Le composant [Component11]

Le code du composant [Component11] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <template>
   <b-button @click="createEvent">Créer un événement</b-button>
 </template>
 <!-- script -->
 <script>
   export default {
     name: "component11",
     // méthodes du composant
     methods: {
       // gestion de l'évt [click] sur le bouton
       createEvent() {
         // on émet un événement couplé à une donnée
         this.$emit("someEvent", { x: 2, y: 4 })
       }
     }
   };
 </script>

Commentaires

  • lignes 1-3 : le composant [Component11] ne comporte qu’un bouton qu’on peut cliquer. Lorsqu’on le clique, la méthode [createEvent] des lignes 11-14 est exécutée ;
  • ligne 13 : chaque instance [Vue] dispose d’une méthode [$emit] qui permet d’émettre un événement :
    • le 1er paramètre est le nom de l’événement émis ;
    • le second paramètre est la donnée que l’on veut associer à cet événement ;
    • l’événement émis remonte la hiérarchie des composants qui chapeautent le composant [Component11]. Il remonte la hiérarchie jusqu’à ce qu’il trouve un composant disposé à le traiter. Cette remontée commence par le composant parent de [Component11] ;

Le composant [Component1]

Le code du composant [Component1] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 <template>
   <b-row>
     <!-- le composant qui lance l'événement -->
     <b-col cols="2">
       <Component11 @someEvent="doSomething" />
     </b-col>
     <!-- message affiché par la méthode de gestion de l'évt-->
     <b-col>
       <b-alert show
                variant="warning"
                v-if="showMsg">Evénement [someEvent] intercepté par [Component1]. Valeur reçue={{data}}</b-alert>
     </b-col>
   </b-row>
 </template>

 <script>
   import Component11 from "./Component11";
   export default {
     name: "component1",
     // composants
     components: {
       Component11
     },
     // état du composant
     data() {
       return {
         data: "",
         showMsg: false
       };
     },
     // méthodes de gestion des évts
     methods: {
       doSomething(data) {
         // data est l'objet qui a été associé à l'évt [someEvent]
         this.data = data;
         // affiche l'alerte
         this.showMsg = true;
       }
     }
   };
 </script>

Rendu visuel

image1

image2

Commentaires

  • ligne 5 : [Component1] est parent de [Component11] et peut donc ‘écouter’ (c’est le terme) les événements émis par ce composant. On sait que celui-ci peut émettre l’événement [someEvent]. L’attribut [@someEvent= »doSomething »] indique que si l’événement [someEvent] émis par [Component11] se produit, alors la méthode [doSomething] de la ligne 33 doit être exécutée ;
  • lignes 9-11 : un message affiché par la méthode [doSomething]. Son affichage est contrôlé par le booléen [showMsg] de la ligne 28. L’alerte affiche l’attribut [data] de la ligne 27 ;
  • ligne 33 : la méthode [doSomething] exécutée lorsque l’événement [someEvent] se produit ligne 5, reçoit comme paramètre la donnée [data] associée à cet événement. Ce paramètre est affecté à l’attribut [data] de la ligne 27 et qui est affiché par la ligne 11 ;
  • ligne 37 : on met l’attribut [showMsg] à [true] pour afficher l’alerte des lignes 9-11 ;

Exécution du projet

image3

projet [vuejs-08] : événements indépendants de la hiérarchie des composants, cycle de vie des composants

Le projet [vuejs-08] lontre comment des composants non reliés directement dans la hiérarchie des composants peuvent néanmoins communiquer via des événements. L’arborescence du projet [vuejs-08] est la suivante :

image0

Le script principal [main.js]

Le script [main.js] reste ce qu’il était dans les exemples précédents.

Le script [even-bus.js]

Le script [event-bus.js] est l’outil que nous allons utiliser pour émettre et écouter des événements :

1
2
3
 import Vue from 'vue';
 const eventBus = new Vue();
 export default eventBus;

Le script [event-bus] se contente de créer une instance de la classe [Vue] (lignes 1-2) et de l’exporter (ligne 3). La classe [Vue] possède deux méthodes pour gérer les événements :

  • [Vue].$emit(nom, donnée) : permet d’émettre un événement nommé [nom] et associé à la donnée [donnée] ;
  • [Vue].$on(nom, fn) : permet d’intercepter l’événement nommé [nom] et de le faire traiter par la fonction [fn]. La fonction [fn] recevra en paramètre la donnée [donnée] associée à l’événement par son émetteur ;

Cette gestion d’événements n’est pas liée à la hiérarchie des composants. Il faut simplement que les composants qui veulent communiquer ainsi utilisent la même instance de la classe [Vue] pour émettre / écouter les événements. Dans notre exemple, nous utiliserons l’instance [eventBus] définie par le script [event-bus.js] précédent.

La vue principale [App.vue]

Le code de la vue principale [App] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 <template>
   <div class="container">
     <b-card>
       <b-alert show variant="success" align="center">
         <h4>[vuejs-08] : événements indépendants de la hiérarchie des composants, cycle de vie des composants</h4>
       </b-alert>
       <!-- les trois composants qui communiquent par évt -->
       <Component1 />
       <Component3 />
       <Component2 />
     </b-card>
   </div>
 </template>

 <script>
   import Component1 from "./components/Component1";
   import Component2 from "./components/Component2";
   import Component3 from "./components/Component3";
   export default {
     name: "app",
     // composants
     components: {
       Component1,
       Component2,
       Component3
     }
   };
 </script>
  • lignes 8-10 : la vue principale [App] utilise trois composants [Component1, Component2, Component3] qui vont communiquer par événements. Les trois composants sont au même niveau dans la vue [App]. De plus, il n’y aura pas de relation de hiérarchie entre eux ;

Le composant [Component1]

Le code du composant [Component1] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 <template>
   <b-row>
     <b-col>
       <b-alert show
                variant="warning"
                v-if="showMsg">Evénement [someEvent] intercepté par [Component1]. Valeur reçue={{data}}</b-alert>
     </b-col>
   </b-row>
 </template>

 <script>
   import eventBus from "../event-bus.js"
   export default {
     name: "component1",
     // état du composant
     data() {
       return {
         data: "",
         showMsg: false
       };
     },
     // méthodes de gestion des évts
     methods: {
       // gestion de l'evt [someEvent]
       doSomething(data) {
         this.data = data;
         this.showMsg = true;
       }
     },
     // gestion du cycle de vie du composant
     // évt [created] - le composant a été créé
     created() {
       // écoute de l'évt [someEvent]
       eventBus.$on("someEvent", this.doSomething);
     }
   };
 </script>

Commentaires

  • lignes 4-6 : une alerte qui affiche la donnée [data] de la ligne 18. Cette donnée sera initialisée par le gestionnaire d’événement [doSomething] de la ligne 25. Ce gestionnaire est activé à réception de l’événement [someEvent] (ligne 34). L’alerte est affichée conditionnellement à la valeur de l’attribut [showMsg] de la ligne 19. Cet attribut est lui également positionné par le gestionnaire d’événement [doSomething] de la ligne 25 ;
  • ligne 32 : la fonction [created] est un gestionnaire d’événement. Elle gère l’événement [created], un événement émis au cours du cycle de vie du composant. Il en existe d’autres [beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed]. L’événement [created] est émis lorsque le composant a été créé ;
  • ligne 12 : l’instance [eventBus] de la classe [Vue] est importée ;
  • ligne 34 : elle est utilisée pour écouter l’événement [someEvent]. Lorsque celui-ci se produit, la méthode [doSomething] de la ligne 25 est appelée ;
  • ligne 25 : la méthode [doSomething] reçoit comme paramètre [data] la donnée que l’émetteur de l’événement [someEvent] a associée à l’événement ;
  • lignes 26-27 : l’état du composant est modifié pour que l’alerte des lignes 4-6 affiche la donnée reçue ;

Le composant [Component2]

Le composant [Component2] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 <template>
   <div>
     <b-button @click="createEvent">Créer un événement</b-button>
   </div>
 </template>
 <!-- script -->
 <script>
   import eventBus from '../event-bus.js'
   export default {
     name: "component2",
     // méthodes de gestion des évts
     methods: {
       createEvent() {
         eventBus.$emit("someEvent", { x: 2, y: 4 })
       }
     }
   };
 </script>

Commentaires

  • ligne 3 : un bouton pour créer un événement. Lorsque l’utilisateur clique sur ce bouton, la méthode [createEvent] de la ligne 13 est appelée ;
  • ligne 8 : l’instance [eventBus] définie par le script [event-bus.js] est importée ;
  • ligne 14 : cette instance est utilisée pour émettre un événement nommé [someEvent] associé à la donnée [{ x: 2, y: 4 }]. Au final, lorsque l’utilisateur clique sur le bouton de la ligne 3, l’événement [someEvent] est émis. Si on se rappelle la définition de [Component1], celui-ci interceptera cet événement ;

Le composant [Component3]

Le code de ce composant est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 <template>
   <b-row>
     <b-col>
       <b-alert show
                v-if="showMsg">Evénement [someEvent] intercepté par [Component3]. Valeur reçue={{data}}</b-alert>
     </b-col>
   </b-row>
 </template>

 <script>
   import eventBus from "../event-bus.js"
   export default {
     name: "component3",
     // état du composant
     data() {
       return {
         data: "",
         showMsg: false
       };
     },
     methods: {
       // gestion de l'evt [someEvent]
       doSomething(data) {
         this.data = data;
         this.showMsg = true;
       }
     },
     // gestion du cycle de vie du composant
     // évt [created] - le composant a été créé
     created() {
       // écoute de l'évt [someEvent]
       eventBus.$on("someEvent", this.doSomething);
     }
   };
 </script>

[Component3] est un clone de [Component1]. Il n’est là que pour montrer qu’un événement peut être intercepté par plusieurs composants.

Exécution du projet [vuejs-08]

image1

image2

image3

projet [vuejs-09] : utilisation d’un plugin [eventBus]

Le projet [vuejs-09] est identique au projet [vuejs-08] si ce n’est qu’il introduit la notion de plugin. L’arborescence du projet est la suivante :

image0

Le plugin [./plugins/event-bus]

Le script [./event-bus.js] reste ce qu’il était dans l’exemple précédent :

1
2
3
 import Vue from 'vue';
 const eventBus = new Vue();
 export default eventBus;

Le plugin [./plugins/event-bus.js] est le suivant :

1
2
3
4
5
6
7
8
9
 export default {
   install(Vue, eventBus) {
     // ajoute une propriété [$eventBus] à la classe Vue
     Object.defineProperty(Vue.prototype, '$eventBus', {
       // lorsque Vue.$eventBus est référencé, on rend le 2ième paramètre [eventBus]
       get: () => eventBus,
     })
   }
 }

Commentaires

  • un plugin [Vue] est un objet ayant une fonction [install] (ligne 2). Celle-ci sera automatiquement appelée lorsqu’un code déclare l’utilisation du plugin ;
  • lignes 1-9 : l’objet exporté par le script ;
  • ligne 2 : la fonction [install] accepte ici deux paramètres :
    • [Vue] : la fonction obtenue par l’instruction [import Vue from ‘Vue’]. Peut-être assimilée à une classe ;
    • [eventBus] : l’objet exporté par le script [./event-bus] ;
  • lignes 4-7 : modifient la définition (on dit le prototype) de la fonction [Vue] en lui ajoutant la propriété [$eventBus]. Si on raisonne avec le terme [classe], la propriété [$eventBus] est ajoutée à la clase [Vue]. Les composants qui sont des instances de [Vue] auront donc accès à cette nouvelle propriété ;
  • la ligne 6 indique que lorsqu’on référencera la propriété [Vue].$eventBus, on obtiendra le paramètre [eventBus] de la ligne 2. Nous allons voir un peu plus loin que ce second paramètre sera l’objet [eventBus] exporté par le script [./event-bus.js]. Donc au final, lorsqu’un composant C utilisera l’expression [C.$eventBus] il référencera l’objet [eventBus] exporté par le script [./event-bus.js]. Cela lui évitera d’importer le script [./event-bus.js]. L’intérêt du plugin est là : simplifier l’accès à l’objet [eventBus] exporté par le script [./event-bus.js] ;
  • on notera que le plugin n’a pas à s’appeler lui-même [event-bus.js]. On aurait pu l’appeler [plugin-event-bus] par exemple ;
  • on notera également que le terme $ dans [$eventBus] est une convention pour désigner les propriétés de [Vue] qui ont été ajoutées via des plugins. On n’est pas obligés d’observer cette convention. Dans ce texte, nous l’observerons ;

Le script principal [main.js]

Le script [./plugins/event-bus.js] définit un plugin pour le framework [Vue.js]. Ce plugin n’est pas encore utilisé, juste défini. C’est le script [main.js] qui l’active :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // eventbus
 import eventBus from './event-bus';
 import PluginEventBus from './plugins/event-bus';
 Vue.use(PluginEventBus, eventBus);

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   render: h => h(App),
 }).$mount('#app')

Commentaires

  • les lignes 14-16 activent le plugin [PluginEventBus]. Après la ligne 16, toutes les instances de la classe (fonction) [Vue] possèdent la propriété [$eventBus] qui pointe pour chacune d’elles sur le même object exporté par le script [./event-bus.js]. Ce sera le cas pour chacun des composants du projet ;

La vue principale [App]

La vue principale [App] reste ce qu’elle était dans le projet précédent.

Le composant [Component1]

Le composant [Component1] utilise désormais sa propriété [$eventBus] pour écouter l’événement [someEvent] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 <template>
   <b-row>
     <b-col>
       <b-alert show
                variant="warning"
                v-if="showMsg">Evénement [someEvent] intercepté par [Component1]. Valeur reçue={{data}}</b-alert>
     </b-col>
   </b-row>
 </template>

 <script>
   export default {
     name: "component1",
     // état du composant
     data() {
       return {
         data: "",
         showMsg: false
       };
     },
     // méthodes de gestion des évts
     methods: {
       // gestion de l'evt [someEvent]
       doSomething(data) {
         this.data = data;
         this.showMsg = true;
       }
     },
     // gestion du cycle de vie du composant
     // évt [created] - le composant a été créé
     created() {
       // écoute de l'évt [someEvent]
       this.$eventBus.$on("someEvent", this.doSomething);
     }
   };
 </script>

Commentaires

  • ligne 33, utilisation de la propriété [this.$eventBus] du composant. On remarquera de plus que le script ligne 11 n’importe plus le script [./event-bus.js] ;

Le composant [Component2]

Le composant [Component2] utilise désormais sa propriété [$eventBus] pour émettre l’événement [someEvent] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 <template>
   <div>
     <b-button @click="createEvent">Créer un événement</b-button>
   </div>
 </template>
 <!-- script -->
 <script>
   export default {
     name: "component2",
     // méthodes de gestion des évts
     methods: {
       createEvent() {
         this.$eventBus.$emit("someEvent", { x: 2, y: 4 })
       }
     }
   };
 </script>

Commentaires

  • ligne 13, utilisation de la propriété [this.$eventBus] du composant. On remarquera de plus que le script ligne 7 n’importe plus le script [./event-bus.js] ;

Composant [Component3]

Le composant [Component3] a le même code que [Component1]. Lui également écoute l’événement [someEvent].

Exécution du projet

image1

On obtient les mêmes résultats que dans le projet précédent.

projet [vuejs-10] : plugin [dao], requêtes HTTP asynchrones

L’arborescence du projet [vuejs-10] est la suivante :

image0

Le projet [vuejs-10] montre un composant faisant une requête HTTP à un serveur distant. L’architecture utilisée est la suivante :

image1

Un composant [Vue.js] utilise la couche [dao] pour dialoguer avec le serveur de calcul de l’impôt.

Installation des dépendances

L’application [vuejs-10] utilise la bibliothèque [axios] pour faire les requêtes asynchrones vers le serveur de calcul d’impôt. Il nous faut installer cette dépendance :

image2

  • en [4-5], la ligne ajoutée au fichier [package.json] après l’installation de la bibliothèque [axios] [1-3] ;

La classe [Dao]

La classe [Dao] est celle qui a été développée au paragraphe [La classe Dao]. Nous la redonnons ici pour mémoire :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
 'use strict';

 // imports
 import qs from 'qs'

 // classe [Dao]
 class Dao {

   // constructeur
   constructor(axios) {
     this.axios = axios;
     // cookie de session
     this.sessionCookieName = "PHPSESSID";
     this.sessionCookie = '';
   }

   // init session
   async  initSession() {
     // options de la requête HHTP [get /main.php?action=init-session&type=json]
     const options = {
       method: "GET",
       // paramètres de l'URL
       params: {
         action: 'init-session',
         type: 'json'
       }
     };
     // exécution de la requête HTTP
     return await this.getRemoteData(options);
   }

   async  authentifierUtilisateur(user, password) {
     // options de la requête HHTP [post /main.php?action=authentifier-utilisateur]
     const options = {
       method: "POST",
       headers: {
         'Content-type': 'application/x-www-form-urlencoded',
       },
       // corps du POST
       data: qs.stringify({
         user: user,
         password: password
       }),
       // paramètres de l'URL
       params: {
         action: 'authentifier-utilisateur'
       }
     };
     // exécution de la requête HTTP
     return await this.getRemoteData(options);
   }

   async getAdminData() {
     // options de la requête HHTP  [get /main.php?action=get-admindata]
     const options = {
       method: "GET",
       // paramètres de l'URL
       params: {
         action: 'get-admindata'
       }
     };
     // exécution de la requête HTTP
     const data = await this.getRemoteData(options);
     // résultat
     return data;
   }

   async  getRemoteData(options) {
     // pour le cookie de session
     if (!options.headers) {
       options.headers = {};
     }
     options.headers.Cookie = this.sessionCookie;
     // exécution de la requête HTTP
     let response;
     try {
       // requête asynchrone
       response = await this.axios.request('main.php', options);
     } catch (error) {
       // le paramètre [error] est une instance d'exception - elle peut avoir diverses formes
       if (error.response) {
         // la réponse du serveur est dans [error.response]
         response = error.response;
       } else {
         // on relance l'erreur
         throw error;
       }
     }
     // response est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
     // on récupère le cookie de session s'il existe
     const setCookie = response.headers['set-cookie'];
     if (setCookie) {
       // setCookie est un tableau
       // on cherche le cookie de session dans ce tableau
       let trouvé = false;
       let i = 0;
       while (!trouvé && i < setCookie.length) {
         // on cherche le cookie de session
         const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
         if (results) {
           // on mémorise le cookie de session
           // eslint-disable-next-line require-atomic-updates
           this.sessionCookie = results[1];
           // on a trouvé
           trouvé = true;
         } else {
           // élément suivant
           i++;
         }
       }
     }
     // la réponse du serveur est dans [response.data]
     return response.data;
   }
 }

 // export de la classe
 export default Dao;

Le projet [vuejs-10] n’utilise que la méthode asynchrone [initSession] des lignes 18-30. On rappelle que la classe [Dao] est instanciée avec un paramètre [axios], ligne 10, paramètre initialisé par le code appelant. Ce code appelant sera ici le script [./main.js].

Le plugin [pluginDao]

Le plugin [pluginDao] est le suivant :

1
2
3
4
5
6
7
8
9
 export default {
   install(Vue, dao) {
     // ajoute une propriété [$dao] à la classe Vue
     Object.defineProperty(Vue.prototype, '$dao', {
       // lorsque Vue.$dao est référencé, on rend le 2ième paramètre [dao]
       get: () => dao,
     })
   }
 }

Si on se souvient de l’explication donnée pour le plugin [event-bus], on voit que le plugin [pluginDao] crée dans la classe / fonction [Vue], une nouvelle propriété appelée [$dao]. Cette propriété aura (ça reste à montrer) pour valeur, l’objet exporté par le script [./Dao], ç-à-d la classe [Dao] précédente.

Le script principal [main.js]

Le code du script principal [main.js] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 // imports
 import Vue from 'vue'
 import App from './App.vue'
 import axios from 'axios';

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // couche [dao]
 import Dao from './Dao';
 // configuration axios
 axios.defaults.timeout = 2000;
 axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
 axios.defaults.withCredentials = true;
 // instanciation couche [dao]
 const dao = new Dao(axios);

 // plugin [dao]
 import pluginDao from './plugins/dao'
 Vue.use(pluginDao, dao)

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   render: h => h(App),
 }).$mount('#app')

Le script [main.js] :

  • instancie la couche [dao] aux lignes 14-21 ;
  • intègre le plugin [pluginDao] aux lignes 24-25 ;
  • ligne 15 : la classe [Dao] est importée ;
  • lignes 17-18 : on configure l’objet [axios] qui réalise les requêtes HTTP. Cet objet est importé à la ligne 4 ;
    • ligne 17 : définition d’un [timeout] de 2 secondes ;
    • ligne 18 : l’URL du serveur de calcul de l’impôt ;
    • ligne 19 : pour pouvoir échanger des cookies avec le serveur ;
  • lignes 24-25 : utilisation du plugin [pluginDao]
    • ligne 24 : import du plugin ;
    • ligne 25 : intégration du plugin. On voit que le second paramètre de la méthode [Vue.use] est la référence de la couche [dao] définie ligne 21. C’est pour cette raison que la propriété [Vue.$dao] désignera la couche [dao] dans toutes les instances de la classe / fonction [Vue], ç-à-d dans tous les composants [Vue.js] ;

La vue principale [App.vue]

Le code de la vue principale [App] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
 <template>
   <div class="container">
     <b-card>
       <!-- message -->
       <b-alert show variant="success" align="center">
         <h4>[vuejs-10] : plugin [dao], requêtes HTTP asynchrones</h4>
       </b-alert>
       <!-- composant faisant une requête asynchrone au serveur de calcul d'impôt-->
       <Component1 @error="doSomethingWithError" @endWaiting="endWaiting" @beginWaiting="beginWaiting" />
       <!-- affichage d'une éventuelle erreur -->
       <b-alert show
                variant="danger"
                v-if="showError">Evénement [error] intercepté par [App]. Valeur reçue = {{error}}</b-alert>
       <!-- message d'attente avec un spinner -->
       <b-alert show v-if="showWaiting" variant="light">
         <strong>Requête au serveur de calcul d'impôt en cours...</strong>
         <b-spinner variant="primary" label="Spinning"></b-spinner>
       </b-alert>
     </b-card>
   </div>
 </template>

 <script>
   import Component1 from "./components/Component1";
   export default {
     name: "app",
     // état du composant
     data() {
       return {
         // contrôle le spinner d'attente
         showWaiting: false,
         // contrôle l'affichage de l'erreur
         showError: false,
         // l'erreur interceptée
         error: {}
       };
     },
     // composants utilisés
     components: {
       Component1
     },
     // méthodes de gestion des évts
     methods: {
       // début attente
       beginWaiting() {
         // on affiche l'attente
         this.showWaiting = true;
         // on cache le msg d'erreur
         this.showError = false;
       },
       // fin attente
       endWaiting() {
         // on cache l'attente
         this.showWaiting = false;
       },
       // gestion d'erreur
       doSomethingWithError(error) {
         // on note qu'il y a eu erreur
         this.error = error;
         // on affiche le msg d'erreur
         this.showError = true;
       }
     }
   };
 </script>

Commentaires

  • ligne 9 : [Component1] est le composant qui fait la requête HTTP asynchrone. Il peut émettre trois événements :
    • [beginWaiting] : la requête va être faite. Il faut afficher un message d’attente à destination de l’utilisateur ;
    • [endWaiting] : la requête est terminée. Il faut arrêter l’attente ;
    • [error] : la requête s’est mal passée. Il faut afficher un message d’erreur ;
  • lignes 10-13 : l’alerte qui affiche l’éventuel message d’erreur. Elle est contrôlée par le booléen [showError] de la ligne 33. Elle affiche l’erreur de la ligne 35 ;
  • lignes 14-18 : l’alerte qui affiche le message d’attente avec un spinner. Elle est contrôlée par le booléen [showWaiting] de la ligne 47 ;
  • lignes 45-50 : [beginWaiting] est la méthode exécutée à réception de l’événement [beginWaiting]. Elle affiche le message d’attente (ligne 47) et cache le message d’erreur (ligne 49) au cas où celui-ci serait visible suite à une opération précédente ;
  • lignes 52-55 : [endWaiting] est la méthode exécutée à réception de l’événement [endWaiting]. Elle cache le message d’attente (ligne 54) ;
  • lignes 57-62 : [doSomethingWithError] est la méthode exécutée à réception de l’événement [error]. Elle enregistre l’erreur reçue (ligne 59) et affiche le message d’erreur (ligne 61) ;

Le composant [Component1]

Le code du composant [Component1] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
 <template>
   <b-row>
     <b-col>
       <b-alert show
                variant="warning"
                v-if="showMsg">Valeur reçue du serveur = {{data}}</b-alert>
     </b-col>
   </b-row>
 </template>

 <script>
   export default {
     name: "component1",
     // état du composant
     data() {
       return {
         showMsg: false
       };
     },
     // méthodes de gestion des évts
     methods: {
       // traitement de la donnée reçue du serveur
       doSomethingWithData(data) {
         // on enregistre la donnée reçue
         this.data = data;
         // on l'affiche
         this.showMsg = true;
       }
     },
     // le composant vient d'être créé
     created() {
       // on initialise la session avec le serveur - requête asynchrone
       // on utilise la promesse rendue par les méthodes de la couche [dao]
       // on signale le début de l'opération
       this.$emit("beginWaiting");
       // on lance l'opération asynchrone
       this.$dao
         // il s'agit d'initialiser une session jSON avec le serveur de calcul de l'impôt
         .initSession()
         // méthode qui traite la donnée reçue en cas de succès
         .then(data => {
           // on traite la donnée reçue
           this.doSomethingWithData(data);
         })
         // méthode qui traite l'erreur en cas d'erreur
         .catch(error => {
           // on remonte l'erreur au composant parent
           this.$emit("error", error.message);
         }).finally(() => {
           // fin de l'attente
           this.$emit("endWaiting");
         })
     }
   };
 </script>

Commentaires

  • lignes 4-6 : le composant est constitué d’une unique alerte qui affiche la valeur renvoyée par le serveur de calcul de l’impôt, ceci uniquement en cas de succès de la requête HTTP. Cette alerte est contrôlée par le booléen [showMsg] de la ligne 17 ;
  • lignes 31-53 : la requête HTTP est faite dès que le composant a été créé. On met donc son code dans la méthode [created] de la ligne 31 ;
  • ligne 35 : on indique au composant parent que la requête asynchrone va démarrer ;
  • lignes 37-39 : la méthode [this.$dao.initSession] est exécutée. Elle initialise une session jSON avec le serveur de calcul d’impôt. Le résultat immédiat de cette méthode est une [Promise] ;
  • lignes 41-44 : ce code s’exécute lorsque le serveur a rendu son résultat sans erreur. Le résultat du serveur est dans [data]. Ligne 43, on demande à la méthode [doSomethingWithData] de traiter ce résultat ;
  • lignes 46-49 : ce code s’exécute en cas d’erreur lors de l’exécution de la requête. Ligne 48, on indique au composant parent qu’une erreur est survenue et on lui passe le message de l’erreur [error.message] ;
  • lignes 49-52 : ce code s’exécute dans tous les cas. On indique au composant parent que la requête HTTP est terminée ;
  • lignes 23-28 : la méthode [doSomethingWithData] est la méthode chagée d’exploiter la donnée [data] envoyée par le serveur. Ligne 25, on enregistre cette donnée et ligne 27 on l’affiche ;

Exécution du projet

image3

Si lorsqu’on lance le projet, le serveur de calcul d’impôt n’est pas lancé alors on obtient le résultat suivant :

image4

Lançons le serveur [Laragon] (cf https://tahe.developpez.com/tutoriels-cours/php7) et rechargeons la page ci-dessus. Le résultat est alors le suivant :

image5

Note : nous utilisons ici la version 14 du serveur de calcul d’impôt définie au https://tahe.developpez.com/tutoriels-cours/php7.

projet [vuejs-11] : routage et navigation

Le routage est ce qui va permettre à l’utilisateur de naviguer entre les différentes pages de l’application.

L’arborescence du projet est la suivante :

image0

Installation des dépendances

Le routage dans [Vue.js] nécessite la dépendance [vue-router] :

image1

  • en [1-3], on installe la dépendance [vue-router] ;
  • en [4-6], après installation de la dépendance, le fichier [package.json] a été modifié ;

Le script de routage [router.js]

Les règles de navigation de l’application sont inscrites dans le fichier [router.js] (le nom du script peut être quelconque) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 // imports nécessaires au routage
 import Vue from 'vue'
 import VueRouter from 'vue-router'

 // plugin de routage
 Vue.use(VueRouter)

 // les composants cibles du routage
 import Component1 from './components/Component1.vue'
 import Component2 from './components/Component2.vue'
 import Component3 from './components/Component3.vue'


 // les routes de l'application
 const routes = [
   // home
   { path: '/', name: 'home', component: Component1 },
   // Component1
   {
     path: '/vue1', name: 'vue1', component: Component1
   },
   {
     // Component2
     path: '/vue2', name: 'vue2', component: Component2
   },
   // Component3
   {
     path: '/vue3', name: 'vue3', component: Component3
   },
 ]

 // le routeur
 const router = new VueRouter({
   routes,
   mode: 'history',
 })

 // export du routeur
 export default router

Commentaires

  • le script [router.js] va fixer les règles de routage de notre application ;
  • ligne 1-6 : activation du plugin [vue-router] nécessaire au routage. Cette activation nécessite l’import de la classe / fonction [Vue] (ligne 2) et celui du plugin de routage (ligne 3). Ce plugin a été installé avec la dépendance [router] que nous venons d’installer ;
  • lignes 8-11 : import des vues cibles du routage ;
  • lignes 15-21 : définition du tableau des routes. Chaque élément de ce tableau est un objet avec les propriétés suivantes :
    • [path] : l’URL de la vue, celle que nous voulons voir affichée dans le champ [URL] du navigateur. On est libre de mettre ce qu’on veut ;
    • [name] : le nom de la route. Là également on peut mettre ce qu’on veut ;
    • [component] : le composant qui affiche la vue. Là c’est forcément un composant existant ;
On aura donc ici quatre routes [/, /vue1, /vue2, /vue3].
  • lignes 33-36 : le routeur est une instance de la classe [VueRouter] importée ligne 3. Le constructeur de [VueRouter] est ici utilisé avec deux paramètres :
    • les routes de l’application ;
    • un mode d’écriture des URL dans le navigateur : le mode par défaut [hash] écrit les URL sous la forme [localhost:8080/#/vue1] (# est le hash). Le mode [history] enlève le # [localhost:8080/vue1] ;
  • ligne 39 : on exporte le routeur ;

Le script principal [main.js]

Le script principal [main.js] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // routeur
 import monRouteur from './router'

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   name: "app",
   // vue principale
   render: h => h(App),
   // routeur
   router: monRouteur,
 }).$mount('#app')

Commentaires

  • ligne 14 : on importe le routeur exporté par le script [router.js] ;
  • ligne 25 : ce routeur est passé en paramètre du constructeur de la classe [Vue] qui va afficher la vue principale [App], associé à la propriété [router] de la vue ;

La vue principale [App]

Le code de la vue principale est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 <template>
   <div class="container">
     <b-card>
       <!-- un message -->
       <b-alert show variant="success" align="center">
         <h4>[vuejs-11] : routage et navigation</h4>
       </b-alert>
       <!-- la vue courante du routage -->
       <router-view />
     </b-card>
   </div>
 </template>

 <script>
   export default {
     name: "app"
   };
 </script>

Commentaires

  • ligne 9 : affiche la vue courante du routage. La balise <router-view> n’est reconnue que si la propriété [router] de la vue a été initialisée ;

La mise en page des vues

La mise en page des vues est assurée par le composant [Layout] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 <template>
   <!-- ligne -->
   <div>
     <b-row>
       <!-- zone à trois colonnes -->
       <b-col cols="2" v-if="left">
         <slot name="left" />
       </b-col>
       <!-- zone à neuf colonnes -->
       <b-col cols="10" v-if="right">
         <slot name="right" />
       </b-col>
     </b-row>
   </div>
 </template>

 <script>
   export default {
     // paramètres
     props: {
       left: {
         type: Boolean
       },
       right: {
         type: Boolean
       }
     }
   };
 </script>

Nous avons déjà utilisé et expliqué cette mise en page dans le projet [vuejs-06] du paragraphe [vuejs-06].

Le composant de navigation

Le composant [Navigation] offre un menu de navigation à l’utilisateur :

image2

Le composant qui génère le bloc [1] est le suivant :

1
2
3
4
5
6
7
8
 <template>
   <!-- menu Bootstrap à trois options -->
   <b-nav vertical>
     <b-nav-item to="/vue1" exact exact-active-class="active">Vue 1</b-nav-item>
     <b-nav-item to="/vue2" exact exact-active-class="active">Vue 2</b-nav-item>
     <b-nav-item to="/vue3" exact exact-active-class="active">Vue 3</b-nav-item>
   </b-nav>
 </template>
  • ce code génère le bloc 1 de trois liens permettant la navigation ;
  • l’attribut [to] des balises <b-nav-item> doit correspondre à l’une des propriétés [path] des routes du routeur de l’application ;

Les vues

La vue n° 1 est la suivante :

image3

Les zone [3-4] sont générées par le composant [Component1] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 <template>
   <Layout :left="true" :right="true">
     <!-- navigation -->
     <Navigation slot="left" />
     <!-- message-->
     <b-alert show variant="primary" slot="right">Vue 1</b-alert>
   </Layout>
 </template>

 <script>
   import Navigation from './Navigation';
   import Layout from './Layout';

   export default {
     name: "component1",
     // composants utilisés
     components: {
       Layout, Navigation
     }
   };
 </script>

Commentaires

  • ligne 2 : la vue n° 1 utilise la mise en page du composant [Layout] composée de deux slots appelés [left, right] ;
  • ligne 4 : le menu de navigation est placé dans le slot de gauche. C’est la zone [3] qu’on voit ci-dessus ;
  • ligne 6 : un message est placé dans le slot de droite. C’est la zone [4] qu’on voit ci-dessus ;

Les vues 2 et 3 sont analogues.

Vue n° 2 affichée par le composant [Component2] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <!-- vue n° 2 -->
 <template>
   <Layout :left="true" :right="true">
     <!-- navigation -->
     <Navigation slot="left" />
     <!-- message -->
     <b-alert show variant="secondary" slot="right">Vue 2</b-alert>
   </Layout>
 </template>

 <script>
   import Navigation from './Navigation';
   import Layout from './Layout';

   export default {
     name: "component2",
     // composants de la vue
     components: {
       Layout, Navigation
     }
   };
 </script>

Vue n° 3 affichée par le composant [Component3] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 <!-- vue n° 3 -->
 <template>
   <Layout :left="true" :right="true">
     <!-- navigation -->
     <Navigation slot="left" />
     <!-- message -->
     <b-alert show variant="info" slot="right">Vue 3</b-alert>
   </Layout>
 </template>

 <script>
   import Navigation from "./Navigation";
   import Layout from "./Layout";

   export default {
     name: "component3",
     // composants de la vue
     components: {
       Layout,
       Navigation
     }
   };
 </script>

Exécution du projet

image4

A l’exécution la vue suivante s’affiche :

image5

  • en [1], l’URL est [http://localhost:8080]. C’est alors la règle de routage suivante qui s’est exécutée :
1
 { path: '/', name: 'home', component: Component1 }
C’est donc le composant [Component1] qui a été affiché. Il affiche la vue n° 1 [2]. Maintenant cliquons sur le lien [Vue 1] dont le code est le suivant :
1
 <b-nav-item to="/vue1" exact exact-active-class="active">Vue 1</b-nav-item>
L’affichage devient le suivant :
  • en [3], la règle suivante de routage s’est exécutée :

path: “/vue1”, name: “vue1”, component: Component1

C’est donc de nouveau le composant [Component1] qui a été affiché et donc la vue n° 1 [4]. Maintenant cliquons sur le lien [Vue 2] dont le code est le suivant :

<b-nav-item to= »/vue2 » exact exact-active-class= »active »>Vue 2</b-nav-item>

La nouvelle vue est alors la suivante :

image6

  • en [5], la règle suivante de routage s’est exécutée :
1
 path: '/vue2', name: 'vue2', component: Component2
C’est donc le composant [Component2] qui a été affiché et donc la vue n° 2. Si maintenant nous cliquons sur le lien [Vue 3], dont le code est le suivant :
1
 <b-nav-item to="/vue3" exact exact-active-class="active">Vue 3</b-nav-item>
Nous obtenons la nouvelle vue suivante :

image7

  • en [6], la règle de routage suivante s’est exécutée :
1
 path: '/vue3', name: 'vue3', component: Component3

C’est donc le composant [Component3] qui s’est affiché, ç-à-d la vue n° 3 [8].

projet [vuejs-12] : gestion des tables HTML

L’arborescence du projet [vuejs-12] est la suivante :

image0

Le script principal [main.js]

Le script principal redevient ce qu’il était dans les premiers projets :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   name: "app",
   render: h => h(App),
 }).$mount('#app')

La vue principale [App]

Le code de la vue principale [App] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
 <template>
   <div class="container">
     <b-card>
       <!-- message -->
       <b-alert show variant="success" align="center">
         <h4>[vuejs-12] : gestion des tables</h4>
       </b-alert>
       <!-- composant Table -->
       <Table @supprimerLigne="supprimerLigne" :lignes="lignes" @rechargerListe="rechargerListe" />
     </b-card>
   </div>
 </template>

 <script>
   import Table from "./components/Table";
   export default {
     // nom
     name: "app",
     // composants utilisés
     components: {
       Table
     },
     // état interne
     data() {
       return {
         // liste des simulations
         lignes: [
           { id: 3, marié: "oui", enfants: 2, salaire: 35000, impôt: 1200 },
           { id: 5, marié: "non", enfants: 2, salaire: 35000, impôt: 1900 },
           { id: 7, marié: "non", enfants: 0, salaire: 30000, impôt: 2000 }
         ]
       };
     },

     // méthodes
     methods: {
       // suppression d'une ligne
       supprimerLigne(index) {
         // eslint-disable-next-line
         console.log("App supprimerLigne", index);
         // suppression ligne à la position [index]
         this.lignes.splice(index, 1);
       },
       // rechargement de la table des lignes
       rechargerListe() {
         // eslint-disable-next-line
         console.log("App rechargerListe");
         // on régénère la liste des lignes
         this.lignes = [
           { id: 3, marié: "oui", enfants: 2, salaire: 35000, impôt: 1200 },
           { id: 5, marié: "non", enfants: 2, salaire: 35000, impôt: 1900 },
           { id: 7, marié: "non", enfants: 0, salaire: 30000, impôt: 2000 }
         ];
       }
     }
   };
 </script>

Commentaires

  • la vue principale affiche un composant [Table] (lignes 9, 15, 21) ;
  • ligne 9 : le composant [Table] admet le paramètre [:lignes] qui représente les lignes à afficher dans une table HTML. Ces lignes sont définies par les lignes 27-31 du code ;
  • ligne 9 : le composant [Table]est susceptible d’émettre deux événements :
    • [supprimerLigne] : pour supprimer une ligne dont on donne l’index (ligne 38) ;
    • [rechargerListe] : pour régénérer la liste des lignes 27-31. En effet, nous allons voir que l’utilisateur peut supprimer certaines des lignes affichées ;
  • lignes 38-43 : la méthode chargée de gérer l’événement [supprimerLigne]. Elle reçoit en paramètre l’index de la ligne à supprimer ;
  • lignes 45-54 : la méthode chargée de gérer l’événement [rechargerListe]. En modifiant l’attribut [lignes] de la ligne 27, on provoque la mise à jour du composant [Table] de la ligne 9, puisqu’il a un paramètre [:lignes= »lignes »] ;

Le composant [Table]

Le composant [Table] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
 <template>
   <div>
     <!-- liste vide -->
     <template v-if="lignes.length==0">
       <b-alert show variant="warning">
         <h4>Votre liste de simulations est vide</h4>
       </b-alert>
       <!-- bouton de rechargement-->
       <b-button variant="primary" @click="rechargerListe">Recharger la liste</b-button>
     </template>
     <!-- liste non vide-->
     <template v-if="lignes.length!=0">
       <b-alert show variant="primary" v-if="lignes.length==0">
         <h4>Liste de vos simulations</h4>
       </b-alert>
       <!-- tableau des simulations -->
       <b-table striped hover responsive :items="lignes" :fields="fields">
         <template v-slot:cell(action)="row">
           <b-button variant="link" @click="supprimerLigne(row.index)">Supprimer</b-button>
         </template>
       </b-table>
     </template>
   </div>
 </template>

 <script>
   export default {
     // propriétés
     props: {
       lignes: {
         type: Array
       }
     },
     // état interne
     data() {
       return {
         fields: [
           { label: "#", key: "id" },
           { label: "Marié", key: "marié" },
           { label: "Nombre d'enfants", key: "enfants" },
           { label: "Salaire", key: "salaire" },
           { label: "Impôt", key: "impôt" },
           { label: "", key: "action" }
         ]
       };
     },
     // méthodes
     methods: {
       // suppression d'une ligne
       supprimerLigne(index) {
         // eslint-disable-next-line
         console.log("Table supprimerLigne", index);
         // on passe l'information au composant parent
         this.$emit("supprimerLigne", index);
       },
       // rechargement de la liste affichée
       rechargerListe() {
         // eslint-disable-next-line
         console.log("Table rechargerListe");
         // on émet un événement vers le composant parent
         this.$emit("rechargerListe");
       }
     }
   };
 </script>

Commentaires

Ce composant a deux états :

  1. il affiche une liste dans un tableau HTML ;
  2. ou bien un message indiquant que la liste à afficher est vide ;

Le 1er état est affiché si la condition [lignes.length!=0] est vérifiée (ligne 12) :

image1

Le second est affiché si la condition [lignes.length==0] est vérifiée (ligne 4).

image2

  • [lignes] est un paramètre d’entrée du composant (lignes 29-33) ;
  • lignes 4-10 : au lieu d’introduire un bloc de code avec une balise <div>, on a ici utilisé une balise <template>. la différence entre les deux est que la balise <template> n’est pas insérée dans le code HTML généré ;
  • ligne 9 : lorsque la liste affichée par la table HTML est vide, un bouton propose de la régénérer. Lorsqu’on clique sur ce bouton, la méthode [rechargerListe] des lignes 57-62 est exécutée. Celle-ci se contente d’émettre vers le composant parent l’événement [rechargerListe] qui demande au parent de régénérer la liste affichée par la table HTML ;
  • lignes 12-22 : le code affiché lorsque la liste à afficher n’est pas vide ;
  • ligne 17 : la balise <b-table> est la balise qui génère une table HTML. Les attributs utilisés ici sont les suivants :
    • [striped] : la couleur de fond des lignes alterne. Il y a une couleur pour les lignes paires et une autre pour les lignes impaires. Cela améliore la visibilité ;
    • [hover] : la ligne sur laquelle on passe la souris change de couleur ;
    • [responsive] : la taille de la table s’adapte à l’écran qui l’affiche ;
    • [:items] : le tableau des éléments à afficher. Ici le tableau [lignes] passé en paramètre au composant (lignes 30-32) ;
    • [:fields] : un tableau définissant la mise en page de la table HTML (lignes 37-44) ;
      • chaque élément du tableau [fields] définit une colonne de la table HTML ;
      • [label] : désigne le titre de la colonne ;
      • [key] : désigne le contenu de la colonne ;
  • ligne 38 : définit la colonne 0 de la table HTML :
    • [#] : est le titre de la colonne ;
    • [id] : est son contenu. C’est le champ [id] de la ligne affichée qui remplira la colonne 0 ;
  • ligne 39 : définit la colonne 1 de la table HTML :
    • [Marié] : est le titre de la colonne ;
    • [marié] : est son contenu. C’est le champ [marié] de la ligne affichée qui remplira la colonne 0 ;
  • ligne 43 : définit la dernière colonne de la table HTML :
    • la colonne n’a pas de titre ;
    • son contenu est défini par un champ [action], un champ qui n’existe pas dans les lignes affichées. Cette clé est référencée ligne 18 du [template]. La clé est donc utilisée ici uniquement pour identifier une colonne ;
  • lignes 18-20 : ce code sert à définir la dernière colonne de la table HTML, celle de clé [action] :

<template v-slot:cell(action)= »row »>

La syntaxe [v-slot:cell(action)] désigne la colonne de clé [action]. Cette syntaxe permet de définir une colonne lorsque la syntaxe du tableau [fields] n’est pas suffisante pour la décrire. Ici nous voulons que la dernière colonne contienne un lien permettant de supprimer une ligne de la table HTML :

image3

Dans la syntaxe [<template v-slot:cell(action)= »row »>], le nom [row] désigne la ligne de la table. On peut utiliser le nom que l’on veut. On aurait pu écrire [<template v-slot:cell(action)= »ligne »>] ;

  • ligne 19 : un bouton <b-button> affiché comme un lien [variant=’link’]. Un clic sur ce lien provoque l’exécution de la méthode [supprimerLigne(row.index)]. Ici [row] est le nom donné à la ligne de la table HTML, ligne 18 du code ;
  • lignes 50-55 : la méthode [supprimerLigne] ;
  • ligne 54 : on émet l’événement [supprimerLigne] vers le composant parent accompagné du n° de la ligne à supprimer ;
  • lignes 57-62 : la méthode [rechargerListe] ;
  • ligne 61 : on émet l’événement [rechargerListe] vers le composant parent ;

Exécution du projet

image4

La 1ère vue affichée est la suivante :

image5

Après avoir supprimé la ligne 1 [1], la vue devient la suivante :

image6

Après avoir supprimé toutes les lignes :

image7

Après avoir cliqué sur le bouton [2], on obtient de nouveau la liste :

image8

projet [vuejs-13] : mise à jour d’un composant, utilisation d’une session

Le projet [vuejs-13] reprend le projet [vuejs-12] en amenant la modification suivante : le tableau affiché par la table HTML est défini dans un objet [session] connu de tous les composants. C’est donc une façon de partager de l’information entre composants. Ce concept est directement inspiré de la session web. Nous utilisons la méthode du plugin pour rendre disponible cet objet partagé dans un attribut [Vue.$session].

L’arborescence du projet est la suivante :

image0

L’objet [session]

L’objet [session] partagé par tous les composants est défini dans le script [./session.js] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 const session = {
   // liste des simulations
   get lignes() {
     return this._lignes;
   },
   // génération de la liste des simulations
   generateLignes() {
     this._lignes =
       [
         { id: 3, marié: "oui", enfants: 2, salaire: 35000, impôt: 1200 },
         { id: 5, marié: "non", enfants: 2, salaire: 35000, impôt: 1900 },
         { id: 7, marié: "non", enfants: 0, salaire: 30000, impôt: 2000 }
       ]
   },
   // suppression ligne n° index
   deleteLigne(index) {
     this._lignes.splice(index, 1);
   }
 }
 // export de l'objet [session]
 export default session;

Le plugin [./plugins/pluginSession]

Le script [pluginSession] est le suivant :

1
2
3
4
5
6
7
8
9
 export default {
   install(Vue, session) {
     // ajoute une propriété [$session] à la classe vue
     Object.defineProperty(Vue.prototype, '$session', {
       // lorsque Vue.$session est référencé, on rend le 2ième paramètre [session] de la méthode [install]
       get: () => session,
     })
   }
 }
  • ligne 4 : l’objet partagé [session] sera disponible dans la propriété [$session] de tous les composants ;

Le script principal [main.js]

Le script principal [main.js] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // session
 import session from './session';
 import pluginSession from './plugins/pluginSession'
 Vue.use(pluginSession, session)

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   name: "app",
   render: h => h(App),
 }).$mount('#app')
  • lignes 14-16 : le plugin [pluginSession] est intégré au framework [Vue.js] ;
  • après la ligne 16, l’attribut [$session] est disponible pour tous les composants ;

La vue principale [App]

La vue [App] est désormais la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 <template>
   <div class="container">
     <b-card>
       <!-- message -->
       <b-alert show variant="success" align="center">
         <h4>[vuejs-13] : mise à jour d'un composant, partage des données avec une session</h4>
       </b-alert>
       <!-- table HTML -->
       <Table @updateTable="updateTable" :key="versionTable"/>
     </b-card>
   </div>
 </template>

 <script>
 import Table from "./components/Table";
 export default {
   // nom
   name: "app",
   // composants
   components: {
     Table
   },
   // état interne
   data() {
     return {
       // version table
       versionTable: 1
     };
   },

   // méthodes
   methods: {
     updateTable() {
       // eslint-disable-next-line
       console.log("App updateTable");
       // incrément version table
       this.versionTable++;
     }
   }
 };
 </script>

Commentaires

  • la vue [App] ne gère plus désormais le tableau affiché par le composant [Table] de la ligne 9 ;
  • ligne 9 : le composant [Table] émet l’événement [updateTable] qui demande à ce que le composant [Table] soit régénéré. Une façon de faire cela est d’utiliser l’attribut [:key]. On donne à cet attribut une valeur modifiable. A chaque fois qu’elle est modifiée, le composant [Table] est régénéré ;
  • ligne 9 : la valeur de l’attribut [:key] est l’attribut [versionTable] de la ligne 27. La méthode [updateTable] (lignes 33-38) est chargée de régénérer le composant [Table] de la ligne 9. Pour cela, la méthode incrémente la valeur de l’attribut [:key] du composant [Table], ligne 37. Le composant [Table] est alors automatiquement régénéré ;

Le composant [Table]

Le composant [Table] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 <template>
   <div>
     <!-- liste vide -->
     <template v-if="lignes.length==0">
       <b-alert show variant="warning">
         <h4>Votre liste de simulations est vide</h4>
       </b-alert>
       <!-- bouton de rechargement-->
       <b-button variant="primary" @click="rechargerListe">Recharger la liste</b-button>
     </template>
     <!-- liste non vide-->
     <template v-if="lignes.length!=0">
       <b-alert show variant="primary" v-if="lignes.length==0">
         <h4>Liste de vos simulations</h4>
       </b-alert>
       <!-- tableau des simulations -->
       <b-table striped hover responsive :items="lignes" :fields="fields">
         <template v-slot:cell(action)="row">
           <b-button variant="link" @click="supprimerLigne(row.index)">Supprimer</b-button>
         </template>
       </b-table>
     </template>
   </div>
 </template>

 <script>
 export default {
   // état calculé
   computed: {
     lignes() {
       return this.$session.lignes;
     }
   },
   // état interne
   data() {
     return {
       fields: [
         { label: "#", key: "id" },
         { label: "Marié", key: "marié" },
         { label: "Nombre d'enfants", key: "enfants" },
         { label: "Salaire", key: "salaire" },
         { label: "Impôt", key: "impôt" },
         { label: "", key: "action" }
       ]
     };
   },
   // méthodes
   methods: {
     supprimerLigne(index) {
       // eslint-disable-next-line
       console.log("Table supprimerLigne", index);
       // on supprime la ligne
       this.$session.deleteLigne(index);
       // on demande au composant parent de mettre à jour la vue
       this.$emit("updateTable");
     },
     // rechargement de la liste affichée
     rechargerListe() {
       // eslint-disable-next-line
       console.log("Table rechargerListe");
       // on régènère la liste des simulations
       this.$session.generateLignes();
       // on demande au composant parent de mettre à jour la vue
       this.$emit("updateTable");
     }
   }
 };
 </script>

Commentaires :

  • l’attribut [lignes] (lignes 4, 12, 17) n’est plus un paramètre fixé par le composant parent mais un attribut calculé du composant [Table] (lignes 30-32). [lignes] est alors le tableau [$session.lignes] (ligne 31) ;
  • lignes 49-56 : la méthode [supprimerLigne] fait supprimer une ligne du tableau [$session.lignes]. Cette suppression ne change pas, par défaut, l’affichage de la table HTML. En effet, les éléments de [$session] ne sont pas réactifs : leur modification n’est pas répercutée sur les composants qui les utilisent. Pour cette raison, le composant [Table] demande à son parent de le régénérer au moyen de l’événement [updateTable] (ligne 55). On a vu que le composant parent allait alors incrémenter l’attribut [:key] du composant [Table] pour forcer sa régénération ;
  • lignes 58-65 : la méthode [rechargerListe] demande à l’objet [$session] de régénérer le tableau [$session.lignes]. Pour la même raison que précédemment, cette modification de [$session.liste] ne change pas, par défaut, l’affichage de la table HTML. Pour cette raison, le composant [Table] demande à son parent de le régénérer au moyen de l’événement [updateTable] (ligne 64).

Exécution du projet

image1

On obtient les mêmes résultats que dans le projet [vuejs-12].

projet [vuejs-14] : rendre la session réactive

On a vu que l’objet [session] utilisé dans le projet précédent avait des propriétés non réactives : si on les modifie, les vues utilisant ces propriétés ne sont pas mise à jour. Il est possible d’avoir un objet [session] réactif si on le stocke dans les données réactives des vues. C’est ce que montre le projet [vuejs-14].

L’arborescence du projet est la suivante :

image0

L’objet [session]

L’objet [session] partagé par tous les composants ne change pas.

Le plugin [./plugins/pluginSession]

Le script [pluginSession] ne change pas. L’objet partagé [session] est disponible dans la propriété [$session] de tous les composants.

Le script principal [main.js]

Le script principal [main.js] ne change pas.

La vue principale [App]

La vue [App] est désormais la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 <template>
   <div class="container">
     <b-card>
       <!-- message -->
       <b-alert show variant="success" align="center">
         <h4>[vuejs-14] : utilisation d'un objet partagé entre composants</h4>
       </b-alert>
       <!-- table HTML -->
       <Table/>
     </b-card>
   </div>
 </template>

 <script>
 import Table from "./components/Table";
 export default {
   // nom
   name: "app",
   // composants
   components: {
     Table
   },
   // cycle de vie
   created() {
     // génération du tableau des simulations
     this.$session.generateLignes();
   }
 };
 </script>

Commentaires

  • ligne 9 : le composant [Table] n’émet plus l’événement [updateTable] qui demande à ce que le composant [Table] soit régénéré. Du coup, la méthode [updateTable] a disparu ;

Le composant [Table]

Le composant [Table] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
 <template>
   <div>
     <!-- liste vide -->
     <template v-if="lignes.length==0">
       <b-alert show variant="warning">
         <h4>Votre liste de simulations est vide</h4>
       </b-alert>
       <!-- bouton de rechargement-->
       <b-button variant="primary" @click="rechargerListe">Recharger la liste</b-button>
     </template>
     <!-- liste non vide-->
     <template v-if="lignes.length!=0">
       <b-alert show variant="primary" v-if="lignes.length==0">
         <h4>Liste de vos simulations</h4>
       </b-alert>
       <!-- tableau des simulations -->
       <b-table striped hover responsive :items="lignes" :fields="fields">
         <template v-slot:cell(action)="row">
           <b-button variant="link" @click="supprimerLigne(row.index)">Supprimer</b-button>
         </template>
       </b-table>
     </template>
   </div>
 </template>

 <script>
 export default {
   // état calculé
   computed: {
     lignes() {
       return this.session.lignes;
     }
   },
   // état interne
   data() {
     return {
       fields: [
         { label: "#", key: "id" },
         { label: "Marié", key: "marié" },
         { label: "Nombre d'enfants", key: "enfants" },
         { label: "Salaire", key: "salaire" },
         { label: "Impôt", key: "impôt" },
         { label: "", key: "action" }
       ],
       session : {}
     };
   },
   // cycle de vie
   created(){
     this.session=this.$session
   },
   // méthodes
   methods: {
     supprimerLigne(index) {
       // eslint-disable-next-line
       console.log("Table supprimerLigne", index);
       // on supprime la ligne
       this.session.deleteLigne(index);
     },
     // rechargement de la liste affichée
     rechargerListe() {
       // eslint-disable-next-line
       console.log("Table rechargerListe");
       // on régènère la liste des simulations
       this.session.generateLignes();
     }
   }
 };
 </script>

Commentaires :

  • la nouveauté est lignes 49-51 : lorsque la vue est créée, la session [this.$session] est stockée dans la propriété [session] de la ligne 45. Placée ici, la propriété [session] est réactive ;
  • lignes 58 et 65 : au lieu d’utiliser [this.$session] pour ajouter / supprimer une ligne de la table, on utilise la propriété réactive [this.session] ;

Exécution du projet

image1

On obtient les mêmes résultats que dans le projet [vuejs-12].

projet [vuejs-15] : utilisation du plugin [Vuex]

Le projet [vuejs-15] reprend le projet [vuejs-14] en utilisant un objet [session] réactif généré par [Vuex]. L’aborescence du projet est la suivante :

image0

Installation de la dépendance [vuex]

image1

Le script [./session.js]

L’objet [session] devient le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 // plugin Vuex
 import Vue from 'vue'
 import Vuex from 'vuex'
 Vue.use(Vuex);

 // la session est un store Vuex
 const session = new Vuex.Store({
   state: {
     // le tableau des simulations
     lignes: []
   },
   mutations: {
     // génération de la liste des simulations
     generateLignes(state) {
       // eslint-disable-next-line no-console
       console.log("mutation generateLignes");
       // on initialise [state.lignes]
       state.lignes =
         [
           { id: 3, marié: "oui", enfants: 2, salaire: 35000, impôt: 1200 },
           { id: 5, marié: "non", enfants: 2, salaire: 35000, impôt: 1900 },
           { id: 7, marié: "non", enfants: 0, salaire: 30000, impôt: 2000 }
         ];
       // eslint-disable-next-line no-console
       console.log(state.lignes);
     },
     // suppression ligne n° index
     deleteLigne(state, index) {
       // eslint-disable-next-line no-console
       console.log("mutation deleteLigne");
       // on supprime la ligne n° [index]
       state.lignes.splice(index, 1);
     }
   }
 });
 // export de l'objet [session]
 export default session;

Commentaires

  • lignes 2-4 : on intègre le plugin [Vuex] au framework [Vue] ;
  • ligne 7 : la session devient un objet de type [Vuex.Store] ;
  • lignes 8-11 : la propriété [state] contient l’état partagé de l’application [Vue]. Cette propriété sera accessible à tous les composants de l’application. Ici nous partageons le tableau des simulations [lignes] (ligne 10) ;
  • lignes 12-35 : la propriété [mutations] rassemble les méthodes qui modifie le contenu de l’objet [state] ;
  • lignes 14-26 : la propriété [generateLignes] est une fonction générant une valeur initiale pour la propriété [state.lignes]. Elle admet ici [state] comme paramètre. Lignes 18-23 : la propriété [state.lignes] est initialisée ;
  • lignes 28-35 : la propriété [deleteLigne] est une fonction supprimant une ligne du tableau [state.lignes]. Elle a pour paramètres :
    • [state] qui représente l’objet des lignes 8-11 ;
    • [index] qui est le n° de la ligne à supprimer ;
  • les fonctions de la propriété [mutations] admettent toujours comme 1er paramètre, un objet représentant la propriété [state] de la ligne 8. Les paramètres suivants sont fournis par le code appelant la mutation ;
  • ligne 37 : l’objet [session] est exporté.

Contrairement au projet précédent [vuejs-13] nous n’aurons pas ici de plugin pour rendre la session accessible aux composants dans un attribut [Vue.$session].

Le script principal [./main.js]

Le script principal évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // imports
 import Vue from 'vue'
 import App from './App.vue'

 // plugins
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // session
 import session from './session';

 // configuration
 Vue.config.productionTip = false

 // instanciation projet [App]
 new Vue({
   name: "app",
   // utilisation store de Vuex
   store: session,
   render: h => h(App),
 }).$mount('#app')

Commentaires

  • ligne 14 : la session est importée ;
  • ligne 23 : elle est passée à la vue principale dans un attribut nommé [store] (c’est imposé). Grâce au plugin [Vuex], cet attribut devient alors disponible à tous les composants dans un attribut [Vue.$store]. On est donc dans une configuration très proche de celle du projet précédent : là où dans un composant on accédait à la session via la notation [this.$session], on y accèdera maintenant via la notation [this.$store] ;

La vue principale [App]

La vue principale [App] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 <template>
   <div class="container">
     <b-card>
       <!-- message -->
       <b-alert show variant="success" align="center">
         <h4>[vuejs-14] : utilisation du plugin [Vuex]</h4>
       </b-alert>
       <!-- table HTML -->
       <Table />
     </b-card>
   </div>
 </template>

 <script>
 import Table from "./components/Table";
 export default {
   // nom
   name: "app",
   // composants
   components: {
     Table
   },
   // cycle de vie
   created() {
     // génération du tableau des simulations
     this.$store.commit("generateLignes");
   }
 };
 </script>

Commentaires

  • ligne 9 : la vue [App] utilise le composant [Table] mais ne reçoit plus d’événements de sa part, ceci grâce au fait que le store [Vuex] est réactif ;
  • lignes 24-27 : la méthode [created] est exécutée juste après la création du composant [App]. Dans celle-ci, on exécute la mutation nommée [generateLignes] qui génère une valeur initiale pour le tableau des simulations. On notera la syntaxe particulière de l’instruction. On rappelle que la notation [this.$store] fait référence à la propriété [store] de la vue instanciée dans [main.js] :
1
2
3
4
5
6
7
 // instanciation vue [App]
 new Vue({
   name: "app",
   // utilisation store de Vuex
   store: session,
   render: h => h(App),
 }).$mount('#app')
La notation [this.$store] désigne donc l’objet [session]. On écrit ensuite [this.$store.commit(« generateLignes »)] pour exécuter la mutation s’appelant [generateLignes]. Cette mutation est une fonction ;

Le composant [Table]

Le composant [Table] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
 <template>
   <div>
     <!-- liste vide -->
     <template v-if="lignes.length==0">
       <b-alert show variant="warning">
         <h4>Votre liste de simulations est vide</h4>
       </b-alert>
       <!-- bouton de rechargement-->
       <b-button variant="primary" @click="rechargerListe">Recharger la liste</b-button>
     </template>
     <!-- liste non vide-->
     <template v-if="lignes.length!=0">
       <b-alert show variant="primary" v-if="lignes.length==0">
         <h4>Liste de vos simulations</h4>
       </b-alert>
       <!-- tableau des simulations -->
       <b-table striped hover responsive :items="lignes" :fields="fields">
         <template v-slot:cell(action)="row">
           <b-button variant="link" @click="supprimerLigne(row.index)">Supprimer</b-button>
         </template>
       </b-table>
     </template>
   </div>
 </template>

 <script>
 export default {
   // état calculé
   computed: {
     lignes() {
       return this.$store.state.lignes;
     }
   },
   // état interne
   data() {
     return {
       fields: [
         { label: "#", key: "id" },
         { label: "Marié", key: "marié" },
         { label: "Nombre d'enfants", key: "enfants" },
         { label: "Salaire", key: "salaire" },
         { label: "Impôt", key: "impôt" },
         { label: "", key: "action" }
       ]
     };
   },
   // méthodes
   methods: {
     supprimerLigne(index) {
       // eslint-disable-next-line
       console.log("Table supprimerLigne", index);
       // on supprime la ligne
       this.$store.commit("deleteLigne", index);
     },
     // rechargement de la liste affichée
     rechargerListe() {
       // eslint-disable-next-line
       console.log("Table rechargerListe");
       // on régènère la liste des simulations
       this.$store.commit("generateLignes");
     }
   }
 };
 </script>

Commentaires

  • le [template] des lignes 1-24 ne change pas ;
  • lignes 30-32 : la propriété calculée [lignes] utilise désormais le [store] de [Vuex] ;
  • lignes 49-54 : pour supprimer une ligne de la table HTML, on utilise la mutation [deleteLigne] du [store] de [Vuex]. On passe en paramètre le n° [index] de la ligne à supprimer (ligne 53) ;
  • lignes 56-61 : pour recharger la table HTML avec une nouvelle liste, on utilise la mutation [generateLignes] du [store] de [Vuex] ;

Conclusion

Les attributs [Vue.$session] du projet [vuejs-13] et [Vue.$store] du projet [vuejs-15] sont très proches l’un de l’autre. Ils visent le même objectif : partager de l’information entre vues. L’avantage de l’objet [store] est d’être réactif alors que l’objet [session] ne l’est pas. Mais le projet [vuejs-14] a montré qu’il était aisé de rendre réactif l’objet [session] en le dupliquant dans les propriétés réactives des vues.

Client Vue.js du serveur de calcul de l’impôt

Architecture

Nous allons implémenter une application client / serveur avec l’architecture suivante :

image0

Le serveur de calcul de l’impôt sera la version 14 développée dans le document |https://tahe.developpez.com/tutoriels-cours/php7|

Les vues de l’application

Les vues de l’application [vuejs-10] sont celles de la version 13 du document |https://tahe.developpez.com/tutoriels-cours/php7| du serveur de calcul de l’impôt lorsqu’il est utilisé en mode HTML. Mais dans l’application présente, ces vues seront générées par le client Javascript et non par le serveur PHP.

La 1ère vue est la vue d’authentification :

image1

La seconde vue est celle du calcul de l’impôt :

image2

La 3ième vue est celle qui affiche la liste des simulations faites par l’utilisateur :

image3

L’écran ci-dessus montre qu’on peut supprimer la simulation n° 1. On obtient alors la vue suivante :

image4

Si on supprime maintenant la dernière simulation, on obtient la nouvelle vue suivante :

image5

Eléments du projet [vuejs-20]

L’arborescence du projet [vuejs-20] est la suivante :

image6

Les éléments du projet sont les suivants :

  • [assets/logo.jpg] : le logo du projet ;
  • [couches] : les couches [métier] et [dao] de l’application ;
  • [plugins] : les plugins de l’application ;
  • [views] : les vues de l’application ;
  • [config.js] : configure l’application ;
  • [router.js] : définit le routage de l’application ;
  • [store.js] : le store de [Vuex] ;
  • [main.js] : le script principal de l’application ;

Les couches [métier] et [dao]

La couche [dao]

La couche [dao] est implémentée par la classe [Dao] du paragraphe |vuejs-10|

La couche [métier]

La couche [métier] est implémentée par la classe [Métier] du document |https://tahe.developpez.com/tutoriels-cours/php7|. On y a ajouté la méthode [setTaxAdminData] suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 // constructeur
   constructor(taxAdmindata) {
     // this.taxAdminData : données de l'administration fiscale
     this.taxAdminData = taxAdmindata;
   }

   // setter
   setTaxAdminData(taxAdmindata) {
     // this.taxAdminData : données de l'administration fiscale
     this.taxAdminData = taxAdmindata;
 }

La méthode [setTaxAdminData] fait la même chose que le constructeur. Sa présence permet la séquence suivante :

  1. instancier la classe [Métier] avec une instruction [métier=new Métier()] lorsqu’on veut instancier la classe mais qu’on n’a pas encore la donnée [taxAdminData] ;
  2. puis renseigner ultérieurement sa propriété [taxAdminData] par une opération [métier.setTaxAdminData(taxAdmindata)] ;

Le fichier de configuration [config]

Le fichier [config.js] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 // utilisation de la bibliothèque [axios]
 const axios = require('axios');
 // timeout des requêtes HTTP
 axios.defaults.timeout = 2000;
 // la base des URL du serveur de calcul de l'impôt
 // le schéma [https] pose des problèmes à Firefox parce que le serveur de calcul
 // de l'impôt envoie un certificat autosigné. ok avec Chrome et Edge. Safari pas testé.
 axios.defaults.baseURL = 'https://localhost/php7/scripts-web/impots/version-14';
 // on va utiliser des cookies
 axios.defaults.withCredentials = true;

 // export de la configuration
 export default {
   axios: axios
 }

Cette configuration est celle de la bibliothèque [axios] que la couche [dao] utilise pour faire ses requêtes HTTP. On notera ligne 8, que le serveur opère sur port sécurisé [https].

Les plugins

Les plugins [pluginDao, pluginMétier, pluginConfig] ont pour but de créer trois nouvelles propriétés à la fonction / classe [Vue] :

  • [$dao] : aura pour valeur une instance de la classe [Dao] ;
  • [$métier] : aura pour valeur une instance de la classe [Métier] ;
  • [$config] : aura pour valeur l’objet exporté par le fichier de configuration [config] ;

[pluginDao]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 export default {
   install(Vue, dao) {
     // ajoute une propriété [$dao] à la classe Vue
     Object.defineProperty(Vue.prototype, '$dao', {
       // lorsque Vue.$dao est référencé, on rend le 2ième paramètre [dao]
       get: () => dao,
     })
   }
 }

 [pluginMétier]

 export default {
   install(Vue, métier) {
     // ajoute une propriété [$métier] à la classe Vue
     Object.defineProperty(Vue.prototype, '$métier', {
       // lorsque Vue.$métier est référencé, on rend le 2ième paramètre [métier]
       get: () => métier,
     })
   }
 }

[pluginConfig]

1
2
3
4
5
6
7
8
9
 export default {
   install(Vue, config) {
     // ajoute une propriété [$config] à la classe vue
     Object.defineProperty(Vue.prototype, '$config', {
       // lorsque Vue.$config est référencé, on rend le 2ième paramètre [config]
       get: () => config,
     })
   }
 }

Le store [Vuex]

Le store de [Vuex] est implémenté par le fichier [store] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 // plugin Vuex
 import Vue from 'vue'
 import Vuex from 'vuex'
 Vue.use(Vuex);

 // store Vuex
 const store = new Vuex.Store({
   state: {
     // le tableau des simulations
     simulations: [],
     // le n° de la dernière simulation
     idSimulation: 0
   },
   mutations: {
     // suppression ligne n° index
     deleteSimulation(state, index) {
       // eslint-disable-next-line no-console
       console.log("mutation deleteSimulation");
       // on supprime la ligne n° [index]
       state.simulations.splice(index, 1);
       // eslint-disable-next-line no-console
       console.log("store simulations", state.simulations);
     },
     // ajout d'une simulation
     addSimulation(state, simulation) {
       // eslint-disable-next-line no-console
       console.log("mutation addSimulation");
       // n° de la simulation
       state.idSimulation++;
       simulation.id = state.idSimulation;
       // on ajoute la simulation au tableau des simulations
       state.simulations.push(simulation);
     },
     // nettoyage state
     clear(state) {
       state.simulations = [];
       state.idSimulation = 1;
     }
   }
 });
 // export de l'objet [store]
 export default store;

Commentaires

  • lignes 2-4 : le plugin [Vuex] est intégré au framework [Vue] ;
  • lignes 8-13 : nous mettons dans le store de [Vuex] les éléments suivants :
    • [simulations] : la liste des simulations faites par l’utilisateur ;
    • [idSimulation] : le n° de la dernière simulation faite par l’utilisateur ;
On rappelle que le store va être partagé entre les vues et que son contenu est réactif : lorsqu’il est modifié, les vues qui l’utilisent sont automatiquement mises à jour. Dans notre application, seul l’élément [simulations] a besoin d’être réactif, pas l’élément [idSimulation]. On a laissé cet élément dans le store par commodité ;
  • lignes 14-40 : les mutations autorisées sur l’objet [state] des lignes 8-13. On rappelle que celles-ci reçoivent toujours l’objet [state] des lignes 8-13 en 1er paramètre ;
    • ligne 16 : la mutation [deleteSimulation] permet de supprimer une simulation dont on donne le n° [index] ;
    • ligne 25 : la mutation [addSimulation] permet d’ajouter une nouvelle simulation au tableau des simulations ;
    • ligne 35 : la mutation [clear] permet de réinitialiser l’objet [state] des lignes 8-13 ;

Le fichier de routage [router]

Le fichier de routage est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 // imports
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 // les vues
 import Authentification from './views/Authentification'
 import CalculImpot from './views/CalculImpot'
 import ListeSimulations from './views/ListeSimulations'

 // plugin de routage
 Vue.use(VueRouter)

 // les routes de l'application
 const routes = [
   // authentification
   {
     path: '/', name: 'authentification', component: Authentification
   },
   // calcul de l'impôt
   {
     path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
   },
   // liste des simulations
   {
     path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
   },
   // fin de session
   {
     path: '/fin-session', name: 'finSession', component: Authentification
   }
 ]

 // le routeur
 const router = new VueRouter({
   // les routes
   routes,
   // le mode d'affichage des routes dans le navigateur
   mode: 'history',
 })

 // export du router
 export default router

Commentaires

  • ligne 16 : au démarrage de l’application, c’est la vue [Authentification] qui est affichée car son URL est la racine [/] ;
  • ligne 20 : la vue [CalculImpot] est affichée lorsque l’URL [/calcul-impot] est demandée ;
  • ligne 24 : la vue [ListeSimulations] est affichée lorsque l’URL [/liste-des-simualtions] est demandée ;
  • ligne 28 : la vue [Authentification] est affichée lorsque l’URL [/fin-session] est demandée ;
  • lignes 33-38 : un objet [router] est créé avec ces routes (ligne 35) et le mode [history] (ligne 37) de gestion des URL ;
  • ligne 41 : ce routeur est exporté ;

Le script principal [main.js]

Le script [main.js] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 // imports
 import Vue from 'vue'

 // vue principale
 import Main from './views/Main.vue'

 // plugin [bootstrap-vue]
 import BootstrapVue from 'bootstrap-vue'
 Vue.use(BootstrapVue);

 // CSS bootstrap
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'

 // routeur
 import router from './router'

 // plugin [config]
 import config from './config';
 import pluginConfig from './plugins/pluginConfig'
 Vue.use(pluginConfig, config)

 // instanciation couche [dao]
 import Dao from './couches/Dao';
 const dao = new Dao(config.axios);

 // plugin [dao]
 import pluginDao from './plugins/pluginDao'
 Vue.use(pluginDao, dao)

 // instanciation couche [métier]
 import Métier from './couches/Métier';
 const métier = new Métier();

 // plugin [métier]
 import pluginMétier from './plugins/pluginMétier'
 Vue.use(pluginMétier, métier)

 // store Vuex
 import store from './store'

 // démarrage de l'UI
 new Vue({
   el: '#app',
   // le routeur
   router: router,
   // le store Vuex
   store: store,
   // la vue principale
   render: h => h(Main),
 })

On notera les points suivants :

  • lignes 18-21, l’objet exporté par le script [./config] va être disponible dans l’attribut [Vue.$config] donc disponible à toutes les vues de l’application. C’était inutile ici car l’objet [config] n’est utilisé que par le script [main] (ligne 25). Néanmoins il est fréquent que la configuration soit nécessaire à plusieurs vues. On a donc voulu ici garder le principe de la rendre disponible dans un attribut de la vue ;
  • lignes 24-25 : instanciation de la couche [dao]. La classe [Dao] est importée ligne 24 puis instanciée ligne 25. Son constructeur admet pour unique paramètre l’objet [axios], propriété de configuration ;
  • lignes 27-29 : la couche [dao] est rendue disponible dans l’attribut [$dao] de toutes les vues ;
  • lignes 31-37 : on répète la même séquence pour la couche [métier]. Le constructeur de la classe [Métier] a pour paramètre [taxAdminData] qui représente les données de l’administration fiscale. Nous n’avons pas encore cette donnée. L’objet [métier] de la ligne 33 devra donc être complété ultérieurement ;
  • ligne 40 : on importe le store [Vuex] ;
  • lignes 43-51 : on instancie la vue principale [Main] (lignes 5 et 50), en lui passant deux paramètres :
    • ligne 46 : le routeur [router] défini ligne 16 ;
    • ligne 48 : le store [Vuex] [store] défini ligne 40 ;
    • dans les deux cas, le nom de la propriété est à gauche et sa valeur à droite. Les noms des propriétés [router, store] sont fixés par les frameworks [vue-router] et [vuex]. Les valeurs associées peuvent elles être quelconques ;

Les vues de l’application

La vue principale [Main]

Le code de la vue principale [Main] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 <!-- définition HTML de la vue -->
 <template>
   <div class="container">
     <b-card>
       <!-- jumbotron -->
       <b-jumbotron>
         <b-row>
           <b-col cols="4">
             <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
           </b-col>
           <b-col cols="8">
             <h1>Calculez votre impôt</h1>
           </b-col>
         </b-row>
       </b-jumbotron>
       <!-- erreur requête HTTP -->
       <b-alert
         show
         variant="danger"
         v-if="showError"
       >L'erreur suivante s'est produite : {{error.message}}</b-alert>
       <!-- vue courante -->
       <router-view v-if="showView" @loading="mShowLoading" @error="mShowError" />
       <!-- loading -->
       <b-alert show v-if="showLoading" variant="light">
         <strong>Requête au serveur de calcul d'impôt en cours...</strong>
         <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
       </b-alert>
     </b-card>
   </div>
 </template>

 <script>
 export default {
   // nom
   name: "app",
   // état interne
   data() {
     return {
       // contrôle l'alerte d'attente
       showLoading: false,
       // contrôle l'alerte d'erreur
       showError: false,
       // contrôle l'affichage de la vue de routage courante
       showView: true,
       // un message d'erreur
       error: ""
     };
   },
   // gestionnaires d'évts
   methods: {
     // erreur requête asynchrone
     mShowError(error) {
       // eslint-disable-next-line
       console.log("Main evt error");
       // on affiche le msg d'erreur
       this.error = error;
       this.showError = true;
       // on cache la vue routée
       this.showView = false;
       // on cache le message d'attente
       this.showLoading = false;
     },
     // affichage ou pas d'une icône d'attente
     mShowLoading(value) {
       // eslint-disable-next-line
       console.log("Main evt showLoading");
       // on affiche ou pas l'alerte d'attente
       this.showLoading = value;
     }
   }
 };
 </script>

Commentaires

  • la vue [Main] assure une mise en page de la vue routée et affichée ligne 23 :

image7

  • les lignes 5-15 affichent la zone 1 ;
  • le ligne 23 affiche la vue routée [2] ;
  • lignes 16-19 : une alerte affichée seulement en cas d’erreur de communication avec le serveur de calcul de l’impôt ;
  • lignes 25-28 : un message d’attente affiché à chaque requête HTTP faite au serveur ;
  • toutes les vues vont être affichées avec cette mise en page puisque chaque vue routée est affichée par les lignes 20-24. La vue [Main] sert à factoriser ce qui peut être partagé par les différentes vues ;
  • ligne 23 : chaque vue routée peut émettre trois événements :
    • [loading] : une requête HTTP a été lancée. Il faut montrer le message d’attente de la réponse ;
    • [error] : la requête HTTP s’est terminée sur une erreur. Il faut montrer le message d’erreur et cacher la vue routée ;
  • lignes 38-49 : l’état de la vue :
    • ligne 41 : [showLoading] contrôle l’affichage du message d’attente de la fin d’une requête HTTP (ligne 25) ;
    • ligne 43 : [showError] contrôle l’affichage du message d’erreur d’une requête HTTP (lignes 17-21) ;
    • ligne 45 : [showView] contrôle l’affichage de la vue routée (ligne 23) ;
  • lignes 53-63 : la méthode [mShowError] gère l’événement [error] émis par la vue routée (ligne 23) ;
  • lignes 65-70 : la méthode [mShowLoading] gère l’événement [loading] émis par la vue routée (ligne 23) ;
  • ligne 23 : on prêtera attention aux événements [error] et [loading]. Ils ne sont interceptés que si la vue routée est affichée [showView=true]. C’est pourquoi la vue routée est au départ affichée (ligne 45). Elle n’est cachée qu’en cas d’erreur (ligne 60). Pour éviter ce problème on aurait pu utiliser la directive [v-show] au lieu de [v-if]. la différence entre ces deux directives est la suivante :
    • [v-if=’false’] cache le bloc contrôlé en l’éliminant du code HTML global. Les événements de la vue routée ne peuvent plus alors être interceptés ;
    • [v-show=’false’] cache le bloc contrôlé en jouant sur son CSS, mais le code du bloc reste présent dans le HTML global et peut ainsi intercepter les événements de la vue routée ;

La vue de mise en page [Layout]

Le code de la vue [Layout] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 <!-- définition HTML de la mise en page de la vue routée -->
 <template>
   <!-- ligne -->
   <div>
     <b-row>
       <!-- zone de trois colonnes à gauche -->
       <b-col cols="3" v-if="left">
         <slot name="left" />
       </b-col>
       <!-- zone de neuf colonnes à droite -->
       <b-col cols="9" v-if="right">
         <slot name="right" />
       </b-col>
     </b-row>
   </div>
 </template>

 <script>
   export default {
     // paramètres de la vue
     props: {
       // contrôle la colonne de gauche
       left: {
         type: Boolean
       },
       // contrôle la colonne de droite
       right: {
         type: Boolean
       }
     }
   };
 </script>

Commentaires

  • la vue [Layout] permet de diviser la vue routée en deux zones :
    • une zone de 3 colonnes Bootstrap à gauche (lignes 7-9). Cette zone accueillera le menu de navigation lorsqu’il y en a un ;
    • une zone de 9 colonnes à droite (lignes 11-13). Cette zone accueillera l’information amenée par la vue routée ;

La vue [Authentification]

La vue d’authentification est la suivante :

image8

Cette vue est obtenue à partir du [Layout] en supprimant la colonne de gauche pour n’afficher que la colonne de droite.

Son code est le suivant :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
 <!-- définition HTML de la vue -->
 <template>
   <Layout :left="false" :right="true">
     <template slot="right">
       <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
       <b-form @submit.prevent="login">
         <!-- titre -->
         <b-alert show variant="primary">
           <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
         </b-alert>
         <!-- 1ère ligne -->
         <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
           <!-- zone de saisie user -->
           <b-col cols="6">
             <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
           </b-col>
         </b-form-group>
         <!-- 2ième ligne -->
         <b-form-group label="Mot de passe" label-for="password" label-cols="3">
           <!-- zone de saisie password -->
           <b-col cols="6">
             <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
           </b-col>
         </b-form-group>
         <!-- 3ième ligne -->
         <b-alert
           show
           variant="danger"
           v-if="showError"
           class="mt-3"
         >L'erreur suivante s'est produite : {{message}}</b-alert>
         <!-- bouton de type [submit] sur une 3ième ligne -->
         <b-row>
           <b-col cols="2">
             <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
           </b-col>
         </b-row>
       </b-form>
     </template>
   </Layout>
 </template>

 <!-- dynamique de la vue -->
 <script>
 import Layout from "./Layout";
 export default {
   // état du composant
   data() {
     return {
       // utilisateur
       user: "",
       // son mot de passe
       password: "",
       // contrôle l'affichage d'un msg d'erreur
       showError: false,
       // le message d'erreur
       message: "",
       // session démarrée
       sessionStarted: false
     };
   },

   // composants utilisés
   components: {
     Layout
   },

   // propriétés calculées
   computed: {
     // saisies valides
     valid() {
       return this.user && this.password && this.sessionStarted;
     }
   },

   // gestionnaires d'évts
   methods: {
     // ----------- authentification
     async login() {
       try {
         // début attente
         this.$emit("loading", true);
         // authentification bloquante auprès du serveur
         const response = await this.$dao.authentifierUtilisateur(
           this.user,
           this.password
         );
         // fin du chargement
         this.$emit("loading", false);
         // analyse de la réponse
         if (response.état != 200) {
           // on affiche l'erreur
           this.message = response.réponse;
           this.showError = true;
           return;
         }
         // pas d'erreur
         this.showError = false;
         // --------- on demande maintenant les données de l'administration fiscale
         // début attente
         this.$emit("loading", true);
         // demande bloquante auprès du serveur
         const response2 = await this.$dao.getAdminData();
         // fin du chargement
         this.$emit("loading", false);
         // analyse de la réponse
         if (response2.état != 1000) {
           // on affiche l'erreur
           this.message = response2.réponse;
           this.showError = true;
           return;
         }
         // pas d'erreur
         this.showError = false;
         // on mémorise la donnée reçue dans la couche [métier]
         this.$métier.setTaxAdminData(response2.réponse);
         // on passe à la vue du calcul de l'impôt
         this.$router.push({ name: "calculImpot" });
       } catch (error) {
         // on remonte l'erreur au composant principal
         this.$emit("error", error);
       }
     }
   },
   // cycle de vie : le composant vient d'être créé
   created() {
     // eslint-disable-next-line
     console.log("authentification", "created");
     // on démarre une session jSON avec le serveur
     // début attente
     this.$emit("loading", true);
     // on initialise la session avec le serveur - requête asynchrone
     // on utilise la promesse rendue par les méthodes de la couche [dao]
     this.$dao
       // on initialise une session jSON
       .initSession()
       // on a obtenu la réponse
       .then(response => {
         // fin attente
         this.$emit("loading", false);
         // analyse de la réponse
         if (response.état != 700) {
           // on affiche l'erreur
           this.message = response.réponse;
           this.showError = true;
           return;
         }
         // la session a démarré
         this.sessionStarted = true;
       })
       // en cas d'erreur
       .catch(error => {
         // on remonte l'erreur à la vue [Main]
         this.$emit("error", error);
       });
   }
 };
 </script>

Commentaires

  • ligne 3 : la vue [Authentification] utilise uniquement la colonne de droite du [Layout] (lignes 3 et 4) ;
  • lignes 6-38 : le formulaire Bootstrap qui génère la zone 1 de la copie d’écran ci-dessus ;
  • ligne 6 : l’événement [@submit] se produit lorsque l’utilisateur va cliquer sur le bouton de type [submit] de la ligne 35. Le modificateur [prevent] demande à ce que la page ne soit pas rechargée lors du [submit]. On aurait pu écrire également :
    • une balise <b-form> sans gestion de l’événement [submit] ;
    • une balise <b-button> avec l’événement [@click=’login’] et sans l’attribut [type=’submit’] ;
Ca marche également. L’avantage de la solution retenue est que le submit se fait non seulement avec un clic sur le bouton [Valider] mais également sur une validation (touche [Entrée]) dans les zones de saisie. C’est donc par commodité pour l’utilisateur que la solution [<b-form @submit.prevent= »login »>] a été retenue ici ;
  • lignes 33-37 : une alerte qui apparaît lorsque le serveur a rejeté les identifiants saisis par l’utilisateur :

image9

  • ligne 35 : le bouton [Valider] n’est pas toujours actif. Son état dépend de l’attribut calculé [valid] des lignes 71-73. L’attribut [valid] est vrai si :
    • il y a quelque chose dans les champs [user, password] du formulaire ;
    • la session jSON a démarré. Au départ, cette session n’a pas démarré (ligne 59) et donc le bouton [Valider] est inactif.
  • lignes 49-60 : l’état de la vue ;
    • [user] représente la saisie de l’utilisateur dans le champ [user] (lignes 12-17) du formulaire. La directive [v-model] de la ligne 15 établit une liaison bidirectionnelle entre la saisie de l’utilisateur et l’attribut [user] de la vue ;
    • [password] représente la saisie de l’utilisateur dans le champ [password] (lignes 19-24) du formulaire. La directive [v-model] de la ligne 22 établit une liaison bidirectionnelle entre la saisie de l’utilisateur et l’attribut [password] de la vue ;
    • [showError] contrôle (ligne 29) l’affichage de l’alerte des lignes 26-31 ;
    • [message] est le message d’erreur (ligne 31) à afficher dans l’alerte des lignes 26-31 ;
    • [sessionStarted] indique si la session jSON avec le serveur a démarré ou non. Au départ cet attribut a la valeur [false] (ligne 59). La session jSON avec le serveur est initialisée dans l’événement [created] du cycle de vie de la vue, lignes 126-156. Si le serveur répond positivement, alors l’attribut [sessionStarted] est passé à [true] (ligne 149) ;
  • lignes 126-156 : la fonction [created] est exécutée lorsque la vue [Authentification] a été créée (pas forcément encore affichée). En tâche de fond, on initialise alors une session jSON avec le serveur. On sait que c’est la 1ère action à faire avec le serveur de calcul de l’impôt. Pour ce faire, on utilise la couche [dao] de l’application (ligne 134). Toutes les méthodes de cette couche sont asynchrones. On utilise ici la promesse (Promise) rendue par la méthode [$dao.initSession] qui initialise la session jSON avec le serveur.
  • lignes 138-150 : le code exécuté lorsque le serveur a rendu sa réponse sans erreur ;
  • ligne 142 : on vérifie la propriété [état] de la réponse. Elle doit avoir la valeur [700] pour une opération réussie. Sinon, il s’est produit une erreur dont la cause est indiquée dans la propriété [response.réponse] (ligne 144). On affiche alors le message d’erreur de la vue (ligne 145) ;
  • ligne 149 : on note que la session jSON a démarré ;
  • lignes 152-155 : le code exécuté en cas d’erreur. Celle-ci est remontée à la vue parente [Main] qui
    • affichera l’erreur ;
    • cachera le message d’attente ;
    • cachera la vue routée, la vue [Autentification] ;
  • lignes 79-124 : la méthode [login] traite le clic sur le bouton [Valider] ;
  • ligne 79 : la méthode a été préfixée avec le mot clé [async] pour permettre l’utilisation du mot clé [await], lignes 84 et 103 ;
  • lignes 84-87 : appel bloquant à la méthode [$dao.authentifierUtilisateur(user, password)]. On aurait pu utiliser une promesse [Promise] comme il a été fait dans la fonction [created]. Nous avons voulu varier les styles. Il n’y a pas de risque à bloquer l’utilisateur car nous avons mis un [timeout] de 2 secondes à toutes les requêtes HTTP. Il n’attendra pas longtemps. De plus, il ne peut rien faire tant que le serveur n’a pas rendu sa réponse car alors le bouton [Valider] reste inactif ;
  • ligne 91 : le serveur de calcul de l’impôt envoie des réponses jSON ayant toutes la structure [{‘action’:action, ‘état’:val, ‘réponse’:réponse}]. L’authentification a réussi si [état==200]. Si ce n’est pas le cas, un message d’erreur est affiché, lignes 93-94 ;
  • ligne 98 : on cache un éventuel message d’erreur d’une opération précédente ;
  • lignes 99-116 : on demande mainteant au serveur les données de l’administration fiscale qui permettent le calcul de l’impôt. Dans [this.$métier] nous avons une instance de la classe [Métier] qui pour l’instant ne peut rien faire car elle n’a pas ces données ;
  • ligne 103 : les données de l’administration fiscale sont demandées au serveur par une opération bloquante ;
  • lignes 107-112 : la réponse du serveur est analysée. Elle doit avoir une valeur d’état égale à 1000 sinon c’est qu’il s’est produit une erreur. Dans ce dernier cas, on affiche le message d’erreur (lignes 109-110) ;
  • lignes 113-118 : en cas de réussite de l’opération, on :
    • cache le message d’erreur, ligne 114 ;
    • on transmet les données de l’administration fiscale à la couche [métier] (ligne 116) ;
    • on fait afficher la vue [CalculImpot], ligne 118. On se rappelle que [this.$router] désigne le routeur de l’application. La méthode [push] permet de fixer la prochaine vue routée. Ici on la désigne par son attribut [name]. On aurait pu également la désigner par son attribut [path]. Ces informations sont dans le fichier de routage :
1
2
3
4
 // calcul de l'impôt
   {
     path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
   },
  • lignes 119-122 : le [catch] se déclenche lorsqu’une des deux requêtes HTTP a échoué (serveur pas présent, timeout dépassé, …). On signale alors l’erreur à la vue parente [Main] qui l’affichera, cachera le message d’attente et la vue [Authentification] ;

La vue [CalculImpot]

La vue [CalculImpot] est la suivante :

image10

  • [1] : un menu de navigation occupe la colonne de gauche de la vue routée ;
  • [2] : le formulaire de calcul de l’impôt occupe la colonne de droite de la vue routée ;

Le code de la vue [CalculImpot] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
 <!-- définition HTML de la vue -->
 <template>
   <div>
     <Layout :left="true" :right="true">
       <!-- formulaire de calcul de l'impôt à droite -->
       <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
       <!-- menu de navigation à gauche -->
       <Menu slot="left" :options="options" />
     </Layout>
     <!-- zone d'affichage des résultat du calcul de l'impôt sous le formulaire -->
     <b-row v-if="résultatObtenu" class="mt-3">
       <!-- zone de trois colonnes vide -->
       <b-col cols="3" />
       <!-- zone de neuf colonnes -->
       <b-col cols="9">
         <b-alert show variant="success">
           <span v-html="résultat"></span>
         </b-alert>
       </b-col>
     </b-row>
   </div>
 </template>

 <script>
 // imports
 import FormCalculImpot from "./FormCalculImpot";
 import Menu from "./Menu";
 import Layout from "./Layout";

 export default {
   // état interne
   data() {
     return {
       // options du menu
       options: [
         {
           text: "Liste des simulations",
           path: "/liste-des-simulations"
         },
         {
           text: "Fin de session",
           path: "/fin-session"
         }
       ],
       // résultat du calcul de l'impôt
       résultat: "",
       résultatObtenu: false
     };
   },
   // composants utilisés
   components: {
     Layout,
     FormCalculImpot,
     Menu
   },
   // méthodes de gestion des évts
   methods: {
     // résultat du calcul de l'impôt
     handleResultatObtenu(résultat) {
       // on construit le résultat en chaîne HTML
       const impôt = "Montant de l'impôt : " + résultat.impôt + " euro(s)";
       const décôte = "Décôte : " + résultat.décôte + " euro(s)";
       const réduction = "Réduction : " + résultat.réduction + " euro(s)";
       const surcôte = "Surcôte : " + résultat.surcôte + " euro(s)";
       const taux = "Taux d'imposition : " + résultat.taux;
       this.résultat =
         impôt +
         "<br/>" +
         décôte +
         "<br/>" +
         réduction +
         "<br/>" +
         surcôte +
         "<br/>" +
         taux;
       // affichage du résultat
       this.résultatObtenu = true;
       // ---- maj du store [Vuex]
       // une simulation de +
       this.$store.commit("addSimulation", résultat);
     }
   }
 };
 </script>

Commentaires

  • ligne 4 : les deux colonnes du [Layout] sont ici présentes ;
  • ligne 6 : le formulaire de calcul de l’impôt occupe la colonne de droite. Il émet l’événement [resultatObtenu] lorsque le résultat du calcul de l’impôt a été obtenu. On notera que les noms d’événements et les noms des méthodes qui les gèrent ne peuvent contenir de caractères accentués ;
  • ligne 8 : le menu de navigation occupe la colonne de gauche ;
  • lignes 11-20 : le résultat du calcul de l’impôt est affiché sous le formulaire :

image11

ligne 11 : le résultat n’est affiché que si l’attribut [résultatObtenu] (ligne 47) vaut [true] ;

  • lignes 34-48 : l’état de la vue :
    • [options] : la liste des options du menu de navigation. Ce tableau est passé en paramètre au composant [Menu], ligne 8 ;
    • [résultat] : le résultat du calcul de l’impôt. Ce résultat est une chaîne HTML. C’est pourquoi on a utilisé la directive [v-html] à la ligne 17 pour l’afficher ;
    • [résultatObtenu] : le booléen qui contrôle l’affichage du résultat, ligne 11 ;
  • lignes 59-81 : la méthode [handleResultatObtenu] affiche le résultat du calcul de l’impôt que lui a envoyé la vue fille [FormCalculImpot], ligne 6. Ce résultat est un objet avec les propriétés [impot, décôte, réduction, surcôte, taux, marié, enfants, salaire] ;
  • lignes 61-75 : on inscrit l’objet [impot, décôte, réduction, surcôte, taux] dans un texte HTML qui est visualisé par la ligne 17 du template ;
  • ligne 77 : on affiche ce résultat ;
  • ligne 80 : on appelle la mutation [addSimulation] du store Vuex qui va ajouter [résultat] aux simulations déjà présentes dans le store ;

Le menu de navigation [Menu]

Le menu de navigation s’affiche dans la colonne de gauche des vues routées :

image12

Le code de la vue [Menu] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 <!-- définition HTML de la vue -->
 <template>
   <!-- menu Bootstrap vertical -->
   <b-nav vertical>
     <!-- options du menu -->
     <b-nav-item
       v-for="(option,index) of options"
       :key="index"
       :to="option.path"
       exact
       exact-active-class="active"
     >{{option.text}}</b-nav-item>
   </b-nav>
 </template>

 <script>
 export default {
   // paramètres de la vue
   props: {
     options: {
       type: Array
     }
   }
 };
 </script>

Commentaires

  • les options du menu sont fournies par le paramètre [options] (lignes 7, 20-22) ;
  • chaque élément du tableau [options] a une propriété [text] (ligne 12) qui est le texte du lien et une propriété [path] (ligne 9) qui sera le chemin de la vue cible du lien ;

La vue [FormCalculImpot]

Cette vue fournit le formulaire de calcul de l’impôt :

image13

Son code est le suivant :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
   <!-- définition HTML de la vue -->
   <template>
   <!-- formulaire HTML -->
   <b-form @submit.prevent="calculerImpot" class="mb-3">
     <!-- message sur 12 colonnes sur fond bleu -->
     <b-alert show variant="primary">
       <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
     </b-alert>
     <!-- éléments du formulaire -->
     <!-- première ligne -->
     <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?" label-cols="4">
       <!-- boutons radio sur 5 colonnes-->
       <b-col cols="5">
         <b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
         <b-form-radio v-model="marié" value="non">Non</b-form-radio>
       </b-col>
     </b-form-group>
     <!-- deuxième ligne -->
     <b-form-group label="Nombre d'enfants à charge" label-cols="4" label-for="enfants">
       <b-input
         type="text"
         id="enfants"
         placeholder="Indiquez votre nombre d'enfants"
         v-model="enfants"
         :state="enfantsValide"
       />
       <!-- message d'erreur éventuel -->
       <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
     </b-form-group>
     <!-- troisème ligne -->
     <b-form-group
       label="Salaire annuel"
       label-cols="4"
       label-for="salaire"
       description="Arrondissez à l'euro inférieur"
     >
       <b-input
         type="text"
         id="salaire"
         placeholder="Salaire annuel"
         v-model="salaire"
         :state="salaireValide"
       />
       <!-- message d'erreur éventuel -->
       <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
     </b-form-group>
     <!-- quatrième ligne, bouton [submit] sur 5 colonnes -->
     <b-col cols="5">
       <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
     </b-col>
   </b-form>
 </template>

 <!-- script -->
 <script>
 export default {
   // état interne
   data() {
     return {
       // marié ou pas
       marié: "non",
       // nombre d'enfants
       enfants: "",
       // salaire annuel
       salaire: ""
     };
   },
   // état interne calculé
   computed: {
     // validation du formulaire
     formInvalide() {
       return (
         // salaire invalide
         !this.salaireValide ||
         // ou enfants invalide
         !this.enfantsValide ||
         // ou données fiscales pas obtenues
         !this.$métier.taxAdminData
       );
     },
     // validation du salaire
     salaireValide() {
       // doit être numérique >=0
       return Boolean(this.salaire.match(/^\s*\d+\s*$/));
     },
     // validation des enfants
     enfantsValide() {
       // doit être numérique >=0
       return Boolean(this.enfants.match(/^\s*\d+\s*$/));
     }
   },
   // gestionnaire d'évts
   methods: {
     calculerImpot() {
       // on calcule l'impôt à l'aide de la couche [métier]
       const résultat = this.$métier.calculerImpot(
         this.marié,
         this.enfants,
         this.salaire
       );
       // eslint-disable-next-line
       console.log("résultat=", résultat);
       // on complète le résultat
       résultat.marié = this.marié;
       résultat.enfants = this.enfants;
       résultat.salaire = this.salaire;
       // on émet l'évt [resultatObtenu]
       this.$emit("resultatObtenu", résultat);
     }
   }
 };
 </script>

Commentaires

  • lignes 4-51 : le formulaire Bootstrap ;
  • lignes 11-17 : un groupe de boutons radio avec leur libellé ;
  • lignes 14-15 : la balise <b-form-radio> assure l’affichage d’un bouton radio :
    • ligne 14 : la directive [v-model] assure que lors d’un clic sur le bouton, l’attribut [marié] de la ligne 61 recevra la valeur [oui] (attribut [value= »oui »]) ;
    • ligne 15 : la directive [v-model] assure que lors d’un clic sur le bouton, l’attribut [marié] de la ligne 61 recevra la valeur [non] (attribut [value= »non »]) ;
  • lignes 19-29 : la partie saisie du nombre d’enfants :
    • ligne 24 : la saisie du nombre d’enfants est liée à l’attribut [enfants] de la ligne 63 ;
    • ligne 25 : la validité de la saisie est vérifiée par l’attribut calculé [enfantsValide] des lignes 87-89 ;
    • ligne 28 : assure l’affichage d’un message d’erreur si la saisie est invalide ;
  • lignes 31-45 : la partie saisie du salaire annuel :
    • ligne 35 : affiche un message d’aide juste sous la zone de saisie ;
    • ligne 41 : la saisie du salaire est liée à l’attribut [salaire] de la ligne 65 ;
    • ligne 42 : la validité de la saisie est vérifiée par l’attribut calculé [salaireValide] des lignes 82-85 ;
    • ligne 45 : assure l’affichage d’un message d’erreur si la saisie est invalide ;
  • lignes 48-50 : un bouton de type [submit]. Lorsqu’on clique sur ce bouton ou lorsqu’on valide une saisie avec la touche [Entrée], la méthode [calculerImpot] est exécutée (ligne 94) ;
    • ligne 49 : l’état du bouton actif / inactif est contrôlé par l’attribut calculé [formInvalide] des lignes 71-80 ;
  • lignes 71-80 : le formulaire est valide si :
    • le nombre d’enfants est valide ;
    • le salaire est valide ;
    • l’application a obtenu du serveur les données de l’administration fiscale permettant le calcul de l’impôt. On rappelle que cette donnée est enregistrée dans la propriété [$métier.taxAdminData]. La vue [FormCalculImpot] peut être affichée avant que cette donnée ait été obtenue car elle est demandée de façon asynchrone en même temps que se produit l’affichage de la vue. On s’assure ici que l’utilisateur ne peut pas cliquer sur le bouton [Valider] tant que la donnée n’a pas été obtenue ;
  • lignes 94-109 : la méthode de calcul de l’impôt :
    • lignes 96-100 : c’est la couche [métier] qui fait ce calcul. C’est un calcul synchrone. Une fois la donnée [taxAdminData] a été obtenue, le client [Vue] n’a plus à communiquer avec le serveur. Tout se fait localement. On obtient un objet [résultat] avec les propriétés [impôt, décôte, surcôte, réduction, taux] ;
    • lignes 104-106 : on ajoute les propriétés [marié, enfants, salaire] au résultat ;
    • ligne 108 : le résultat est passé à la vue parent [CalculImpot] via l’événement [resultatObtenu]. Cette vue est chargée d’afficher le résultat ;

La vue [ListeSimulations]

La vue [ListeSimulations] affiche la liste des simulations faites par l’utilisateur :

image14

Le code de la vue est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
 <!-- définition HTML de la vue -->
 <template>
   <div>
     <!-- mise en page -->
     <Layout :left="true" :right="true">
       <!-- simulations dans colonne de droite -->
       <template slot="right">
         <template v-if="simulations.length==0">
           <!-- pas de simulations -->
           <b-alert show variant="primary">
             <h4>Votre liste de simulations est vide</h4>
           </b-alert>
         </template>
         <template v-if="simulations.length!=0">
           <!-- il y a des simulations -->
           <b-alert show variant="primary">
             <h4>Liste de vos simulations</h4>
           </b-alert>
           <!-- tableau des simulations -->
           <b-table striped hover responsive :items="simulations" :fields="fields">
             <template v-slot:cell(action)="data">
               <b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
             </template>
           </b-table>
         </template>
       </template>
       <!-- menu de navigation dans colonne de gauche -->
       <Menu slot="left" :options="options" />
     </Layout>
   </div>
 </template>

 <script>
   // imports
   import Layout from "./Layout";
   import Menu from "./Menu";
   export default {
     // composants
     components: {
       Layout,
       Menu
     },
     // état interne
     data() {
       return {
         // options du menu de navigation
         options: [
           {
             text: "Calcul de l'impôt",
             path: "/calcul-impot"
           },
           {
             text: "Fin de session",
             path: "/fin-session"
           }
         ],
         // paramètres de la table HTML
         fields: [
           { label: "#", key: "id" },
           { label: "Marié", key: "marié" },
           { label: "Nombre d'enfants", key: "enfants" },
           { label: "Salaire", key: "salaire" },
           { label: "Impôt", key: "impôt" },
           { label: "Décôte", key: "décôte" },
           { label: "Réduction", key: "réduction" },
           { label: "Surcôte", key: "surcôte" },
           { label: "", key: "action" }
         ]
       };
     },
     // état interne calculé
     computed: {
       // liste des simulations prise dans le store Vuex
       simulations() {
         return this.$store.state.simulations;
       }
     },
     // méthodes
     methods: {
       supprimerSimulation(index) {
         // eslint-disable-next-line
         console.log("supprimerSimulation", index);
         // suppression de la simulation n° [index]
         this.$store.commit("deleteSimulation", index);
       }
     }
   };
 </script>

Commentaires

  • ligne 5 : la vue occupe les deux colonnes de la mise en page [Layout] des vues routées ;
  • lignes 7-26 : les simulations vont dans la colonne de droite ;
  • ligne 28 : le menu de navigation va dans la colonne de gauche ;
  • lignes 8, 14, 20, 75 : les simulations proviennent du store [Vuex] [$this.store] ;
  • lignes 8-13 : alerte affichée lorsque la liste des simulations est vide ;
  • lignes 14-25 : la table HTML affichée lorsque la liste des simulations n’est pas vide ;
  • lignes 20-24 : la table HTML est générée par une balise <b-table> ;
    • ligne 20 : le tableau des simulations est fourni par l’attribut calculé [simulations] des lignes 74-76 ;
    • ligne 20 : la configuration de la table HTML est faite par l’attribut calculé [fields] des lignes 58-69. Ligne 67, la colonne de clé [action] est la dernière colonne de la table HTML ;
    • lignes 21-23 : template de la dernière colonne de la table HTML ;
    • ligne 22 : on y met un bouton de type lien. Lorsqu’on clique dessus, la méthode [supprimerSimulation(data.index)] est appelée, où [data] représente la ligne courante (ligne 21). [data.index] représente le n° de cette ligne dans la liste des lignes affichées ;
  • ligne 28 : génération du menu de navigation. Les options de celui-ci sont fournies par l’attribut [options] des lignes 47-56 ;
  • lignes 80-85 : la méthode qui réagit au clic sur un lien [Supprimer] de la page HTML ;
    • ligne 84 : on fait appel à la mutation [deleteSimulation] du store [Vuex] (cf paragraphe |vuejs-15|) ;

Exécution du projet

image15

Il faut également lancer le serveur [Laragon] (cf document |https://tahe.developpez.com/tutoriels-cours/php7|) pour que le serveur de calcul d’impôt soit en ligne.

Déploiement de l’application sur un serveur local

Actuellement, notre client [Vue] est déployé sur un serveur de test à l’URL [http://localhost:8080]. Nous allons le déployer sur le serveur [Laragon] à l’URL [http://localhost:80]. Il y a plusieurs étapes à effectuer pour en arriver là.

étape 1

Tout d’abord, nous allons faire en sorte que le client [Vue] soit déployé sur le serveur de test à l’URL [http://localhost:8080/client-vuejs-impot/].

Nous créons un fichier [vue.config.js] à la racine de notre projet [VSCode] actuel :

image16

Le fichier [vue.config.js] [1] aura le contenu suivant :

1
2
3
4
5
 // vue.config.js
 module.exports = {
   // l'URL de service du client [vuejs] du serveur de calcul de l'impôt
   publicPath: '/client-vuejs-impot/'
 }

Il nous faut également modifier le fichier de routage [router.js] [2] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 // imports
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 // les vues
 import Authentification from './views/Authentification'
 import CalculImpot from './views/CalculImpot'
 import ListeSimulations from './views/ListeSimulations'

 // plugin de routage
 Vue.use(VueRouter)

 // les routes de l'application
 const routes = [
   // authentification
   {
     path: '/', name: 'authentification', component: Authentification
   },
   // calcul de l'impôt
   {
     path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
   },
   // liste des simulations
   {
     path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
   },
   // fin de session
   {
     path: '/fin-session', name: 'finSession', component: Authentification
   }
 ]

 // le routeur
 const router = new VueRouter({
   // les routes
   routes,
   // le mode d'affichage des routes dans le navigateur
   mode: 'history',
   // l'URL de base de l'application
   base: '/client-vuejs-impot/'
 })

 // export du router
 export default router
  • ligne 39 : on indique au routeur que les chemins des routes définies lignes 13-30 sont relatives au chemin défini ligne 39. Par exemple, le chemin de la ligne 20 [/calcul-impot] deviendra [/client-vuejs-impot/calcul-impot] ;

On peut alors tester de nouveau le projet [vuejs-20] pour vérifier le changement des chemins de l’application :

image17

étape 2

Nous construisons maintenant la version de production du projet [vuejs-20] :

image18

  • en [1-2], nous configurons la tâche [build] [2] dans le fichier [package.json] [1] ;
  • en [3-5], nous exécutons cette tâche. C’est elle qui va construire la version de production du projet [vuejs-20] ;

L’exécution de la tâche [build] se passe dans un terminal de [VSCode] :

image19

image20

  • en [3-6], des avertissements nous disent que le code généré est trop gros et qu’il faudrait le découper [8]. Cela relève de l’optimisation de l’architecture du code que nous n’aborderons pas ici ;
  • en [7], on nous dit que le dossier [dist] contient la version de production générée :

image21

  • en [3], le fichier [index.html] est le fichier qui sera utilisé lorsqu’on demandera l’URL [https://localhost:80/client-vue-js-impot/];

On a ici un site statique qui peut être déployé sur n’importe quel serveur. Nous allons le déployer sur le serveur Laragon local (cf document |https://tahe.developpez.com/tutoriels-cours/php7|). Le dossier [dist] [2] est copié dans le dossier [<laragon>/www] [4] où <laragon> est le dossier d’installation du serveur Laragon. Nous renommons ce dossier [client-vuejs-impot] [5] puisque nous avons configuré la version de production pour fonctionner à l’URL [/client-vuejs-impot/].

étape 3

Nous ajoutons dans le dossier [client-vuejs-impot] qui vient d’être créé le fichier [.htaccess] suivant :

1
2
3
4
5
6
7
8
 <IfModule mod_rewrite.c>
   RewriteEngine On
   RewriteBase /client-vuejs-impot/
   RewriteRule ^index\.html$ - [L]
   RewriteCond %{REQUEST_FILENAME} !-f
   RewriteCond %{REQUEST_FILENAME} !-d
   RewriteRule . /client-vuejs-impot/index.html [L]
 </IfModule>

image22

Ce fichier est un fichier de configuration du serveur web Apache. Si nous ne le mettons pas et que nous demandons directement l’URL [https://localhost/client-vuejs-impot/calcul-impot], sans passer d’abord par l’URL [https://localhost/client-vuejs-impot/] nous obtenons une erreur 404. Avec ce fichier, nous obtenons bien la vue [CalculImpot].

Ceci fait, nous lançons le serveur Laragon si ce n’est déjà fait et demandons l’URL [https://localhost/client-vuejs-impot/] :

image23

Le lecteur est invité à tester la version de production de notre application.

Nous pouvons modifier le serveur de calcul de l’impôt sur un point : les entêtes CORS qu’il envoie systématiquement à ses clients. Cela avait été nécessité pour la version du client exécutée à partir du domaine [localhost:8080]. Maintenant que client et serveur s’exécutent tous deux dans le domaine [localhost:80], les entêtes CORS deviennent inutiles.

Nous modifions le fichier [config.json] de la version 14 du serveur :

image24

  • en [4], nous indiquons que désormais les requêtes CORS sont refusées ;

Sauvegardons cette modification et redemandons l’URL [https://localhost/client-vuejs-impot/]. Ca doit continuer à marcher.

Gestion des URL manuelles

Au lieu d’utiliser sagement les liens du menu de navigation, l’utilisateur peut vouloir taper les URL de l’application manuellement dans le champ d’adresse du navigateur. Demandons par exemple l’URL [https://client-vuejs-impot/calcul-impot] sans passer par la case d’authentification. Un hacker tenterait sûrement ça. On obtient la vue suivante ;

image25

On obtient bien la vue du calcul de l’impôt. Maintenant essayons de remplir les zones de saisie et de les valider :

image26

On découvre alors que le bouton [1] [Valider] reste toujours désactivé même si les saisies sont correctes. Regardons le code de la vue [FormCalculImpot] :

1
2
3
 <b-col cols="5">
       <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
 </b-col>

Ligne 2, on voit que son état actif / inactif dépend de la propriété [formInvalide]. Celle-ci est la propriété calculée suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 formInvalide() {
       return (
         // salaire invalide
         !this.salaireValide ||
         // ou enfants invalide
         !this.enfantsValide ||
         // ou données fiscales pas obtenues
         !this.$métier.taxAdminData
       );
 },

Ligne 8, on voit que pour que le formulaire soit valide, il faut avoir obtenu les données fiscales. Or celles-ci sont obtenues lors de la validation de la vue [Authentification] que l’utilisateur a ‘sautée’. Il ne pourra donc pas valider le formulaire. S’il avait pu le faire, il aurait reçu un message d’erreur du serveur lui indiquant qu’il n’était pas authentifié. Les vérifications doivent toujours être faites côté serveur. Les vérifications côté navigateur peuvent toujours être contournées. Il suffit de prendre un client de type [Postman] qui enverra des requêtes brutes au serveur.

Maintenant demandons l’URL [https://localhost/client-vuejs-impot/liste-des-simulations]. On obtient la vue suivante :

image27

Maintenant l’URL [https://localhost/client-vuejs-impot/fin-session]. Nous obtenons la vue suivante :

image28

Maintenant une vue qui n’existe pas [https://localhost/client-vuejs-impot/abcd] :

image29

Notre application résiste plutôt bien aux URL tapées à la main. Lorsque celles-ci sont appelées, le routeur de l’application le sait. Il est donc possible d’intervenir avant que la vue ne soit finalement affichée. Nous allons regarder ce point dans le projet [vuejs-21].

Un autre point à regarder est le suivant. Imaginons que l’utilisateur ait fait quelques simulations dans les règles :

image30

Maintenant rafraîchissons la page par un F5 :

image31

On a fait quelque chose de déconseillé : taper l’URL à la main (faire F5 revient à ça). Nous avons alors perdu nos simulations.

Le projet suivant [vuejs-21] se propose d’apporter deux améliorations :

  • contrôler les URL tapées par l’utilisateur ;
  • garder une mémoire de l’application même si l’utilisateur tape une URL. Ci-dessus, on voit qu’on a perdu la liste des simulations ;

Améliorations du client Vue.js

Introduction

Nous allons tester le projet [vuejs-21] avec le serveur de développement. Nous allons donc avoir besoin de nouveau que le serveur envoie les entêtes CORS. Il faut donc que le fichier [config.json] de la version 14 du serveur de calcul de l’impôt autorise ces entêtes :

image0

Le projet [vuejs-21] est créé initialement par duplication du projet [vuejs-20]. Il est ensuite modifié [3].

De nouveaux fichiers apparaissent :

  • [session.js] : exporte un objet [session] qui va encapsuler des informations sur la session courante ;
  • [pluginSession] : rend disponible l’objet [session] précédent dans la propriété [$session] des vues ;
  • [NotFound.vue] : une nouvelle vue affichée lorsque l’utilisateur demande manuellement une URL qui n’existe pas ;

Des fichiers seront modifiés :

  • [main.js] : va initialiser la session courante puis, lorsque l’utilisateur va taper manuellement des URL va la restaurer ;
  • [router.js] : des contrôles sont ajoutés pour traiter le cas des URL tapées par l’utilisateur ;
  • [store.js] : une nouvelle mutation est ajoutée ;
  • [config.js] : une nouvelle configuration est ajoutée ;
  • différentes vues essentiellement pour sauver la session courante à des moments clés de la vie de l’application. Celle-ci est ensuite restaurée à chaque fois que l’utilisateur tape manuellement des URL ;

Le store [Vuex]

Le script [./store] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 // plugin Vuex
 import Vue from 'vue'
 import Vuex from 'vuex'
 Vue.use(Vuex);

 // store Vuex
 const store = new Vuex.Store({
   state: {
     // le tableau des simulations
     simulations: [],
     // le n° de la dernière simulation
     idSimulation: 0
   },
   mutations: {
     // suppression ligne n° index
     deleteSimulation(state, index) {
       ...
     },
     // ajout d'une simulation
     addSimulation(state, simulation) {
       ...
     },
     // nettoyage state
     clear(state) {
       // plus de simulations
       state.simulations = [];
       // la numérotation des simulations repart de 0
       state.idSimulation = 0;
     }
   }
 });
 // export de l'objet [store]
 export default store;
  • lignes 24-29 : la mutation [clear] supprime la liste des simulations enregistrées et remet à 0 le n° de la dernière simulation.

La session

Le besoin d’une session vient du fait que lorsque l’utilisateur tape une URL dans le champ adresse du navigateur, le script [main.js] est exécuté de nouveau. Or celui-ci contient l’instruction :

1
2
 // store Vuex
 import store from './store'

Cette instruction importe le fichier [./store] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 // plugin Vuex
 import Vue from 'vue'
 import Vuex from 'vuex'
 Vue.use(Vuex);

 // store Vuex
 const store = new Vuex.Store({
   state: {
     // le tableau des simulations
     simulations: [],
     // le n° de la dernière simulation
     idSimulation: 0
   },
   mutations: {
     ...
   }
 });
 // export de l'objet [store]
 export default store;

On voit, lignes 7-13, qu’on importe un tableau de simulations vide. Si donc on avait des simulations avant que l’utilisateur ne tape une URL dans le champ adresse du navigateur, après on n’en a plus. L’idée est :

  • d’utiliser une session qui stockerait les informations qu’on veut conserver si l’utilisateur tape manuellement des URL ;
  • de la sauvegarder à des moments clés de l’application ;
  • de la restaurer dans [main.js] qui est toujours exécuté lorsqu’une URL est tapée manuellement ;

Le script [./session] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
 // on importe le store Vuex
 import store from './store'
 // on importe la configuration
 import config from './config';

 // l'objet [session]
 const session = {
   // session démarrée
   started: false,
   // authentification
   authenticated: false,
   // heure de sauvegarde
   saveTime: "",
   // couche [métier]
   métier: null,
   // état Vuex
   state: null,

   // sauvegarde de la session dans une chaîne jSON
   save() {
     // on ajoute à la session quelques proprités
     this.saveTime = Date.now();
     this.state = store.state;
     // on la transforme en jSON
     const json = JSON.stringify(this);
     // on la stocke sur le navigateur
     localStorage.setItem("session", json);
     // eslint-disable-next-line no-console
     console.log("session save", json);
   },

   // restauration de la session
   restore() {
     // on récupère la session jSON à partir du navigateur
     const json = localStorage.getItem("session")
     // si on a récupéré qq chose
     if (json) {
       // on restaure toutes les clés de la session
       const restore = JSON.parse(json);
       for (var key in restore) {
         if (restore.hasOwnProperty(key)) {
           this[key] = restore[key];
         }
       }
       // si on a dépassé une certaine durée d'inactivité depuis le début de la session, on repart de zéro
       let durée = Date.now() - this.saveTime;
       if (durée > config.duréeSession) {
         // on vide la session - elle sera également sauvegardée
         session.clear();
       } else {
         // on régénère le store Vuex
         store.replaceState(JSON.parse(JSON.stringify(this.state)));
       }
     }
     // eslint-disable-next-line no-console
     console.log("session restore", this);
   },

     // on nettoie la session
   clear() {
     // eslint-disable-next-line no-console
     console.log("session clear");
     // raz de certains champs de la session
     this.authenticated = false;
     this.saveTime = "";
     this.started = false;
     if (this.métier) {
       // on réinitialise le champ [taxAdminData]
       this.métier.taxAdminData = null;
     }
     // le store Vuex est nettoyé également
     store.commit("clear");
     // on sauvegarde la nouvelle session
     this.save();
   },
 }

 // export de l'objet [session]
 export default session;

Commentaires

  • ligne 2 : la session va encapsuler également le store [Vuex] (liste des simulations, n° de la dernière simulation faite) ;
  • lignes 7-17 : les informations conservées par la session :
    • [started] : la session jSON avec le serveur a démarré ou non ;
    • [authenticated] : l’utilisateur s’est authentifié ou pas ;
    • [saveTime] : la date en millisecondes de la dernière sauvegarde ;
    • [métier] : une référence sur la couche [métier]. Celle-ci contient la donnée [taxAdminData] qui permet le calcul de l’impôt ;
    • [state] : le state du store [Vuex] (liste des simulations, n° de la dernière simulation faite) ;
  • lignes 20-30 : la méthode [save] sauvegarde la session localement sur le navigateur exécutant l’application ;
    • ligne 22 : on note l’heure de sauvegarde ;
    • ligne 23 : on récupère le [state] du store [Vuex] ;
    • ligne 25 : on crée la chaîne jSON de la session ;
    • ligne 27 : on la stocke localement sur le navigateur associée à la clé [session] ;
  • lignes 33-57 : la méthode [restore] permet de restaurer une session à partir de sa sauvegarde locale sur le navigateur ;
    • ligne 35 : on récupère la sauvegarde jSON locale ;
    • ligne 37 : si on a récupéré quelque chose ;
    • lignes 39-44 : l’objet [session] est reconstitué ;
    • ligne 46 : on calcule la durée qui nous sépare de la dernière sauvegarde ;
    • lignes 47-50 : si cette durée est supérieure à une valeur [config.duréeSession] fixée par configuration, la session est réinitialisée (ligne 49) et à cette occasion sauvegardée ;
    • ligne 52 : sinon on régénère l’attribut [state] du store [Vuex] ;
  • lignes 60-75 : la méthode [clear] réinitialise la session ;
    • lignes 64-70 : les propriétés de la session sont réinitialisées à leurs valeurs initiales ;
    • ligne 72 : ainsi que le store [Vuex] ;
    • ligne 74 : la nouvelle session est sauvegardée ;

Le fichier de configuration [config]

Le fichier [./config] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 // utilisation de la bibliothèque [axios]
 const axios = require('axios');
 // timeout des requêtes HTTP
 axios.defaults.timeout = 2000;
 ...

 // export de la configuration
 export default {
   // objet [axios]
   axios: axios,
   // délai maximal d'inactivité de la session : 5 mn = 300 s = 300000 ms
   duréeSession: 300000
 }
  • ligne 12 : on va gérer la session de l’application un peu comme on gère une session web. On fixe ici une durée d’inactivité maximale de 5 minutes ;

Le plugin [pluginSession]

Comme il a été fait déjà de nombreuses fois, le plugin [pluginSession] va permettre aux vues d’avoir accès à la session via la propriété [this.$session] :

1
2
3
4
5
6
7
8
9
 export default {
   install(Vue, session) {
     // ajoute une propriété [$session] à la classe vue
     Object.defineProperty(Vue.prototype, '$session', {
       // lorsque Vue.$session est référencé, on rend le 2ième paramètre [session]
       get: () => session,
     })
   }
 }

Le script principal [main]

Le script principal [./main.js] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 // log de démarrage
 // eslint-disable-next-line no-console
 console.log("main started");

 // imports
 import Vue from 'vue'

 ...

 // instanciation couche [métier]
 import Métier from './couches/Métier';
 const métier = new Métier();

 // plugin [métier]
 import pluginMétier from './plugins/pluginMétier'
 Vue.use(pluginMétier, métier)

 // store Vuex
 import store from './store'

 // session
 import session from './session';
 import pluginSession from './plugins/pluginSession'
 Vue.use(pluginSession, session)

 // on restore la session avant de redémarrer
 session.restore();

 // on restaure la couche [métier]
 if (session.métier && session.métier.taxAdminData) {
   métier.setTaxAdminData(session.métier.taxAdminData);
 }

 // démarrage de l'UI
 new Vue({
   el: '#app',
   // le routeur
   router: router,
   // le store Vuex
   store: store,
   // la vue principale
   render: h => h(Main),
 })

 // log de fin
 // eslint-disable-next-line no-console
 console.log("main terminated, session=", session);
  • ligne 19 : on importe la session ;
  • ligne 20 : on importe son plugin ;
  • ligne 21 : le plugin [pluginSession] est intégré à [Vue]. Après cette instruction toutes les vues disposent de la session dans leur attribut [$session] ;
  • ligne 27 : la session est restaurée. La session importée ligne 11 est alors initialisée avec le contenu de sa dernière sauvegarde ;
  • après la ligne 16, les vues disposent d’une propriété [$métier] initialisée ligne 12. Cette propriété n’a pas l’information [taxAdminData] qui permet de calculer l’impôt ;
  • lignes 30-32 : si la restauration qui vient d’être faite a restauré la propriété [session.métier.taxAdminData] alors la propriété [$métier] des vues est initialisée avec cette valeur ;

Le fichier de routage [router]

Le fichier de routage [./router] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
 // imports
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 // les vues
 import Authentification from './views/Authentification'
 import CalculImpot from './views/CalculImpot'
 import ListeSimulations from './views/ListeSimulations'
 import NotFound from './views/NotFound'
 // la session
 import session from './session'

 // plugin de routage
 Vue.use(VueRouter)

 // les routes de l'application
 const routes = [
   // authentification
   { path: '/', name: 'authentification', component: Authentification },
   { path: '/authentification', name: 'authentification', component: Authentification },
   // calcul de l'impôt
   {
     path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
     meta: { authenticated: true }
   },
   // liste des simulations
   {
     path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
     meta: { authenticated: true }
   },
   // fin de session
   {
     path: '/fin-session', name: 'finSession'
   },
   // page inconnue
   {
     path: '*', name: 'notFound', component: NotFound,
   },
 ]

 // le routeur
 const router = new VueRouter({
   // les routes
   routes,
   // le mode d'affichage des URL
   mode: 'history',
   // l'URL de base de l'application
   base: '/client-vuejs-impot/'
 })

 // vérification des routes
 router.beforeEach((to, from, next) => {
   // eslint-disable-next-line no-console
   console.log("router to=", to, "from=", from);
   // route réservée aux utilisateurs authentifiés ?
   if (to.meta.authenticated && !session.authenticated) {
     next({
       // on passe à l'authentification
       name: 'authentification',
     })
     // retour à la boucle événementielle
     return;
   }
   // cas particulier de la fin de session
   if (to.name === "finSession") {
     // on nettoie la session
     session.clear();
     // on va sur la vue [authentification]
     next({
       name: 'authentification',
     })
     // retour à la boucle événementielle
     return;
   }
   // autres cas - vue suivante normale du routage
   next();
 })

 // export du router
 export default router

Commentaires

  • lignes 16-38 : certaines routes ont été enrichies d’informations supplémentaires ;
  • ligne 19 : on a créé une nouvelle route pour aller à la vue [Authentification] ;
  • lignes 21-24 : la route qui mène à la vue [CalculImpot] a maintenant une propriété [meta] (ce nom est obligatoire). Le contenu de cet objet peut être quelconque et est fixé par le développeur ;
  • ligne 23 : on met dans [meta], la propriété [authenticated] (ce nom peut être quelconque). Il signifiera pour nous que pour aller à la vue [CalculImpot], l’utilisateur doit être authentifié ;
  • lignes 26-29 : on fait la même chose pour la route qui mène à la vue [ListeSimulations]. Là aussi, l’utilisateur doit être authentifié ;
  • la propriété [meta.authenticated] va nous permettre de vérifier qu’un utilisateur qui tape manuellement les URL des vues [CalculImpot, ListeSimulations] ne peut pas les obtenir s’il n’est pas authentifié ;
  • lignes 51-76 : la méthode [beforeEach] est exécutée avant qu’une vue ne soit routée. C’est le bon moment pour faire des vérifications ;
    • [to] : la prochaine route si on ne fait rien ;
    • [from] : la dernière route affichée ;
    • [next] : fonction permettant de changer la prochaine route affichée ;
  • ligne 55 : on regarde si la prochaine route demande à ce que l’utilisateur soit authentifié ;
  • lignes 56-59 : si oui et que l’utilisateur n’est pas authentifié, on change la prochaine route vers la vue [Authentification] ;
  • lignes 64-73 : on traite le cas particulier de la route [finSession] des lignes 30-32. Celle-ci n’a pas de vue associée ;
    • ligne 66 : on réinitialise la session à sa valeur initiale ;
    • lignes 68-70 : on programme la vue [Authentification] comme prochaine vue ;
  • ligne 75 : si on n’est pas dans les deux cas précédents, on se contente de passer à la route prévue par le fichier de routage ;
  • lignes 35-37 : on prévoit une vue [NotFound] si la route tapée par l’utilisateur ne correspond à aucune route connue. Cette vue est importée ligne 8. Les routes sont vérifiées dans l’ordre du fichier de routage. Si donc on arrive à la ligne 36, c’est que la route demandée n’est aucune des routes des lignes 18-33 ;

La vue [NotFound]

La vue [NotFound] est affichée si la route tapée par l’utilisateur ne correspond à aucune route connue :

image1

Le code de la vue est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
 <!-- définition HTML de la vue -->
 <template>
   <!-- mise en page -->
   <Layout :left="true" :right="true">
     <!-- alerte dans la colonne de droite -->
     <template slot="right">
       <!-- message sur fond jaune -->
       <b-alert show variant="danger" align="center">
         <h4>Cette page n'existe pas</h4>
       </b-alert>
     </template>
     <!-- menu de navigation dans la colonne de gauche -->
     <Menu slot="left" :options="options" />
   </Layout>
 </template>

 <script>
 // imports
 import Layout from "./Layout";
 import Menu from "./Menu";
 export default {
   // composants
   components: {
     Layout,
     Menu
   },
   // état interne du composant
   data() {
     return {
       // options du menu de navigation
       options: [
         {
           text: "Authentification",
           path: "/"
         }
       ]
     };
   },
   // cycle de vie
   created() {
     // eslint-disable-next-line
     console.log("NotFound created");
     // on regarde quelles options de menu offrir
     if (this.$session.authenticated && this.$métier.taxAdminData) {
       // l'utilisateur peut faire des simulations
       Array.prototype.push.apply(this.options, [
         {
           text: "Calcul de l'impôt",
           path: "/calcul-impot"
         },
         {
           text: "Liste des simulations",
           path: "/liste-des-simulations"
         }
       ]);
     }
   }
 };
 </script>

Commentaires

  • ligne 4 : elle utilise les deux colonnes des vues routées ;
  • lignes 6-11 : un message d’erreur ;
  • ligne 13 : le menu de navigation occupe la colonne de gauche ;
  • lignes 31-36 : les options par défaut du menu ;
  • lignes 40-57 : code exécuté lorsque la vue est créée ;
  • ligne 44 : on regarde si l’utilisateur peut faire des simulations ;
  • lignes 45-55 : si oui, on ajoute deux options au menu de navigation, celles où il faut être authentifié et avoir une couche [métier] opérationnelle (lignes 46-55) ;
La vue [Authentification]

La vue [Authentification] évolue comme suit :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
 <!-- définition HTML de la vue -->
 <template>
   <Layout :left="false" :right="true">
     ...
   </Layout>
 </template>

 <!-- dynamique de la vue -->
 <script>
 import Layout from "./Layout";
 export default {
   // état du composant
   data() {
     return {
       // utilisateur
       user: "",
       // son mot de passe
       password: "",
       // contrôle l'affichage d'un msg d'erreur
       showError: false,
       // le message d'erreur
       message: ""
     };
   },

   // composants utilisés
   components: {
     Layout
   },

   // propriétés calculées
   computed: {
     // saisies valides
     valid() {
       return this.user && this.password && this.$session.started;
     }
   },

   // gestionnaires d'évts
   methods: {
     // ----------- authentification
     async login() {
       try {
         // début attente
         this.$emit("loading", true);
         // on n'est pas encore authentifié
         this.$session.authenticated = false;
         // authentification bloquante auprès du serveur
         const response = await this.$dao.authentifierUtilisateur(
           this.user,
           this.password
         );
         // fin du chargement
         this.$emit("loading", false);
         // analyse de la réponse du serveur
         if (response.état != 200) {
           // on affiche l'erreur
           this.message = response.réponse;
           this.showError = true;
           // retour à la boucle événementielle
           return;
         }
         // pas d'erreur
         this.showError = false;
         // on est authentifié
         this.$session.authenticated = true;
         // --------- on demande maintenant les données de l'administration fiscale
         // au départ, pas de donnée
         this.$métier.setTaxAdminData(null);
         // début attente
         this.$emit("loading", true);
         // demande bloquante auprès du serveur
         const response2 = await this.$dao.getAdminData();
         // fin du chargement
         this.$emit("loading", false);
         // analyse de la réponse
         if (response2.état != 1000) {
           // on affiche l'erreur
           this.message = response2.réponse;
           this.showError = true;
           // retour à la boucle événementielle
           return;
         }
         // pas d'erreur
         this.showError = false;
         // on mémorise dans la couche [métier] la donnée reçue
         this.$métier.setTaxAdminData(response2.réponse);
         // on peut passer au calcul de l'impôt
         this.$router.push({ name: "calculImpot" });
       } catch (error) {
         // on remonte l'erreur au composant principal
         this.$emit("error", error);
       } finally {
         // maj session
         this.$session.métier = this.$métier;
         // on sauvegarde la session
         this.$session.save();
       }
     }
   },
   // cycle de vie : le composant vient d'être créé
   created() {
     // eslint-disable-next-line
     console.log("Authentification created");
     // l'utilisateur peut-il faire des simulations ?
     if (
       this.$session.started &&
       this.$session.authenticated &&
       this.$métier.taxAdminData
     ) {
       // alors l'utilisateur peut faire des simulations
       this.$router.push({ name: "calculImpot" });
       // retour à la boucle événementielle
       return;
     }
     // si la session jSON a déjà été démarrée, on ne la redémarre pas de nouveau
     if (!this.$session.started) {
       // début attente
       this.$emit("loading", true);
       // on initialise la session avec le serveur - requête asynchrone
       // on utilise la promesse rendue par les méthodes de la couche [dao]
       this.$dao
         // on initialise une session jSON
         .initSession()
         // on a obtenu la réponse
         .then(response => {
           // fin attente
           this.$emit("loading", false);
           // analyse de la réponse
           if (response.état != 700) {
             // on affiche l'erreur
             this.message = response.réponse;
             this.showError = true;
             // retour à la boucle événementielle
             return;
           }
           // la session a démarré
           this.$session.started = true;
         })
         // en cas d'erreur
         .catch(error => {
           // on remonte l'erreur à la vue [Main]
           this.$emit("error", error);
         })
         // dans tous les cas
         .finally(() => {
           // on sauvegarde la session
           this.$session.save();
         });
     }
   }
 };
 </script>

Commentaires

  • on a surligné en jaune les instructions qui utilisent la session introduite dans cette version du client [Vue.js] ;
  • lignes 97, 148 : à la fin des méthodes [login, created], la session est sauvegardée quelque soit le résultat des requêtes HTTP qui ont lieu dans ces méthodes (clause [finally] dans les deux cas) ;
  • la méthode [created] des lignes 102-150 est exécutée à chaque fois que la vue [Authentification] est créée. Si c’est l’utilisateur qui a tapé l’URL de la vue, la session va nous permettre de savoir quoi faire ;
  • lignes 106-115 : si la session jSON est démarrée, l’utilisateur authentifié et la donnée [this.$métier.taxAdminData] initialisée alors l’utilisateur peut directement aller au formulaire de calcul de l’impôt (ligne 112) ;
  • ligne 117 : la méthode [created] était utilisée dans la version précédente pour initialiser une session jSON avec le serveur. Cette phase est inutile si elle a déjà eu lieu ;
  • lignes 42-66 : la méthode d’authentification ;
  • ligne 66 : si l’authentification réussit, on le note dans la session ;
  • lignes 67-92 : la demande au serveur des données de l’administration fiscale [taxAdminData] ;
  • ligne 95 : à la fin de cette phase, on met à jour la propriété [métier] de la session que l’opération ait réussi ou pas ;

La vue [CalculImpot]

Le code de la vue [CalculImpot] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 <!-- définition HTML de la vue -->
 <template>
   ...
 </template>

 <script>
 // imports
 import FormCalculImpot from "./FormCalculImpot";
 import Menu from "./Menu";
 import Layout from "./Layout";

 export default {
   // état interne
   data() {
     return {
       // options du menu
       options: [
         {
           text: "Liste des simulations",
           path: "/liste-des-simulations"
         },
         {
           text: "Fin de session",
           path: "/fin-session"
         }
       ],
       // résultat du calcul de l'impôt
       résultat: "",
       résultatObtenu: false
     };
   },
   // composants utilisés
   components: {
     Layout,
     FormCalculImpot,
     Menu
   },
   // méthodes de gestion des évts
   methods: {
     // résultat du calcul de l'impôt
     handleResultatObtenu(résultat) {
       // on construit le résultat en chaîne HTML
       ...
       // une simulation de +
       this.$store.commit("addSimulation", résultat);
       // on sauvegarde la session
       this.$session.save();
     }
   },
   // cycle de vie
   created() {
     // eslint-disable-next-line
     console.log("CalculImpot created");
   }
 };
 </script>

Commentaires

  • ligne 45 : la simulation calculée est ajoutée au store [Vuex]. Cela a un impact sur la session qui englobe la propriété [state] du store. Aussi sauvegarde-t-on la session (ligne 47) ;
  • ligne 51 : on crée une méthode [created] pour suivre dans les logs les créations des vues ;

La vue [ListeSimulations]

La vue [ListeSimulations] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 <!-- définition HTML de la vue -->
 <template>
   ...
   </div>
 </template>

 <script>
 // imports
 import Layout from "./Layout";
 import Menu from "./Menu";
 export default {
   // composants
   components: {
     Layout,
     Menu
   },
   // état interne
   data() {
     ...
   },
   // état interne calculé
   computed: {
     // liste des simulations prise dans le store Vuex
     simulations() {
       return this.$store.state.simulations;
     }
   },
   // méthodes
   methods: {
     supprimerSimulation(index) {
       // eslint-disable-next-line
       console.log("supprimerSimulation", index);
       // suppression de la simulation n° [index]
       this.$store.commit("deleteSimulation", index);
       // on sauvegarde la session
       this.$session.save();
     }
   },
   // cycle de vie
   created() {
     // eslint-disable-next-line
     console.log("ListeSimulations created");
   }
 };
 </script>

Commentaires

  • ligne 36 : après la suppression d’une simulation ligne 34, on sauvegarde la session pour tenir compte de ce changement d’état ;
  • lignes 40-43 : on continue à suivre la création des vues ;

Exécution du projet

image2

Lors des tests vérifiez les points suivants :

  • si l’utilisateur ‘utilise’ l’application via les liens du menu de navigation et les boutons / liens d’action, celle-ci fonctionne ;
  • si l’utilisateur tape manuellement des URL, l’application continue à fonctionner. Faites en particulier le test suivant :
    • faites simulations ;
    • une fois sur la vue [ListeSimulations], rechargez (F5) la vue. Dans l’application précédente [vuejs-20], on perdait alors les simulations. Ici ce n’est pas le cas : on retrouve bien les simulations déjà faites ;
  • regardez les logs pour comprendre :
    • à quel moment le script [main] est exécuté. Vous devez voir qu’il l’est à chaque fois que l’utilisateur tape une URL à la main ;
    • à quels moments les vues sont créées. Vous devez voir qu’elles le sont à chaque fois qu’elles vont être affichées ;
    • le fonctionnement du routage. Avant chaque routage un log est fait qui vous indique :
      • la route d’où vous venez ;
      • la route où vous allez ;

Déploiement de l’application sur un serveur local

Comme exercice, suivez le paragraphe |Déploiement sur un serveur local|, pour déployer le projet [vuejs-21] sur le serveur Laragon local. Puis testez-le.

Mise au point de la version mobile

Théoriquement, l’utilisation de Bootstrap devrait nous permettre d’avoir une application utilisable sur différents média : smartphone, tablette, ordinateurs portable et de bureau. Cequi différencie ces média c’est la taille de leur écran.

Si on teste la version [vuejs-21] sur un mobile, on constate que c’est le chaos dans l’affichage des vues. La version [vuejs-22] corrige ce point. Les modifications ont toutes lieu dans les templates des vues. Elles ont consisté essentiellement à mettre au point un affichage pour un écran de smartphone. Lorsque celui-ci est au point, l’affichage sur des écrans de taille plus importante se passe de façon fluide grâce à Bootstrap.

image3

La vue [Main]

La vue [Main] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 <!-- définition HTML de la vue -->
 <template>
   <div class="container">
     <b-card>
       <!-- jumbotron -->
       <b-jumbotron>
         <b-row>
           <b-col sm="4">
             <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
           </b-col>
           <b-col sm="8">
             <h1>Calculez votre impôt</h1>
           </b-col>
         </b-row>
       </b-jumbotron>
       ....
     </b-card>
   </div>
 </template>

Commentaires

  • ligne 8 : là où il y avait [cols=’4’] on écrit [sm=’4’]. [sm] signifie [small]. Les écrans des smartphones tombent dans cette catégorie. Les autres catégories sont [xs=extra small, md=medium, lg=large, xl=extra large] ;
  • ligne 11 : idem ;

La vue [Layout]

La vue [Layout] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 <!-- définition HTML de la mise en page de la vue routée -->
 <template>
   <!-- ligne -->
   <div>
     <b-row>
       <!-- zone de trois colonnes à gauche -->
       <b-col sm="3" v-if="left">
         <slot name="left" />
       </b-col>
       <!-- zone de neuf colonnes à droite -->
       <b-col sm="9" v-if="right">
         <slot name="right" />
       </b-col>
     </b-row>
   </div>
 </template>
La vue [Authentification]

La vue [Authentification] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 <!-- définition HTML de la vue -->
 <template>
   <Layout :left="false" :right="true">
     <template slot="right">
       <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
       <b-form @submit.prevent="login">
         <!-- titre -->
         <b-alert show variant="primary">
           <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
         </b-alert>
         <!-- 1ère ligne -->
         <b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
           <!-- zone de saisie user -->
           <b-col sm="6">
             <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
           </b-col>
         </b-form-group>
         <!-- 2ième ligne -->
         <b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
           <!-- zone de saisie password -->
           <b-col sm="6">
             <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
           </b-col>
         </b-form-group>
         <!-- 3ième ligne -->
         <b-alert
           show
           variant="danger"
           v-if="showError"
           class="mt-3"
         >L'erreur suivante s'est produite : {{message}}</b-alert>
         <!-- bouton de type [submit] sur une 3ième ligne -->
         <b-row>
           <b-col sm="2">
             <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
           </b-col>
         </b-row>
       </b-form>
     </template>
   </Layout>
 </template>

Commentaires

  • lignes 11 et 19 : on a supprimé l’attribut [label-cols] qui fixait un nombre de colonnes au label de la saisie. En l’absence de cet attribut, le label est au-dessus de la zone de saisie. Cela convient mieux aux écrans des smartphones ;

La vue [CalculImpot]

La vue [CalculImpot] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <!-- définition HTML de la vue -->
 <template>
   <div>
     <Layout :left="true" :right="true">
       <!-- formulaire de calcul de l'impôt à droite -->
       <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
       <!-- menu de navigation à gauche -->
       <Menu slot="left" :options="options" />
     </Layout>
     <!-- zone d'affichage des résultat du calcul de l'impôt sous le formulaire -->
     <b-row v-if="résultatObtenu" class="mt-3">
       <!-- zone de trois colonnes vide -->
       <b-col sm="3" />
       <!-- zone de neuf colonnes -->
       <b-col sm="9">
         <b-alert show variant="success">
           <span v-html="résultat"></span>
         </b-alert>
       </b-col>
     </b-row>
   </div>
 </template>

La vue [FormCalculImpot]

La vue [FormCalculImpot] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
 <!-- définition HTML de la vue -->
   <template>
   <!-- formulaire HTML -->
   <b-form @submit.prevent="calculerImpot" class="mb-3">
     <!-- message sur 12 colonnes sur fond bleu -->
     <b-row>
       <b-col sm="12">
         <b-alert show variant="primary">
           <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
         </b-alert>
       </b-col>
     </b-row>
     <!-- éléments du formulaire -->
     <!-- première ligne -->
     <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
       <!-- boutons radio sur 5 colonnes-->
       <b-col sm="5">
         <b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
         <b-form-radio v-model="marié" value="non">Non</b-form-radio>
       </b-col>
     </b-form-group>
     <!-- deuxième ligne -->
     <b-form-group label="Nombre d'enfants à charge" label-for="enfants">
       <b-form-input
         type="text"
         id="enfants"
         placeholder="Indiquez votre nombre d'enfants"
         v-model="enfants"
         :state="enfantsValide"
       ></b-form-input>
       <!-- message d'erreur éventuel -->
       <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
     </b-form-group>
     <!-- troisème ligne -->
     <b-form-group
       label="Salaire annuel net imposable"
       label-for="salaire"
       description="Arrondissez à l'euro inférieur"
     >
       <b-form-input
         type="text"
         id="salaire"
         placeholder="Salaire annuel"
         v-model="salaire"
         :state="salaireValide"
       ></b-form-input>
       <!-- message d'erreur éventuel -->
       <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
     </b-form-group>
     <!-- quatrième ligne, bouton [submit] -->
     <b-col sm="3">
       <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
     </b-col>
   </b-form>
 </template>

Commentaires

  • lignes 15, 23, 35 : on a supprimé l’attribut [label-cols] ;

Par ailleurs, on fait évoluer les tests de validité :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 ...
 // état interne calculé
   computed: {
     // validation du formulaire
     formInvalide() {
       return (
         // salaire invalide
         !this.salaire.match(/^\s*\d+\s*$/) ||
         // ou enfants invalide
         !this.enfants.match(/^\s*\d+\s*$/) ||
         // ou données fiscales pas obtenues
         !this.$métier.taxAdminData
       );
     },
     // validation du salaire
     salaireValide() {
       // doit être numérique >=0
       return Boolean(
         this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
       );
     },
     // validation des enfants
     enfantsValide() {
       // doit être numérique >=0
       return Boolean(
         this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
       );
     }
 },
 ...

Commentaires

  • ligne 19 : lorsque rien n’a été saisi, la saisie est considérée comme valide. Cela permet d’avoir une saisie valide lorsque la vue est initialement affichée. Dans la version précédente, la saisie apparaissait initialement comme erronée ;
  • ligne 26 : idem ;
  • lignes 5-14 : le bouton de validation n’est actif que si les deux saisies contiennent quelque chose et sont valides ;

La vue [Menu]

La vue [Menu] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 <!-- définition HTML de la vue -->
 <template>
   <b-card class="mb-3">
     <!-- menu Bootstrap vertical -->
     <b-nav vertical>
       <!-- options du menu -->
       <b-nav-item
         v-for="(option,index) of options"
         :key="index"
         :to="option.path"
         exact
         exact-active-class="active"
       >{{option.text}}</b-nav-item>
     </b-nav>
   </b-card>
 </template>

Commentaires

  • ligne 3 : on ajoute la balise <b-card> pour entourer le menu d’une fine bordure. Cela permet de mieux localiser le menu sur le smartphone ;

La vue [ListeSimulations]

La vue [ListeSimulations] reste inchangée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 <!-- définition HTML de la vue -->
 <template>
   <div>
     <!-- mise en page -->
     <Layout :left="true" :right="true">
       <!-- simulations dans colonne de droite -->
       <template slot="right">
         <template v-if="simulations.length==0">
           <!-- pas de simulations -->
           <b-alert show variant="primary">
             <h4>Votre liste de simulations est vide</h4>
           </b-alert>
         </template>
         <template v-if="simulations.length!=0">
           <!-- il y a des simulations -->
           <b-alert show variant="primary">
             <h4>Liste de vos simulations</h4>
           </b-alert>
           <!-- tableau des simulations -->
           <b-table striped hover responsive :items="simulations" :fields="fields">
             <template v-slot:cell(action)="data">
               <b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
             </template>
           </b-table>
         </template>
       </template>
       <!-- menu de navigation dans colonne de gauche -->
       <Menu slot="left" :options="options" />
     </Layout>
   </div>
 </template>

Commentaires

  • ligne 20 : on notera l’attribut [responsive] qui fait que l’affichage de la table s’adapte à la taille de l’écran :

image4

  • en [2], sur les petits écrans, une barre de défilement horizontal permet d’afficher la table ;

La vue [NotFound]

Elle reste inchangée.

Les vues sur mobile

image5

image6

Note : il y a sûrement possibilité d’obtenir des vues encore mieux adaptées au mobile. Je pense notamment au menu de navigation qui pourrait être amélioré mais il y a d’autres points. Ce document n’avait pas pour objectif premier la création d’une application mobile. Dans ce cas, on se serait peut-être tourné vers un framework comme Ionic |https://ionicframework.com/|.

Déploiement de l’application client / serveur sur un service d’hébergement

Nous donnons ici les grandes lignes du déploiement de l’application client / serveur que nous avons développée sur un serveur OVH [https://www.ovh.com/fr/]. Le déploiement sur d’autres fournisseurs d’hébergement ne devrait pas être très différent. On veut simplement montrer que notre application se prête bien à ce déploiement.

Déploiement du serveur

L’hébergement OVH visé est un hébergement basique :

  1. un environnement PHP 7.3 ;
  2. un SGBD MySQL ;
  3. pas de serveur [Redis] ;

Le 3ième point nous oblige à modifier la version 14 de notre serveur de calcul de l’impôt.

image0

Il nous faut modifier :

  • les fichiers de configuration [1] ;
  • les contrôleurs [AdminDataController] et [CalculerImpotController] pour tenir compte du fait qu’il n’y a pas de serveur [Redis] ;

Le fichier [config.json] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 {
     "databaseFilename": "Config/database.json",
     "corsAllowed": false,
     "redisAvailable":false,
     "rootDirectory": "/.../www/apps/impot/serveur-php7",
     "relativeDependencies": [

         "/Entities/BaseEntity.php",
         ...
         "/Controllers/AdminDataController.php"
     ],
     "absoluteDependencies": [
         "/.../vendor/autoload.php",
         "/.../vendor/predis/predis/autoload.php"
     ],
     "users": [
         {
             "login": "admin",
             "passwd": "admin"
         }
     ],
     ...
 }

Commentaires

  • ligne 4 : on introduit un booléen [redisAvailable] pour indiquer si on a accès ou non à un serveur [Redis] ;
  • lignes 5, 13, 14 : les chemins absolus vont changer ;

Le fichier [database.json] évolue comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 {
     "dsn": "mysql:host=...;dbname=...",
     "id": "...",
     "pwd": "...",
     "tableTranches": "dbimpots_tbtranches",
     "colLimites": "limites",
     "colCoeffR": "coeffr",
     "colCoeffN": "coeffn",
     "tableConstantes": "dbimpots_tbconstantes",
     "colPlafondQfDemiPart": "plafondQfDemiPart",
     ...
 }

Commentaires

  • lignes 2-4 : l’identité de la base de données ainsi que les identifiants de son propriétaire vont changer ;

Le contrôleur [AdminDataController] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
 <?php

 namespace Application;

 // dépendances Symfony
 use \Symfony\Component\HttpFoundation\Response;
 use \Symfony\Component\HttpFoundation\Request;
 use \Symfony\Component\HttpFoundation\Session\Session;
 // alias de la couche [dao]
 use \Application\ServerDaoWithSession as ServerDaoWithRedis;

 class AdminDataController implements InterfaceController {

   // $config est la configuration de l'application
   // traitement d'une requête Request
   // utile la session Session et peut la modifier
   // $infos sont des informations supplémentaires propres à chaque contrôleur
   // rend un tableau [$statusCode, $état, $content, $headers]
   public function execute(
     array $config,
     Request $request,
     Session $session,
     array $infos = NULL): array {

     // on doit avoir un unique paramètre GET
     $method = strtolower($request->getMethod());
     ...

     // on peut travailler
     // Redis
     if ($config["redisAvailable"]) {
       \Predis\Autoloader::register();
       ...
     } else {
       try {
         // on va chercher les données fiscales en base de données
         $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
         // taxAdminData
         $taxAdminData = $dao->getTaxAdminData();
       } catch (\Throwable $ex) {
         // ça s'est mal passé
         // retour résultat avec erreur au contrôleur principal
         $état = 1051;
         return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
           ["réponse" => utf8_encode($ex->getMessage())], []];
       }
     }

     // retour résultat au contrôleur principal
     $état = 1000;
     return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
   }

 }

Commentaires

  • ligne 31 : on teste désormais si on a ou non un serveur [Redis] ;
  • lignes 32-34 : si oui, le code précédent est repris dans son intégralité ;
  • lignes 35-46 : sinon, les données de l’administration fiscale sont prises dans la base de données ;

Le contrôleur [CalculerImpotController] qui lui également a besoin des données de l’administration fiscale évolue de façon identique.

Ceci fait. Le déploiement sur le serveur OVH a consisté à faire du FTP. On a téléchargé sur OVH :

  • la version [vuejs-14-without-redis] ;
  • le dossier [vendor] qui contient toutes les dépendances du serveur [vuejs-14-without-redis] ;

Le transfert FTP fait, on a généré les tables nécessaires au serveur avec le script SQL suivant :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
 -- phpMyAdmin SQL Dump
 -- version 4.8.5
 -- https://www.phpmyadmin.net/
 --
 -- Host: localhost:3306
 -- Generation Time: Oct 12, 2019 at 07:45 AM
 -- Server version: 5.7.24
 -- PHP Version: 7.2.11

 SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
 SET AUTOCOMMIT = 0;
 START TRANSACTION;
 SET time_zone = "+00:00";


 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
 /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
 /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
 /*!40101 SET NAMES utf8mb4 */;

 --
 -- Table structure for table `dbimpots_tbconstantes`
 --

 CREATE TABLE `dbimpots_tbconstantes` (
   `id` int(11) NOT NULL,
   `plafondQfDemiPart` decimal(10,2) NOT NULL,
   `plafondRevenusCelibatairePourReduction` decimal(10,2) NOT NULL,
   `plafondRevenusCouplePourReduction` decimal(10,2) NOT NULL,
   `valeurReducDemiPart` decimal(10,2) NOT NULL,
   `plafondDecoteCelibataire` decimal(10,2) NOT NULL,
   `plafondDecoteCouple` decimal(10,2) NOT NULL,
   `plafondImpotCelibatairePourDecote` decimal(10,2) NOT NULL,
   `plafondImpotCouplePourDecote` decimal(10,2) NOT NULL,
   `abattementDixPourcentMax` decimal(10,2) NOT NULL,
   `abattementDixPourcentMin` decimal(10,2) NOT NULL
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 --
 -- Dumping data for table `dbimpots_tbconstantes`
 --

 INSERT INTO `dbimpots_tbconstantes` (`id`, `plafondQfDemiPart`, `plafondRevenusCelibatairePourReduction`, `plafondRevenusCouplePourReduction`, `valeurReducDemiPart`, `plafondDecoteCelibataire`, `plafondDecoteCouple`, `plafondImpotCelibatairePourDecote`, `plafondImpotCouplePourDecote`, `abattementDixPourcentMax`, `abattementDixPourcentMin`) VALUES
 (8, '1551.00', '21037.00', '42074.00', '3797.00', '1196.00', '1970.00', '1595.00', '2627.00', '12502.00', '437.00');

 -- --------------------------------------------------------

 --
 -- Table structure for table `dbimpots_tbtranches`
 --

 CREATE TABLE `dbimpots_tbtranches` (
   `id` int(11) NOT NULL,
   `limites` decimal(10,2) NOT NULL,
   `coeffR` decimal(10,2) NOT NULL,
   `coeffN` decimal(10,2) NOT NULL
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 --
 -- Dumping data for table `dbimpots_tbtranches`
 --

 INSERT INTO `dbimpots_tbtranches` (`id`, `limites`, `coeffR`, `coeffN`) VALUES
 (36, '9964.00', '0.00', '0.00'),
 (37, '27519.00', '0.14', '1394.96'),
 (38, '73779.00', '0.30', '5798.00'),
 (39, '156244.00', '0.41', '13913.69'),
 (40, '0.00', '0.45', '20163.45');

 --
 -- Indexes for dumped tables
 --

 --
 -- Indexes for table `dbimpots_tbconstantes`
 --
 ALTER TABLE `dbimpots_tbconstantes`
   ADD PRIMARY KEY (`id`);

 --
 -- Indexes for table `dbimpots_tbtranches`
 --
 ALTER TABLE `dbimpots_tbtranches`
   ADD PRIMARY KEY (`id`);

 --
 -- AUTO_INCREMENT for dumped tables
 --

 --
 -- AUTO_INCREMENT for table `dbimpots_tbconstantes`
 --
 ALTER TABLE `dbimpots_tbconstantes`
   MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=9;

 --
 -- AUTO_INCREMENT for table `dbimpots_tbtranches`
 --
 ALTER TABLE `dbimpots_tbtranches`
   MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=41;
 COMMIT;

 /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
 /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
 /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

Lorsque tout ceci a été fait, on a adapté les fichiers [config.json, database.json] à leur nouvel environnement.

Déploiement du client [Vue.js]

Il a été décidé de déployer le client [Vue.js] à l’URL [http://machine/apps/impot/client-vuejs/]. Cela a entraîné les modifications suivantes :

A la racine du [workspace] de [VSCode] on a créé le fichier [vue.config.js] suivant :

image1

Le fichier [vue.config.js] est le suivant :

1
2
3
4
5
 // vue.config.js
 module.exports = {
   // l'URL de service du client [vuejs] du serveur de calcul de l'impôt
   publicPath: '/apps/impot/client-vuejs/'
 }

Le fichier [router.js] [3] a été également modifié :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 // imports
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 ...

 // plugin de routage
 Vue.use(VueRouter)

 // les routes de l'application
 const routes = [
   ...
 ]

 // le routeur
 const router = new VueRouter({
   // les routes
   routes,
   // le mode d'affichage des URL
   mode: 'history',
   // l'URL de base de l'application
   base: '/apps/impot/client-vuejs/'
 })

 // vérification des routes
 router.beforeEach((to, from, next) => {
   ...
 })

 // export du router
 export default router

Commentaires

  • ligne 21 : la base des URL a été modifiée ;

Le fichier [config.js] est modifié de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 // utilisation de la bibliothèque [axios]
 const axios = require('axios');
 // timeout des requêtes HTTP
 axios.defaults.timeout = 5000;
 // la base des URL du serveur de calcul de l'impôt
 // le schéma [https] pose des problèmes à Firefox parce que le serveur de calcul
 // de l'impôt envoie un certificat autosigné. ok avec Chrome et Edge. Safari pas testé.
 // avec Firefox c'est possible en demandant l'URL ci-dessous directement et en disant à Firefox
 // que vous acceptez le risque d'un certificat non signé. Ensuite le client [vuejs] fonctionnera.
 axios.defaults.baseURL = 'http://.../apps/impot/serveur-php7';
 // on va utiliser des cookies
 axios.defaults.withCredentials = true;

 // export de la configuration
 export default {
   // objet [axios]
   axios: axios,
   // délai maximal d'inactivité de la session : 5 mn = 300 s = 300000 ms
   duréeSession: 300000
 }

Commentaires

  • ligne 10 : on met l’URL du serveur de calcul de l’impôt ;

La version de production du projet a été générée avec la commande [build] du fichier [package.json] [5] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 {
   "name": "vuejs",
   "version": "0.1.0",
   "private": true,
   "scripts": {
     "serve": "vue-cli-service serve vuejs-22/main.js",
     "build": "vue-cli-service build vuejs-22-ovh-withBootstrapVue/main.js",
     "lint": "vue-cli-service lint"
   },
   ...
 }

Ceci fait, le dossier [dist] qui contenait la version de production générée a été ‘uploadée’ sur le serveur OVH dans le dossier [/…/apps/impot] puis renommé [client-vuejs] pour que le code du client soit dans le dossier [/…/apps/impot/client-vuejs/] comme il était prévu. Puis dans ce dossier nous avons téléchargé le fichier [.htaccess] suivant :

1
2
3
4
5
6
7
8
 <IfModule mod_rewrite.c>
   RewriteEngine On
   RewriteBase /apps/impot/client-vuejs/
   RewriteRule ^index\.html$ - [L]
   RewriteCond %{REQUEST_FILENAME} !-f
   RewriteCond %{REQUEST_FILENAME} !-d
   RewriteRule . /apps/impot/client-vuejs/index.html [L]
 </IfModule>

ceci parce que le serveur web d’OVH utilisé ici est un serveur Apache. Pour d’autres types de serveurs, on se reportera à la documentation |https://cli.vuejs.org/guide/deployment.html|.

L’application serveur PHP 7 peut être testée |ici|.

Le client [Vue.js] peut être testé |ici|.

Conclusion

La version [vuejs-21] n’était pas indispensable. On avait vu que la version [vuejs-20] résistait correctement aux URL tapées par l’utilisateur. Néanmoins la nouvelle version amène un confort supplémentaire à l’utilisateur. Il peut naviguer en tapant des URL. L’application lui propose alors la vue qui convient le mieux à l’état actuel (la session) de l’application. Par ailleurs, la version [vuejs-22] amène des améliorations pour l’affichage de l’application sur mobiles.