L'archive des sources (répertoire src) pour les démonstrations est téléchargeable [ ici ].

 1°/ Objectifs de Vuetify

  • Les cours précédents se sont focalisés sur l'aspect fonctionnel d'une application SPA avec vuejs.
  • Il est temps d'aborder le côté visuel.
  • Vuetify est simplement un des plugins les plus aboutis pour créer l'interface graphique d'une application.
  • Il propose des composants plus ou moins complexes (en terme de fonctionnalités) pour construire une application avec un design orienté Material.
  • Pour autant, vuetify a 2 problèmes essentiels :
    • sa richesse : on a parfois du mal à voir à quoi peut servir tel ou tel composant. C'est là que l'aide d'un graphiste est la bienvenue :-)
    • sa documentation exhaustive mais assez peu explicative (cf. le site de vuetify  : https://vuetifyjs.com/)
      • Le descriptif des composants consiste uniquement en des exemples de codes, souvent longs et dont il faut comprendre la mécanique soi même.
      • L'API des composants liste leurs différentes propriétés mais sans faire le lien avec les exemples de code, et parfois sans aucune explications sur comment les utiliser, voire à quoi elles servent.
  • A ces 2 problèmes s'ajoute le côté verbeux du code (cf. les exemples du site) qui rend rapidement les templates des composants peu lisibles.
  • Fort heureusement, ces problèmes disparaissent assez rapidement à partir du moment où l'on a compris les mécanismes de bases d'utilisation de vuetify, car ils sont relativement cohérents pour tous les composants.
  • Ce TD est l'occasion de découvrir ces mécanismes généraux et de les illustrer avec des exemples pratiques "courts".

2°/ Utilisation de Vuetify dans un projet vuejs

2.1°/ Création d'un projet vide

  • Même s'il est possible d'intégrer vuetify dans un projet existant, le plus simple consiste à créer un projet vide intégrant déjà vuetify. Pour cela :  npm create vuetify@latest
  • Comme pour les projet purement vuejs, un menu permet de configurer le type de projet.
  • Malheureusement, les options de configuration ne permettent pas de régler finement ce que l'on veut inclure. Il y a notamment une option qui inclut pinia et vue-router, mais également plein d'autres plugins par forcément très utiles, voire parasites.
  • C'est pourquoi le plus "sain" est de partir d'un projet avec juste vue et vutify (option Barebones)
  • Ensuite, le menu propose d'utiliser typescript ou pas, de choisir le gestionnaire de paquet node (npm, yarn ,...) et enfin d'installer tous les paquets ou non.

2.2°/ Ajout de plugins (pinia, vue-router, ...)

  • Après la création, on obtient dans src une arborescence classique, avec un élément supplémentaire : un répertoire plugins.
  • Dans ce répertoire, le fichier index.js contient :
import vuetify from './vuetify'

export function registerPlugins (app) {
  app.use(vuetify)
}

  • La fonction registerPlugins() est appelée par main.js, lors du lancement de l'application.
  • Il suffit donc d'ajouter des instructions dans cette fonction pour ajouter des plugins, à conditions qu'ils soient installés au préalable.
  • Par exemple, pour utiliser vue-router et pinia dans un projet vuetify, on doit :
  • installer vue-router : npm install vue-router
  • installer pinia : npm install pinia
  • créer dans src un répertoire stores et un répertoire router.
  • dans router, créer un fichier basique de routage index.js, tel qui suivant :
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  // définir les routes
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: routes,
})

export default router
  • Enfin, modifier plugins/index.js comme suivant :
import vuetify from './vuetify'
import { createPinia } from 'pinia'
import router from '@/router/index.js'

export function registerPlugins (app) {
  app.use(vuetify)
  app.use(createPinia())
  app.use(router)
}

 

3°/ Principes fondamentaux de Vuetify

3.1°/ les composants vuetify.

  • Vuetify propose essentiellement des composants graphiques, mais aussi quelques uns purement fonctionnels (par ex, un chargeur de composant dynamique)
  • Cela va de la simple icône à des tables triables/paginables, en passant par les classiques : boutons, champ de saisie, cases à cocher, ...
  • Comme tout composant vuejs, il suffit d'écrire une balise portant le nom du composant pour l'utiliser dans un template.

