Préambule
 
  • Les démonstrations de ce TD se basent sur les sources disponibles [ ici ].
  • Attention, seul le répertoire src est dans l'archive. Il faut donc créer un projet avec vue-cli et remplacer son répertoire src.

 

1°/ Un dialogue avec résultat

  • Comme vu dans le TD consacré à vuetify, le composant v-dialog permet d'afficher au centre de la page du navigateur une boîte de dialogue. On peut notamment fixer sa largeur et son contenu librement.
  • En revanche, cette boîte est forcément reliée au composant père où on l'instancie. Ce n'est donc pas une fenêtre applicative, au sens des dialogues en Java.
  • Cela implique que ce qui permet à la boîte de s'afficher/disparaître doit se trouver dans ce composant.
  • Il faut notamment une variable booléenne qui permet de spécifier si la dialogue est visible ou non et faire un data-binding dessus.
  • On peut ensuite créer un élément cliquable (par ex, un bouton) qui va faire passer cette variable à true afin de montrer le dialogue, et celui-ci contient d'autres boutons permettant de mettre la variable à false et ainsi masquer le dialogue.
  • Généralement, ce masquage est fait grâce à une fonction qui va faire un traitement particulier après la fermeture du dialogue, selon le bouton que l'on a cliqué pour fermer.
  • Ce principe est simple mais il devient fastidieux à mettre en place lorsqu'un composant peut afficher différents dialogues avec la même structure mais avec différents traitements selon le dialogue ET le bouton cliqué.
  • Prenons le cas très courant de dialogues avec un titre, un texte et deux boutons : ok & cancel.
  • Supposons un composant qui, selon les interactions de l'utilisateur, va afficher deux types de dialogue avec cette structure mais un titre et texte différent. Qui plus est, le traitement à faire après clic dépend du bouton et du dialogue, soit 4 possibilités de traitement, représentés par 4 fonctions dans methods.
  • Il y a globalement 2 solutions basiques :
    • créer deux instances différentes du dialogue. Cela implique que chacun des 4 boutons appelle une des 4 fonctions de traitement.
    • créer une seule instance customisable par props. Cela implique une méthode qui met à jour ces props en fonction de la version du dialogue à afficher, une variable supplémentaire qui stocke cette version, et enfin deux fonctions, un pour chaque type de bouton (ok + cancel) qui vont appeler une des 4 fonctions de traitement par rapport à la version du dialogue (cf. démo).
  • Dans les deux cas, c'est du code à écrire, qui va un peu à l'encontre de la philosophie de réutilisation des composants.

 

  • Si on regarde ce qui se fait dans d'autre langages, on constate qu'il est possible de créer des dialogues "à la volée", qui bloquent l'exécution tant qu'ils sont visibles, et qui renvoient une valeur différente selon le bouton cliqué.
  • Cela serait fort pratique d'avoir de tels dialogues en vuejs et c'est effectivement possible.
  • La seule différence est que la dialogue reste un sous-composant, qu'il n'est donc pas créé à la volée.
  • En revanche, on va pouvoir l'afficher grâce à l'appel d'une fonction, qui va retourner une valeur en fonction du bouton cliqué.
  • Pour cela, il faut utiliser les promesses et l'attribut ref permettant de référencer un composant pour manipuler l'objet associé dans le DOM.
  • Le principe général est :
    • on crée un composant, par ex. nommé ConfirmDialog, dont le template est basé sur v-dialog, avec des props qui permettent de spécifier le titre et le texte.
    • le v-dialog utilise une variable locale pour piloter son affichage/masquage.
    • ce composant contient des fonctions open(), accept(), cancel().
    • open() passe la variable à true pour afficher le v-dialog puis crée une promesse qui n'est résolue qu'en cas d'appel à accept() ou cancel(). Bien entendu, ces 2 méthodes sont appelées lorsque l'on clique sur le bouton Ok ou Cancel.
    • la fonction accept() résous la promesse avec comme résultat true, alors que cancel() résous la promesse avec comme résultat false (NB : pas de cas d'erreur). Les 2 masquent le v-dialog. On obtient donc un composant qui s'auto-masque.
    • dans un composant père, on crée une instance de ConfirmDialog, avec un attribut ref, par ex. ref="mydialog". Cet attribut est spécifique à vuejs et permet dans la partie script de manipuler l'objet dans le DOM correspondant à un composant ou une balise, Pour cela, il suffit d'utiliser this.$ref.nom_ref.
    • quand l'utilisateur fait une action qui doit montrer le dialogue, on appelle une fonction de methods. S'il y a plusieurs interactions montrant le dialogue, on écrit une fonction par type d'interaction.
    • chacune de ces fonctions assigne une valeur aux variables qui servent de props au dialogue, puis elle appelle sa fonction open() , et attend son résultat. Par ex, let res = await this.$refs.mydialog.open()
    • il suffit de vérifier res pour savoir si l'utilisateur à cliqué sur Ok ou Cancel et faire le traitement adapté.

 

  • Cette solution est beaucoup plus souple à mettre en place et demande moins de code à écrire.
  • Elle est particulièrement adaptée aux cas de multi-confirmation.
  • On peut même l'améliorer en ajoutant des props et des slots afin de customiser un peu plus le contenu, par exemple avec un nombre variable de boutons, un contenu avec des champs de saisie, boutons radio, etc., des valeurs renvoyées plus complexes, ...

 

Démonstration :
  • Renommer App.vue.1 dans App.vue.
  • lancer npm run serve puis visualiser le résultat dans le navigateur (par ex. http://localhost:8080)
  • Montrer le code de EventDialog.vue. qui encapsule un dialogue avec un fonctionnement classique :
    • le dialogue utilise la props show fournie par le composant père comme déclencheur d'affichage et émet un événement closeDialog avec true/false comme valeur quand on doit le fermer.
    • il ne se masque pas lui-même mais compte sur le composant père pour capturer cet événement et remettre la props show à false pour masquer le dialogue.
    • les autres props permettent de customiser le contenu du dialogue.
  • Montrer le code de App.vue et les fonctions xxxx1() qui concernent la gestion du dialogue classique.
  • Il n'y a qu'une seule instance de EventDialog, mais deux types d'interaction (case à cocher et bouton) permettant de l'afficher. C'est pourquoi on utilise la variable idInteraction pour stocker ce type.
  • Quand on capture l'événement closeDialog, on appelle la fonction getAnswerAndClose(), qui sert de switch. En fonction de idInteraction, elle appelle d'autres fonctions pour faire des traitements paramétrés par la valeur de l'événement (true ou false)
  • Montrer le code de PromiseDialog.vue. qui encapsule un dialogue avec un fonctionnement basé sur les promesses. On remarque que cela correspond bien au principe décrit plus haut, notamment le fait que l'affichage du dialogue se fait par programmation et non interaction, et que l'on a pas besoin de gérer de façon externe le masquage du dialogue.
  • Dans App.vue, montrer les fonctions xxxx2() qui concernent la gestion du dialogue avec promesse. On remarque qu'une fois les logs console enlevés, il y a moins de code à écrire qu'avec la solution classique.

 

2°/ traiter et afficher les erreurs

  • Dans la plupart des applications, une erreur provoque l'affichage d'un dialogue mentionnant l'erreur plus un bouton pour fermer le dialogue.
  • Dans certains cas, une erreur ramène l'utilisateur à un point bien précis de l'application.
  • Malheureusement, en vuetify, les dialogues ne sont pas reliés à l'application mais à un composant bien précis.
  • Cela veut dire que si une interaction d'un utilisateur dans un composant A provoque une erreur et donc l'apparition d'un dialogue, il faudrait en principe que ce dialogue soit instancié dans le composant A.
  • Cependant, grâce à vuex, il est possible de contourner cette difficulté et de n'avoir qu'un seul dialogue affichant les erreurs de toute l'application dans le composant racine App.
  • Le principe : 
    • dans le store, on définit deux variables du state : une pour signifier qu'il y a une erreur, l'autre pour contenir le message d'erreur. Par exemple, isError et errorMsg.
    • on définit également deux mutations :
      • une permettant de passer isError à true et mettre à jour errorMsg. Par exemple, pushError()
      • une permettant de passer isError à false. Par exemple, popError()
    • on définit un composant basé sur v-dialog. Ce composant mappe isError et msgError (grâce à mapState). Le dialogue est affiché en fonction de la valeur de isError. Il contient un bouton qui appelle la fonction popError() et donc masquer le dialogue
    • on crée dans App une instance de ce composant.
    • dans n'importe quel composant générant une erreur, on mappe pushError() et dès qu'il y a une erreur on l'appelle.
  • Le fait d'appeler pushError() va automatiquement provoquer l'apparition du dialogue au centre de la fenêtre, quel que soit le composant qui a appelé la fonction.

Remarques :

  • il est impératif d'utiliser l'attribut persistent de v-dialog afin que le seul moyen de fermer le dialogue soit de cliquer sur le bouton, et donc d'appeler popError().
  • il est possible d'étendre ce principe, par exemple en permettant de mettre en file plusieurs messages d'erreurs, d'associer au message d'erreur une route que l'on suit après avoir cliqué sur le bouton de fermeture du dialogue, etc.
Démonstration :
  • Renommer App.vue.2 dans App.vue.
  • lancer npm run serve puis visualiser le résultat dans le navigateur (par ex. http://localhost:8080)
  • Montrer le code du store dans store/errors.js (NB: le store est modulaire). Il correspond au principe décrit ci-dessus.
  • Montrer le code de ErrorDialog.vue. On remarque que l'affichage/masquage du dialogue est effectivement piloté par la variable isError du store.
  • Montrer le code de App.vue. On remarque qu'il y a une seule et unique instance de ErrorDialog.
  • Cliquer sur le bouton 'check firstname', ce qui permet de suivre une route pour afficher une premier composant TD3Demo2A, qui mappe la mutation pushError()
  • Taper toto dans le champ de saisie : le composant utilise pushError() pour "enregistrer" une erreur dans le store. Cela provoque immédiatement l'apparition du dialogue d'erreur.
  • Si on tape autre chose, on suite une autre route pour afficher TD3Demo2B, qui mappe aussi pushError().
  • Cliquer sur le bouton sans cocher la case : le composant utilise pushError() et le dialogue d'erreur apparaît de nouveau.

 

3°/ Eviter l'accès à des composants avec un accès privilégié et à des routes non prévues

  • Si on utilise vue-router pour "naviguer" dans l'application, il y a certainement ces cas où suivre une route va provoquer l'affichage de composants dont le contenu ne peut être récupéré/visible que si on a les droits suffisants. Si les droits sont insuffisants, il faut que l'application signale une erreur puis redirige vers une route sans problème d'accès.
  • En fait, ce fonctionnement peut être optimisé si on interdit directement le fait de suivre une route menant vers de tels composants. Comme ça, on économise leur affichage et le fait qu'ils échouent lors de la récupération de contenu.
  • Pour cela, il faut utiliser les "gardes de navigation" de vue-router. Une garde est simplement une fonction dont le paramètre est une fonction définie par le développeur. Cette dernière sera appelée quand on essaye de suivre une route, voire après l'avoir suivie.
  • Dans le cas présent, on va utiliser la fonction beforeEach() qui permet de définir la fonction appelée AVANT de suivre n'importe quelle route.
  • Cette fonction peut décider de suivre la route ou non, voire de rediriger vers une autre route.
  • Pour autoriser/interdire de suivre des routes privilégiées grâce à des gardes, cela nécessite un peu de code JS dans le fichier décrivant les routes et le store.
  • Dans le store :
    • on ajoute les mutations/actions permettant de s'authentifier et une variable indiquant l'état authentifié ou non, par exemple auth.
  • Dans le fichier des routes :
    • on utilise le champ meta de chaque route (cf. API de vue-router : https://router.vuejs.org/guide/advanced/meta.html), pour définir une valeur, par exemple levelAuth, décrivant le niveau d'accès nécessaire qu'il faut pour suivre la route (par ex, 0 = libre, 1 = privilégié). Comme meta est un champ relié à la route, les fonctions qui prennent en paramètre une route y accéder.
    • on crée une fonction, par exemple checkAccess(), prenant en paramètre une route. Elle utilise le contenu du champ meta ET le store pour décider si l'accès à la route est autorisé ou non. Elle renvoie true ou false en fonction.
    • on crée une garde de navigation qui appelle checkAccess() avec la route à suivre en paramètre. Si elle renvoie true, on continue avec cette route, sinon, on redirige vers la route permettant l'authentification.

 

  • Pour interdire l'accès à des routes non prévues, le principe consiste à créer une toute dernière route, qui accepte n'importe quel chemin (grâce à *). On lui donne un nom avec name, afin de l'identifier facilement.
  • Ensuite, dans la fonction de garde, on teste si la route à suivre correspond à ce nom. Si c'est le cas, c'est qu'aucun autre route ne correspond, donc qu'il y a erreur de routage. Dans ce cas, on affiche une erreur et on redirige vers l'accueil.

 

Remarques :

  • ce principe peut être étendu à une granularité plus fine des droits d'accès. Il suffit de modifier la valeur de levelAuth et la fonction de vérification pour tenir compte de la diversité des droits.
  • on peut mettre plusieurs gardes, qui vont être appelée dans l'ordre dans lequel on les a définies.
  • il existe d'autres type de gardes, par exemple celles que l'on définit pour une route en particulier, voire même à l'intérieur des composants. Pour le principe à mettre en place ici, il est cependant plus court d'utiliser une garde globale.

 

Démonstration :
  • Renommer App.vue.3 dans App.vue.
  • lancer npm run serve puis visualiser le résultat dans le navigateur (par ex. http://localhost:8080)
  • Montrer le code de auth.js dans le store. Le login est assuré par l'action login() et non pas une mutation, pour prendre en compte le fait que généralement, l'authentification est faite auprès d'un serveur, donc avec un appel asynchrone. 
  • Montrer le code de index.js dans le router. Selon le principe donné ci-dessus, un champ access a été ajouté aux routes et une fonction checkAccess() permet de tester s'il est possible d'emprunter la route. Pour cela, il suffit de tester la valeur de auth dans le store. Comme celui-ci est modulaire, il faut utiliser l'objet store.state.auth pour accéder au state du module auth.  Cet objet permet ensuite d'accéder à l'unique variable auth.
  • Dans cette démo, il existe une route publique permettant d'afficher le composant FreeView, et une route privilégiée affichant PrivateView.
  • La dernière route s'appelle error404 et son chemin est *. Elle sera suivie dès lors qu'aucune autre ne fonctionne.
  • Pour écrire la garde de navigation, beforeEach() prend simplement en paramètre une fonction fléchée avec 3 paramètres : la route actuelle, la route à suivre, et une fonction permettant de continuer sur la nouvelle route, ou bien faire une redirection.
  • On remarque que le code de cette fonction est simple :
    • si la route suivie est error404, on redirige vers l'accueil,
    • sinon s'il est possible de suivre la route, on la suit,
    • sinon, on signale une erreur (cf. démo 2) et on redirige vers le composant de login.
  • Montrer le code de TD3Demo3.vuejs . Il contient 2 boutons permettant de suivre soit la route publique, soit la privilégiée et d'afficher le composant associé en tant que fils de TD3Demo3.
  • Cliquer sur le bouton "Follow Free Access route" : on affiche directement le composant FreeView.
  • Cliquer sur le bouton "Follow Private Access route" : on obtient une erreur.
  • Cliquer sur le bouton "Login", puis taper n'importe quoi comme login/mdp. On obtient une erreur. Si on tape toto/azer, le login est réussi.
  • Cliquer de nouveau sur le bouton "Follow Private Access route" : on affiche le composant PrivateView.
  • Si on se délogue en cliquant sur "Logout", on ne peut plus accéder à PrivateView.
  • Taper une URL avec une route invalide, par exemple localhost:8080/toto. On revient à l'accueil avec un message d'erreur. La route par défaut fonctionne donc bien comme prévu.