in Vue 3 schreiben: die app.directive() API im Detail
Vue liefert mit v-if, v-for und v-model bereits die wichtigsten eingebauten Direktiven, aber manche Anforderungen lassen sich damit nicht abdecken, weil sie direkten, wiederverwendbaren Zugriff auf das rohe DOM-Element brauchen. Ein Klick außerhalb eines Elements erkennen, ein Input automatisch fokussieren, oder ein Element per Tooltip-Bibliothek initialisieren, das sind klassische Fälle für eine eigene Custom Directive. Mit der app.directive() API lässt sich eine solche Direktive global registrieren und danach in jeder Komponente per v-Präfix wie eine eingebaute Direktive verwenden, inklusive eigenem Lifecycle und typisiertem binding-Objekt.
Inhaltsverzeichnis
- 1. Was sind Custom Directives und wann braucht man sie
- 2. Die app.directive() API: globale Registrierung im Detail
- 3. Die Lifecycle-Hooks einer Direktive: created bis unmounted
- 4. Das binding-Objekt: value, oldValue, arg und modifiers
- 5. Praktisches Beispiel: v-focus für automatischen Fokus
- 6. Praktisches Beispiel: v-click-outside für Dropdowns und Modals
- 7. Lokale Registrierung in script setup mit vNameOfDirective
- 8. Direktive oder Composable? Die Entscheidungskriterien
- 9. Häufige Fehler und Fazit
- 10. Zusammenfassung
- 11. FAQ
1. Was sind Custom Directives und wann braucht man sie
Eine Custom Directive ist ein wiederverwendbares Stück Logik, das direkten Zugriff auf das gerenderte DOM-Element einer Komponente bekommt und an genau diesem Element wiederkehrende, oft imperative Aufgaben übernimmt. Anders als eine Komponente rendert eine Direktive kein eigenes Markup und besitzt keinen eigenen Zustand im klassischen Sinn, sondern hängt sich als kleiner Interceptor an den Lifecycle eines bestehenden Elements. Typische Kandidaten sind Aufgaben, die klassisch mit direkter DOM-Manipulation gelöst wurden, etwa Fokus setzen, auf Klicks außerhalb eines Elements reagieren, oder ein Element mit einer externen, nicht Vue-basierten Bibliothek initialisieren.
Der entscheidende Vorteil gegenüber wiederholtem, direktem Zugriff per Template-Ref liegt in der Wiederverwendbarkeit über viele Komponenten hinweg, ohne dass in jeder einzelnen Komponente derselbe onMounted-Code kopiert werden muss. Eine einmal registrierte Direktive wird per v-Präfix im Template genutzt, etwa als v-focus oder v-click-outside, und verhält sich für den Komponentenautor wie eine eingebaute Direktive. Damit bleibt die Logik an einer zentralen Stelle gepflegt, während jede Komponente, die sie braucht, nur eine einzige Zeile im Template ergänzt.
2. Die app.directive() API: globale Registrierung im Detail
Die globale Registrierung erfolgt über app.directive(name, definition), aufgerufen auf der App-Instanz, die auch für app.component() oder app.use() genutzt wird. Der Name wird ohne v-Präfix übergeben, also app.directive('focus', ...) für die spätere Verwendung als v-focus im Template. Die definition kann entweder ein Objekt mit einzelnen Lifecycle-Hooks sein, oder, als Kurzform, direkt eine Funktion, die dann für mounted und updated gleichzeitig aufgerufen wird, sofern beide Hooks identisches Verhalten haben sollen.
Global registrierte Direktiven stehen anschließend in der gesamten Anwendung zur Verfügung, unabhängig davon, in welcher Komponente sie verwendet werden, was sie besonders für generische, projektweite Werkzeuge wie v-focus oder v-tooltip geeignet macht. Für sehr spezifische, nur in einem eng begrenzten Teil der Anwendung benötigte Direktiven lohnt sich dagegen die lokale Registrierung direkt in der Komponente, die weiter unten in diesem Artikel behandelt wird, weil sie das globale Namensraum-Risiko vermeidet.
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// Kurzform: eine Funktion für mounted und updated zugleich
app.directive('highlight', (el, binding) => {
el.style.backgroundColor = binding.value ?? 'yellow'
})
app.mount('#app')
3. Die Lifecycle-Hooks einer Direktive: created bis unmounted
Eine vollständige Direktivendefinition kennt sieben Hooks, die eng an den Lifecycle der Komponente gekoppelt sind, in der das Element gerendert wird: created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount und unmounted. created läuft, bevor die Attribute des Elements gesetzt wurden, beforeMount kurz bevor das Element erstmals in den DOM eingefügt wird, und mounted direkt danach, wenn das Element und alle Kindkomponenten vollständig eingehängt sind. Für die meisten praktischen Fälle wie Fokus setzen oder Event-Listener registrieren ist mounted der richtige Hook, weil das Element dort garantiert im echten DOM vorhanden ist.
beforeUpdate und updated feuern, wenn sich der Host-Vnode neu rendert, unabhängig davon, ob sich der Wert der Direktive selbst geändert hat, weshalb ein Vergleich von binding.value und binding.oldValue innerhalb dieser Hooks oft notwendig ist, um unnötige Arbeit zu vermeiden. beforeUnmount und unmounted schließlich laufen kurz vor beziehungsweise nach dem Entfernen des Elements aus dem DOM und sind der richtige Ort, um in mounted registrierte Event-Listener wieder zu entfernen, damit keine Memory Leaks entstehen.
import type { Directive } from 'vue'
const vFocus: Directive<HTMLElement, void> = {
mounted(el) {
el.focus()
},
}
export default vFocus
4. Das binding-Objekt: value, oldValue, arg und modifiers
Jeder Hook einer Direktive bekommt als zweites Argument ein binding-Objekt, das den kompletten Kontext der Direktivenverwendung im Template beschreibt. value enthält den aktuellen Wert, den die Direktive per v-mydirective="someValue" übergeben bekommt, während oldValue in den Update-Hooks den Wert aus dem vorherigen Render liefert und damit gezielte Vergleiche ermöglicht. arg liest ein optionales Argument nach einem Doppelpunkt aus, etwa das color in v-mydirective:color="'red'", und modifiers ist ein Objekt mit boolschen Flags für jeden per Punkt angehängten Modifier wie das once in v-mydirective.once.
Zusätzlich liefert binding.instance Zugriff auf die Komponenteninstanz, in der die Direktive verwendet wird, was für seltene Fälle nützlich ist, in denen die Direktive auf Komponenten-Properties reagieren muss, ohne dass diese explizit als value übergeben wurden. In TypeScript-Projekten lohnt es sich, die Direktive mit dem generischen Typ Directive
5. Praktisches Beispiel: v-focus für automatischen Fokus
Das klassische Einsteigerbeispiel für eine Custom Directive ist v-focus, die ein Input-Element beim Einhängen automatisch fokussiert, etwa in einem Suchfeld, das direkt nach dem Öffnen eines Modals bereit zur Eingabe sein soll. Ohne Direktive müsste jede Komponente, die dieses Verhalten braucht, einen eigenen Template-Ref anlegen und in onMounted manuell el.value?.focus() aufrufen, was bei mehreren Formularen im Projekt schnell zu Codeduplikation führt. Als Direktive reduziert sich das auf ein einziges v-focus im Template, unabhängig davon, wie oft und wo diese Funktionalität gebraucht wird.
In der Praxis lohnt es sich, v-focus optional an eine Bedingung zu knüpfen, etwa v-focus="shouldFocus", damit der Fokus nur gesetzt wird, wenn tatsächlich ein bestimmter Zustand erreicht ist, zum Beispiel ein frisch geöffnetes Modal. Der Hook prüft dann binding.value zu Beginn von mounted und ruft el.focus() nur bei einem wahrheitswertigen Ergebnis auf, wodurch dieselbe Direktive sowohl für bedingungslosen als auch für bedingten Autofokus wiederverwendbar bleibt, ohne dass zwei getrennte Direktiven gepflegt werden müssen.
6. Praktisches Beispiel: v-click-outside für Dropdowns und Modals
Ein deutlich häufigerer Anwendungsfall in echten Projekten ist v-click-outside, die ein Dropdown, ein Kontextmenü oder ein Modal automatisch schließt, sobald außerhalb des Elements geklickt wird. Die Implementierung registriert in mounted einen globalen Click-Listener auf document, der prüft, ob das Klickziel innerhalb des Elements liegt, und andernfalls die übergebene Callback-Funktion aufruft. Entscheidend ist, den Listener in unmounted wieder zu entfernen, weil sonst bei jedem Öffnen und Schließen des Dropdowns ein neuer, nie aufgeräumter Listener am document hängen bleibt und die Anwendung im Laufe der Zeit langsamer wird.
Damit der Listener zwischen mounted und unmounted konsistent referenziert werden kann, speichert man die Handler-Funktion typischerweise direkt am Element selbst, etwa unter einem eigenen Property wie el._clickOutsideHandler, statt sie in einer externen, außerhalb der Direktive lebenden Variable zu halten. Diese Technik funktioniert zuverlässig auch dann, wenn dieselbe Direktive gleichzeitig an mehreren Elementen in unterschiedlichen Komponenten verwendet wird, weil jedes Element seinen eigenen Handler-Verweis mitbringt und sich die Instanzen nicht gegenseitig überschreiben.
import type { Directive } from 'vue'
type ClickOutsideBinding = (event: MouseEvent) => void
const vClickOutside: Directive<HTMLElement, ClickOutsideBinding> = {
mounted(el, binding) {
const handler = (event: MouseEvent) => {
if (!el.contains(event.target as Node)) {
binding.value(event)
}
}
el._clickOutsideHandler = handler
document.addEventListener('click', handler, true)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutsideHandler, true)
delete el._clickOutsideHandler
},
}
export default vClickOutside
7. Lokale Registrierung in script setup mit vNameOfDirective
Neben der globalen Registrierung über app.directive() unterstützt Vue auch lokale Direktiven, die nur innerhalb einer einzelnen Komponente gültig sind. Im klassischen Options-API-Stil geschieht das über die directives-Option, in script setup dagegen automatisch über die Namenskonvention: Eine importierte oder lokal definierte Konstante, deren Name mit einem kleinen v beginnt und danach in PascalCase weitergeht, wie vClickOutside, wird vom Compiler automatisch als v-click-outside im Template erkannt, ganz ohne explizite Registrierung.
Diese Konvention ist besonders praktisch für Direktiven, die nur in einem eng begrenzten Feature-Bereich gebraucht werden, etwa eine spezielle Validierungsdirektive, die ausschließlich in einem einzigen Formular verwendet wird. Sie vermeidet zusätzlich das Risiko von Namenskollisionen im globalen Namensraum, das bei vielen über app.directive() registrierten Direktiven in einem großen Projekt entstehen kann, weil jede Komponente ihre lokalen Direktiven unabhängig importiert und nur dort sichtbar macht, wo sie tatsächlich gebraucht werden.
8. Direktive oder Composable? Die Entscheidungskriterien
Die zentrale Frage bei der Wahl zwischen Direktive und Composable lautet, ob die Logik direkten, imperativen Zugriff auf ein rohes DOM-Element braucht oder ob sie sich vollständig über reaktiven Zustand und Template-Bindings ausdrücken lässt. Ein Composable wie useMousePosition oder useLocalStorage kapselt reaktive Logik und liefert reaktive Werte zurück, die dann ganz normal per Interpolation oder v-bind im Template landen, ohne dass irgendein Code das DOM-Element selbst anfassen muss.
Eine Direktive ist dagegen die richtige Wahl, sobald die Aufgabe untrennbar mit einer imperativen DOM-Operation verbunden ist, etwa Fokus setzen, einen globalen Event-Listener auf einem bestimmten Element registrieren, oder eine externe, nicht Vue-basierte Bibliothek wie eine Chart- oder Tooltip-Library direkt an ein Element binden. Manche Fälle lassen sich sogar mit beiden Ansätzen lösen, etwa Click-Outside-Erkennung, die auch als Composable mit einem Template-Ref als Parameter implementierbar ist. Faustregel: Braucht mehr als eine Komponente dasselbe Verhalten an mehreren, beliebigen Elementen im Template, ist eine Direktive meist die knappere, deklarativere Lösung.
9. Häufige Fehler und Fazit
Der häufigste Fehler ist, in mounted registrierte Event-Listener oder Timer nicht im passenden Unmount-Hook wieder zu entfernen, wodurch sich bei jedem Ein- und Aushängen der Komponente Listener-Leichen ansammeln, die spürbar Speicher und Performance kosten. Ein zweiter häufiger Fehler ist, in updated blind auf jede Änderung zu reagieren, ohne binding.value gegen binding.oldValue zu vergleichen, was bei teuren Operationen wie dem Neuinitialisieren einer externen Bibliothek unnötige Arbeit bei jedem Render verursacht, selbst wenn sich der relevante Wert gar nicht geändert hat.
Als Fazit bleibt: Custom Directives sind kein Ersatz für Composables, sondern ein ergänzendes Werkzeug für genau die Fälle, in denen imperativer DOM-Zugriff unumgänglich ist. Wer app.directive() für projektweite, generische Direktiven wie v-focus nutzt und lokale vNameOfDirective-Konstanten für feature-spezifische Fälle reserviert, behält eine saubere Trennung zwischen global wiederverwendbarem und lokal begrenztem Verhalten, ohne dass sich Direktivenlogik unkontrolliert über das ganze Projekt verstreut.
| Hook | Zeitpunkt | Typischer Einsatz | Zugriff auf DOM |
|---|---|---|---|
| created | Vor dem Setzen der Attribute | Initiale Vorbereitung ohne DOM-Zugriff | Nein |
| mounted | Element vollständig im DOM eingehängt | Fokus setzen, Listener registrieren | Ja, vollständig |
| updated | Nach jedem Re-Render des Host-Vnodes | Wert-Vergleich, gezieltes Nachziehen | Ja, vollständig |
| unmounted | Nach Entfernen des Elements aus dem DOM | Listener und Timer aufräumen | Element bereits entfernt |
Mironsoft
Vue-Architektur, Composition API und Nuxt-Performance
Vue-Anwendungen, die mit jedem Feature nicht komplizierter werden?
Wir prüfen bestehende Vue- und Nuxt-Projekte auf unstrukturierte Composables, ungenutzte Reaktivität und aufgeblähte Bundles und bauen daraus eine Architektur, die neue Features aufnimmt, ohne die Codebasis unübersichtlicher zu machen.
Architektur-Review
Composables, State-Management und Komponentenstruktur auf Wartbarkeit prüfen.
Performance-Audit
Reaktivitäts-Overhead, Bundle-Größe und Nuxt-Rendering-Strategie systematisch optimieren.
Nuxt-Integration
SSR/SSG-Setup und API-Anbindung robust und typsicher aufbauen.
10. Zusammenfassung
Eigene Custom Directives in Vue: Das Wichtigste auf einen Blick
Registrierung
Global per app.directive(name, definition), lokal in script setup automatisch über Konstanten im vNameOfDirective-Schema.
Lifecycle
Sieben Hooks von created bis unmounted, eng an den Lifecycle der Host-Komponente gekoppelt.
Kontext
Das binding-Objekt liefert value, oldValue, arg und modifiers für jeden Hook-Aufruf.
Abgrenzung
Direktive für imperativen DOM-Zugriff, Composable für reaktive Logik ohne direktes DOM-Handling.