Exemple, avec un bouton vuetify :

<template>
  <div>
    <v-btn>Clic</v-btn>
  </div>
</template>

 

  • Pour modifier le style d'un bouton, il est possible d'utiliser les attributs "classiques" style et class.
  • Cependant, leur donner une valeur définie par le développeur a toute les chances de "casser" le look-and-feel de vuetify.
  • C'est pourquoi il vaut mieux utiliser des valeurs prédéfinies par vuetify, notamment celles qui permettent de modifier les paddings et margins, à la "façon bootstrap"
  • De plus, v-btn fournit beaucoup de props qui vont modifier le visuel, comme par exemple la couleur, le remplissage, l'élévation ,...

Exemple, bouton rouge, marges de 40 pixels et padding de 8 pixels :

<template>
  <div>
    <v-btn
      class="ma-10 pa-2"
      color="red"
    >
      Clic
    </v-btn>
  </div>
</template>

 

3.2°/ La grille de positionnement

  • Généralement, on ne se contente pas d'écrire des balises dans un template en comptant sur le navigateur pour les organiser à l'écran.
  • On essaie de fixer leur position et taille plus ou moins précisément.
  • Vuetify propose un système de positionnement et de redimensionnement automatique des composants, basé sur le système flexbox et donc très proche de celui de bootstrap.
  • Il est responsive et permet d'adapter l'IG à différents supports (mobile, tablette, ordi, ...)
  • Le principe de base est de mettre les composants que l'on veut afficher dans un conteneur, représenté par la balise <v-container>
  • Ensuite, on définit des lignes de composants, grâce à <v-row>. Les lignes auront la largeur maximale du conteneur, qui va varier selon la taille de la fenêtre et des autres conteneurs. Par défaut, la hauteur de ces lignes dépend des composants qui vont s'y trouver.
  • Pour chaque ligne, on utilise <v-col> pour créer une colonne dans laquelle est affiché un composant.
  • Par défaut, chaque colonne aura une taille équivalente, mais il est possible de spécifier une sorte de taille grâce à la props cols, avec une valeur allant de 1 à 12.
  • Sur une même ligne, la somme de ces tailles ne doit pas dépasser 12 si on ne veut pas avoir des éléments qui "vont à la ligne".
  • Qui plus est, il est possible de définir différentes tailles selon la taille du support, grâce aux props sm, md, lg et xl.

Exemple, 1 seule ligne et 2 colonnes, par défaut de rapport de taille 1/5, et de rapport 1/2 pour les supports md et au-delà :

<v-container>
  <v-row>
    <v-col cols="2" md="4">
      <v-btn block> Bouton 1 </v-btn>
    </v-col>
    <v-col cols="10" md="8">
      <v-btn block> Bouton 2 </v-btn>
    </v-col>
  </v-row>
<v-container>

Démonstration n°1 :

  • npm run dev, puis clic sur "Démo 1" pour lancer la démo.
  • faire varier la taille du naviguateur : à 960px de large, on change de rapport de taille.
  • modifier le code et mettre md="10" pour la 2ème colonne : la somme dépasse 12 et cette colonne est "mise à la ligne"
  • modifier le code et mettre md="6" pour la 2ème colonne : la somme est inférieure à 12 donc pas de problème, excepté que le bouton n'atteint pas la droite du conteneur.

 

4°/ exemples

