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 vide incluant vuetify, vue-router et pinia, puis remplacer son répertoire src.

 

1°/ Un dialogue avec résultat

  • Il est relativement fréquent de créer un composant qui va afficher plusieurs boîtes de dialogue en fonction des interactions de l'utilisateur. La plupart du temps, ces dialogues se différencient seulement par leur contenu (titre & texte), mais ont les mêmes boutons.
  • La solution la plus simple consiste a créer autant de dialogues que d'interactions les affichant. Cependant, cela augmente de façon notable la taille du code du composant, et implique pas mal de copier/coller.
  • Pour éviter cela, il est préférable de créer un seul composant de dialogue dont on peut paramétrer le titre et le texte via des props, et que l'on va instancier dans les composants nécessitant un dialogue.
  • Si l'on a besoin de savoir quel bouton a été cliqué, il suffit que ce dialogue envoie lors de sa fermeture un événement au composant parent, avec comme valeur le n° (ou type) du bouton cliqué.
  • Malheureusement, si le composant parent doit faire des traitements différents en fonction du dialogue et du bouton cliqué, il faut de nouveau ajouter pas mal de code.

 

  • 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 dialogues avec cette structure mais avec un titre et texte différents. Qui plus est, le traitement à faire après la fermeture du dialogue dépend du bouton cliqué et de l'interaction d'origine. Cela implique 4 possibilités de traitement, donc potentiellement 4 fonctions à écrire (NB: on peut éventuellement regrouper les traitements par type d'interaction)
  • Pour mettre en place un tel système avec un seul dialogue paramétrable, il faut :
    • une variable qui indique si le dialogue est visible ou non, 
    • une variable qui stocke quelle "version" du dialogue est actuellement affichée,
    • une fonction par type d'interaction, qui met à jour les props du dialogue pour spécifier son titre et texte.
    • une fonction appelée lorsque le dialogue renvoie un événement (donc est fermé), qui va appeler une des 4 fonctions susmentionnées.
  • C'est du code à écrire, certes pas compliqué, mais qui va à 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ésout la promesse avec comme résultat true, alors que cancel() résout 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". Pour y accéder dans la partie script, on utilise la fonction vuejs useTemplateRef() qui permet de récupérer une variable objet représentant le dialogue. Avec l'exemple, cela donnerait : const mydial = useTemplateRef('mydilaog')
    • quand l'utilisateur fait une action qui doit montrer le dialogue, on appelle une fonction de la partie script. 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 mydial.value.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 :
  • lancer npm run dev puis cliquer sur le bouton "Démonstration 1"
  • 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 TD2Demo1View.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 TD2Demo1View.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 actions :
      • 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 importe le store et peut donc utiliser 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 (ou plugins, par ex vue-router) générant une erreur, on importe le store et on appelle pushError() dès qu'il y a une erreur.
  • 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 :
  • lancer npm run dev puis cliquer sur le bouton "Démonstration 2". Cela permet de suivre une route de niveau 2 et d'afficher un composant TD2Demo2AView.
  • Montrer le code du store dans stores/errors.js . 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.
  • Taper toto dans le champ de saisie de TD2Demo2A : 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 suit une autre route pour afficher TD2Demo2BView, qui utilise 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 un fichier store avec dedans une variable indiquant l'état authentifié ou non, par exemple auth, et des fonctions permettant de faire l'authentification (soit locale, soit distante)
  • 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. 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 :
  • lancer npm run dev puis cliquer sur le bouton "Démonstration 3".
  • Montrer le code de auth.js dans le store. Le login est assuré par l'action asynchrone login(), 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 meta-champ levelAuth a été ajouté à certaines 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 TD2Demo3View.vue . Il contient 2 boutons permettant de suivre soit la route publique, soit la privilégiée et d'afficher le composant associé.
  • 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, avec un dialogue et en arrière-plan, on est redirigé vers un composant de login.
  • 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.