frontend / vuejs
Directives personnalisées
Explication
Ce que vous allez apprendre
- Créer une directive personnalisée simple qui manipule directement un élément du DOM
- Nettoyer systématiquement une directive qui ajoute des écouteurs d'événements
- Utiliser les arguments et modificateurs d'une directive personnalisée
- Enregistrer une directive localement dans un composant ou globalement pour toute l'application
- Reconnaître les cas d'usage typiques où une directive personnalisée est le bon outil
Dans quel contexte ?
Un menu déroulant doit se fermer automatiquement dès que l'utilisateur clique n'importe où en dehors de lui. Cette logique de "clic à l'extérieur" n'a rien à voir avec l'état réactif du composant lui-même : elle manipule directement des écouteurs d'événements globaux sur le document. C'est exactement le genre de comportement pour lequel une directive personnalisée est plus adaptée qu'un simple v-on.
D'abord, une directive personnalisée expose les mêmes moments qu'un composant
Une directive comme vFocus définit un hook mounted(el) qui reçoit directement l'élément DOM réel sur lequel elle est appliquée. C'est la différence fondamentale avec un composant : une directive n'a pas son propre template ni son propre état, elle agit directement sur un élément DOM existant.
Prérequis
Il faut avoir compris les hooks de cycle de vie (leçon 7) : une directive personnalisée expose des hooks très similaires (mounted, updated, unmounted).
Ensuite, une directive avec état et nettoyage complet
vClickOutside illustre un cas plus complexe : elle ajoute un écouteur d'événement global au montage (document.addEventListener) et doit impérativement le retirer au démontage (unmounted). Sans ce nettoyage symétrique, exactement comme pour les hooks de cycle de vie vus en leçon 7, chaque instance de cette directive laisserait une fuite mémoire.
Piège dangereux
Stocker le handler d'événement directement sur l'élément DOM (el._clickOutsideHandler = ...) est nécessaire pour pouvoir le retirer précisément plus tard avec removeEventListener — sans cette référence conservée, il serait impossible de cibler exactement quel écouteur retirer lors du nettoyage.
Il reste à comprendre comment personnaliser le comportement d'une directive
Une directive peut recevoir un argument (v-tooltip:top, où top devient binding.arg) et des modificateurs (v-tooltip.instant, où binding.modifiers.instant vaut true). Ces informations permettent à une seule directive de couvrir plusieurs variantes de comportement sans en écrire plusieurs versions.
Élément de binding | Exemple | Contenu |
|---|---|---|
binding.value | v-tooltip="'texte'" | La valeur passée à la directive |
binding.arg | v-tooltip:top | 'top' |
binding.modifiers | v-tooltip.instant | { instant: true } |
Maintenant, choisir entre enregistrement local et global
Importer une directive directement dans un <script setup> (comme vFocus) la rend utilisable UNIQUEMENT dans ce composant. app.directive('focus', vFocus) dans main.js la rend disponible PARTOUT dans l'application, sans import répété — pertinent pour des directives vraiment transverses comme le focus automatique ou le clic extérieur.
Astuce
Les cas d'usage typiques d'une directive personnalisée restent limités et bien identifiés : focus automatique, détection de clic extérieur, tooltips simples, lazy-loading d'image, ou intégration d'une bibliothèque DOM tierce qui ne s'intègre pas naturellement avec la réactivité de Vue. Pour presque tout le reste, un composant classique reste préférable.
Maintenant que tu sais étendre le comportement du DOM lui-même, la prochaine leçon change complètement de sujet : Vue Router, pour transformer une application en une véritable SPA multi-pages.
Commandes & code
Directives personnalisées
Pour manipuler directement le DOM de façon réutilisable, au-delà de ce que les directives natives couvrent.
// directives/vFocus.js — directive locale simple
export const vFocus = {
mounted(el) {
el.focus()
},
}<script setup>
import { vFocus } from './directives/vFocus'
</script>
<template>
<!-- une directive commençant par v- importée est utilisable directement dans le template -->
<input v-focus placeholder="reçoit le focus au montage" />
</template>// directives/vClickOutside.js — directive avec état et nettoyage, hooks complets
export const vClickOutside = {
mounted(el, binding) {
el._clickOutsideHandler = (event) => {
if (!el.contains(event.target)) {
binding.value(event) // binding.value = la fonction passée par l'utilisateur de la directive
}
}
document.addEventListener('click', el._clickOutsideHandler, true)
},
unmounted(el) {
// nettoyage indispensable pour éviter une fuite mémoire
document.removeEventListener('click', el._clickOutsideHandler, true)
delete el._clickOutsideHandler
},
}<script setup>
import { ref } from 'vue'
import { vClickOutside } from './directives/vClickOutside'
const menuOuvert = ref(false)
</script>
<template>
<div v-click-outside="() => (menuOuvert = false)" class="menu">
<button @click="menuOuvert = !menuOuvert">Menu</button>
<ul v-if="menuOuvert">
<li>Profil</li>
<li>Paramètres</li>
<li>Déconnexion</li>
</ul>
</div>
</template>// directives/vTooltip.js — directive avec arguments et modificateurs : v-tooltip:top.instant="'texte'"
export const vTooltip = {
mounted(el, binding) {
const position = binding.arg || 'bottom' // v-tooltip:top -> arg = 'top'
const delai = binding.modifiers.instant ? 0 : 300 // v-tooltip.instant
el.dataset.tooltip = binding.value
el.dataset.tooltipPos = position
el.dataset.tooltipDelay = String(delai)
},
updated(el, binding) {
// se redéclenche si la valeur liée change
if (binding.value !== binding.oldValue) {
el.dataset.tooltip = binding.value
}
},
}// main.js — enregistrement GLOBAL d'une directive (disponible partout sans import)
import { createApp } from 'vue'
import App from './App.vue'
import { vFocus } from './directives/vFocus'
import { vClickOutside } from './directives/vClickOutside'
const app = createApp(App)
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
app.mount('#app')Résumé
- Une directive personnalisée expose les mêmes hooks que le cycle de vie :
mounted,updated,unmounted... binding.value,binding.arg,binding.modifiersdonnent accès à tout ce que l'appelant a passé.- Toujours nettoyer les listeners/observers créés dans
mountedau sein du hookunmounted. - Utile pour : focus auto, click-outside, tooltips, lazy-loading d'image, intégration de libs DOM tierces.
Exercices pratiques
Mission : un menu qui ne se ferme jamais tout seul
Objectif : Corriger une directive click-outside sans nettoyage et sans référence conservée, puis l'enrichir d'un modificateur.
Contexte
Une directive vClickOutside a été codée rapidement : dans mounted, elle fait document.addEventListener('click', (event) => { if (!el.contains(event.target)) binding.value(event) }, true) avec une fonction fléchée anonyme, et il n'existe aucun hook unmounted. Le menu qui l'utilise est ouvert/fermé (donc monté/démonté) très fréquemment sur la page, et après quelques minutes d'utilisation, cliquer n'importe où sur la page devient perceptiblement plus lent.