4.1°/ customiser un bouton

  • D'après l'API vuetify, la balise <v-btn> possède pas moins de 41 props, 4 slots et un signal.
  • Parmi les props on peut noter les suivantes :
    • color : modifie la couleur de fond (ou l'entourage),
    • rounded : bord gauche/droit arrondis,
    • outlined : bouton sans fond mais entouré,
    • block : prend toute la largeur disponible,
    • to : la route à suivre (définie grâce à vue-router) quand on clique sur le bouton,
    • loading : permet de lancer une animation du type chargement quand on clique sur le bouton.
  • Le signal est click, qui est généré quand on clique sur le bouton.

Démonstration n°2 :

  • npm run dev, puis clic sur "Démo 2" pour lancer la démo.
  • cliquer sur le bouton 2 : l'animation par défaut de chargement se lance (NB : pendant 3s grâce à la fonction doLoad() )
  • pour le bouton 1, changer pa-10 en pa-2 : le padding change.
  • pour le bouton 1, supprimer outlined : le style change

Remarques :

  • Les paddings et marges sont bien appliqués grâce aux noms spéciaux de classes CSS.
  • Par exemple, le padding pt-5 du bouton 2 s'applique uniquement au haut du bouton (d'où le t après le p).
  • Le style du bouton change également en fonction des attributs "spéciaux" que l'on ajoute/enlève : rounded, block, ...
  • Le contenu du bouton est bien fixé par slot par défaut (i.e. ce que l'on met entre l'ouverture et la fermeture de <v-btn>).
  • On capture l'événement click comme avec n'importe quel composant vuejs : v-on:click, ou bien @click.

 

4.2°/ Dialog & Cards

  • Cet exemple permet d'illustrer comment certains composants vuetify utilisent v-model pour paramétrer leur comportement.
  • En l'occurrence, on va utiliser <v-dialog> qui permet d'ouvrir une boîte de dialogue quand on clique sur un élément, dénommé activateur dans la suite.
  • Cet élément peut être très divers : bouton, texte, icone, ... Pour autant, quel que soit son type, il faut qu'un clic dessus provoque l'affichage du dialogue.
  • La solution consiste à relier le dialogue à une variable booléenne grâce à v-model et à mettre cette variable à true lorsque l'on clique sur l'activateur.
  • Pour masquer la dialogue, il suffit de remettre cette valeur à false, ce qui se fait généralement grâce à un "désactivateur" se trouvant dans le dialogue, comme par exemple un bouton.
  • Au final, si on veut activer un dialogue à partir d'un bouton, on écrit :
<template>
  <v-dialog v-model="show">
    ...
    <v-btn @click="show=false"> Close </v-btn>
    ...
  </v-dialog>
  <v-btn @click="show=true"> Open <v-btn>
</template>
<script setup>
...
const show = ref(false)
</script>
  ... définition de la fenêtre de dialogue
</v-dialog>
  •  Pour définir la fenêtre de dialogue, il existe plusieurs solutions, mais la plus courante est d'utiliser une <v-card> et ses 3 "sous-balises" <v-card-title>, <v-card-text> et <v-card-actions>.
  • On crée ainsi un panneau dans lequel on met d'autres composants représentant le titre, le texte et les actions du dialogue.

Démonstration n°3 :

  • npm run serve, puis clic sur "Démo 3" pour lancer la démo.
  • cliquer sur le bouton : la fenêtre de dialogue s'ouvre.
  • cliquer en dehors du dialogue : il reste ouvert. C'est l'attribut persistent qui permet cela.
  • cliquer sur oui ou non pour fermer le dialogue.

 

4.3°/ Table de données

  • Une des balises fournissant les fonctionnalités les plus complexes est <v-data-table>
  • Elle permet d'afficher des données sous forme de tableau, mais avec des possibilités natives de tris, filtrage, recherche, pagination, regroupement, ...
  • Le modèle de données qui sert à alimenter le tableau est très simple : on définit 2 tableaux, contentant des objets représentant les colonnes et les lignes du tableau.
    • Pour les colonnes, les objets doivent avoir au minimum une propriété value représentant le nom "abstrait" de la colonne, et une propriété text représentant le nom de la colonne affiché à l'écran.
    • Pour les lignes, les objets doivent avoir au minimum des propriétés correspondant au nom "abstrait" de chaque colonne. On peut cependant avoir d'autres propriétés, qui ne seront pas utilisées pour l'affichage.
  • Les colonnes peuvent également avoir des propriétés qui influent sur les fonctionnalités associées à <v-data-table>. Par exemple :
    • groupable : si false, le tableau ne peut pas être affiché en groupant les éléments de la colonne par valeur identique.
    • sortable : si false, la colonne ne peut pas être triée.
    • filterable: si false, le mécanisme de recherche ne prend pas en compte la colonne
  • Ces 2 tableaux doivent être accessibles dans le composant qui utilise <v-data-table>. On peut donc les mettre dans sa propriété data, ou bien dans un store si les données sont utilisées par d'autres composants.
  • Il suffit ensuite de relier les attributs headers et items à ces 2 tableaux dans <v-data-table>

Exemple basique, avec un tableau de nom/prénom, seule la colonne nom peut être triée :

<template>
  <v-data-table
    :headers="titres"
    :items="infos"
    ...
  >
</template>
<script setup>
...
const  titres = ref([
      {text: 'Nom', value: 'lastName'},
      {text: 'Prenom', value: 'firstName'}
    ])
const infos = ref( [
      {id:0, lastName:'Dupond', firstName:'jean'},
      {id:1, lastName:'Durand', firstName:'alcyonne', sortable: false},
    ])
</script>
      

 

  • Par défaut, le résultat d'affichage contient une ligne avec les titres de colonne, 5 lignes de données, le bandeau de pagination.
  • Pour pouvoir profiter du mécanisme de recherche, il faut donc créer un champ de saisie à part, par exemple grâce à <v-text-field> et relier son contenu à une variable du notre composant.
  • Cette même variable peut être reliée à l'attribut search de <v-data-table>.
  • Dès que la variable est modifiée, le mécanisme de recherche est exécuté et seuls les lignes correspondantes sont affichées.
  • Une correspondance existe pour une ligne à partir du moment où l'un des champs contient la suite de caractères donnée dans la variable.

Exemple :

<template>
  <v-text-field
    v-model="mysearch"
    label="chercher"
  ></v-text-field>
  <v-data-table
    :headers="titres"
    :items="infos"
    :search="mysearch"
  ></v-data-table>
</template>
<script setup>
...
const mysearch = ref("")
</script>
  • Il existe bien d'autres fonctionnalités et personnalisations possibles, décrites sur le site de vuetify. Par exemple, on peut rendre cliquable certains items afin de modifier leur valeur, via un dialogue. On peut personnaliser la fonction de recherche, etc.
  • La démonstration suivante présente celles abordées ci-dessus, plus la sélection de lignes.

Démonstration n°4 :

  • npm run dev, puis clic sur "Démo 4" pour lancer la démo.
  • cliquer sur les boutons de pagination pour parcourir les pages.
  • cliquer sur "Tous" dans "lignes par pages" et réduire la hauteur de la fenêtre : le contenu du tableau n'est pas scrollable, c'est la fenêtre toute entière qui l'est.
  • cliquer une première fois sur la flèche qui apparaît à côté de Name : les données sont triées par ordre croissant sur Name.
  • cliquer une deuxième fois : les données sont triées par ordre décroissant.
  • cliquer une troisième fois : les données sont dans l'ordre de définition du code.
  • cliquer sur "group" dans la colonne "Course" : les lignes sont regroupées en prenant les valeurs identiques de matières, puis ces groupes sont classés par ordre alphabétique. cliquer sur une des croix pour enlever le regroupement.
  • taper 13 dans le champ de recherche : aucune correspondance trouvée. Normal car la colonne des notes n'est pas incluse dans la recherche.
  • taper nr dans le champ de recherche : seules les lignes contenant Henri sont affichées.
  • taper dd dans le champ de recherche : seules les lignes contenant BdD sont affichées.
  • cliquer sur quelques cases à cocher : la moyenne en bas change dynamiquement. En effet, on calcule cette moyenne dès que la variable selected change.
  • changer la ligne item-key="id" en item-key="name", puis sélectionner une case avec "Jean" : toutes les cases Jean sont sélectionnées en même temps. On peut donc influer sur le principe de sélection grâce à item-key.

Remarques :

  • La dernière manipulation implique que si l'on veut absolument pouvoir sélectionner des lignes indépendamment les unes des autres, il faut que item-key référence une propriété dont la valeur est unique pour chaque ligne. Il faut donc prévoir cela dans le modèle de données, notamment quand les données proviennent d'une BdD et qu'elles ne sont pas référencées par un id unique (NB : si la BdD est correctement conçue, cela ne devrait pas être la cas ! ).

 

4.4°/ Groupes d'éléments

  • Il est relativement fréquent que des éléments aient une relation, par exemple le fait qu'un seul soit actif à la fois, ou bien qu'ils soient affichés au même endroit.
  • Pour ces cas, vuetify propose différentes balises, selon le type d'élément à regrouper. Malheureusement, le nommage de ces balises n'est pas totalement cohérent :
    • pour les boutons : <v-btn-toggle>
    • pour les chips : <v-chip-group>
    • pour les items ou listes d'items : <v-item-group> et <v-item>
    • pour un emplacement unique d'affichage multi-contenu : <v-window>
    • pour des emplacements d'affichage multiples avec pagination : <v-slide-group> 
  • La façon de modifier le look des éléments en fonction de s'ils sont sélectionnés ou non dépend également de la balise utilisée. Le descriptif est donné ci-dessous.
  • En revanche, ils ont une base commune de fonctionnement : on peut relier une variable grâce à v-model pour savoir quels éléments sont sélectionnés.

Pour les groupes de boutons :

  • on utilise <v-btn-toggle> avec à l'intérieur des balises <v-btn>,
  • pour autoriser la sélection multiple, il suffit d'utiliser la props multiple de <v-btn-toggle>
  • si on veut modifier le look de tous les boutons sélectionnés, on utilise la props color de <v-btn-toggle>
  • cela laisse la possibilité de spécifier une autre couleur pour un bouton en particulier en utilisant sa props selected-class.

Pour les groupes de chips :

  • comportement et possibilités similaires aux boutons, mais avec des balises <v-chip> à l'intérieur de <v-chip-group>

Pour les groupes d'items :

  • comportement et possibilités similaires aux boutons, mais avec des balises <v-item> à l'intérieur de <v-item-group>,
  • de plus, il faut utiliser le scoped-slot par défaut de <v-item> qui permet de récupérer 3 propriétés : isSelected, selectedClass et toggle. isSelected permet de savoir si l'item est actif et donne la possibilité de modifier le contenu en fonction. tselectedClass permet de spécifier la classe CSS des items sélectionnés (cf. ci-dessous). toggle est une fonction qui doit être appelée lorsque l'on clique sur l'item, ce qui permet de mettre en place le mécanisme de sélection.
  • si on veut modifier le look de tous les items sélectionnés, on utilise la props selected-class de <v-item-group> 

Pour les listes d'items :

  • Une liste d'éléments se crée avec <v-list>. Il suffit de mettre à l'intérieur des <v-list-item>. De base, <v-list> permet d'obtenir une suite d'élément listés verticalement, et le fait de grouper ajoute les mécanismes de sélection.
  • Un <v-list-item> peut être défini grâce à des sous-balises permettant entre autre de définir un icon <v-list-item-icon>, un contenu <v-list-item-content>, ...
  • contrairement à <v-item>, on n'est pas obligé d'utiliser le scoped-slot par défaut. Cependant, si on veut changer le contenu d'un item en fonction de sa sélection, il faut alors utiliser le scoped-slot et récupérer la propriété isSelected

Pour un emplacement multi-contenu :

  • on utilise <v-window> avec à l'intérieur des balises <v-window-item> qui définissent chaque contenu,
  • le fait de modifier la variable reliée avec v-model va faire passer d'un contenu à l'autre.

Pour un multi-contenu avec pagination :

  • on utilise <v-slide-group> avec à l'intérieur des balises <v-slide-item> qui définissent chaque contenu,
  • par défaut, les items sont affichés sur une ligne, qui peut être tronquée si la fenêtre n'est pas assez grande.
  • pour faire apparaître les chevrons de pagination de chaque côté, il faut utiliser la props show-arrows. NB: on peut customiser le look des chevrons.
  • comme pour les groupes d'items, on doit utiliser le scoped-slot par défaut pour récupérer les propriétés isSelected et toggle.

 

Démonstration n°5 :

  • npm run dev, puis cliquer "Demo 5" pour lancer la démo.
  • explorer le code de TD8demo5View.vue avec les différents type de groupes.
  • on notera que le scoped-slot par défaut est utilisé avec la notation raccourcie de vuejs, donc sans utiliser la balise template, comme fait dans les groupes d'items. Ce raccourci n'est utilisable que lorsque qu'il n'existe qu'un seul slot, celui par défaut.

  

4.5°/ Barre de navigation, Suivre des routes & divers bonus

  • Dans la plupart des applications, le haut de la page est constitué par une barre comportant par exemple un titre, une image, des boutons et/ou onglets, ...
  • Dans vuetify, on utilise <v-app-bar>, ses "sous-balises" ( <v-app-bar-title>, <v-app-bar-nav-icon>), ses props et slots pour créer cette barre de façon hautement personnalisable.
  • Parmi les props "remarquables", on note :
    • app : permet à la barre de modifier son apparence (notamment la taille), en fonction du contenu principal donné dans <v-main>, de la taille du navigateur, ...
    • src : pour mettre une image de fond,
    • hide-on-scroll : la barre est masquée dès que l'on scroll vers le bas,
    • height : pour fixer sa hauteur.
  • Généralement, cette barre contient des boutons/onglets servant à naviguer dans l'application.
  • Si on utilise <v-app-bar-nav-icon>, on profite du click sur l'icône pour afficher un dialogue, ou un panneau glissant, grâce à <v-navigation-drawer>.

 

  • Pour peupler le panneau glissant, on utilise généralement une liste d'item. Selon les besoins applicatifs, les items permettent de suivre des routes, d'être sélectionnés, de faire apparaître diverses infos, ...
  • Ce type d'élément est montré plus en détail dans la section suivante.

 

  • Quand les widgets existants sont insuffisants pour créer l'application, il faut parfois directement dessiner.
  • Pour cela, on utilise la balise html <canvas>
  • Pour dessiner sur un canvas dans un composant vuejs, le principe est relativement simple :
    • on définit dans la partie template un canvas avec une valeur d'attribut ref pour "nommer" le canvas.
    • dans la partie script, on importe puis appelle la fonction useTemplateRefs() avec comme paramètre le nom du canvas. Cela crée une variable permettant d'accéder à l'objet canvas.
    • dans la méthode onMounted() de la partie script, on crée un "contexte 2D" de dessin à partie de l'objet canvas, que l'on affecte à une variable locale.
    • on utilise cette variable et les fonction de dessin pour remplir le canvas, par exemple dans les méthodes du composant.

 

Démonstration n°6 :

  • npm run dev, puis cliquer dans le menu glissant gauche sur "Compo A" pour lancer la démo.
  • dans ComponentA, on peut cliquer sur les boutons + et - pour augmenter/réduire la largeur de la zone rouge.
  • scroller vers le bas : la barre du haut est immédiatement masquée.
  • si on affiche ComponentB puis de nouveau ComponentA, la zone rouge fait toujours la même taille : normal car sa taille est stockée dans un store Pinia.

Remarques :

  • Il n'y a pas besoin de fermer explicitement le panneau glissant. En revanche, on doit l'ouvrir en mettant la valeur true dans son attribut value. Pour cela, on fait comme avec les dialogue, on utilise une liaison bidirectionnelle avec une variable que l'on peut mettre à true, en l'occurrence quand on clique sur l'icône du panneau.
  • Dans le fichier views/ComponentA.vue, on voit comment on peut "dessiner" dans un composant, grâce à la balise html <canvas>.
  • ATTENTION : tous les navigateurs ne supportent pas cette balise et/ou toutes les fonctions de dessin possible. Mais avec les derniers navigateurs, on peut carrément récupérer un contexte de dessin 3D et faire de l'openGL.