Eigene Vue Custom Directives schreiben: Die app.directive() API im Detail
AI generated
{ }
Vue.js · Custom Directives · Composition API · DOM-Zugriff
Eigene Custom Directives
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.

14 Min. Lesezeit app.directive() · Lifecycle-Hooks Vue 3 · Composition API · TypeScript

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 zu deklarieren, damit sowohl das el-Argument als auch binding.value korrekt typisiert sind und Tippfehler oder falsche Werttypen bereits beim Kompilieren auffallen, statt erst zur Laufzeit.

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.

11. FAQ: Eigene Custom Directives in Vue: Das Wichtigste auf einen Blick

1Wie registriert man eine Custom Directive global in Vue 3?
Mit app.directive(name, definition) auf der App-Instanz, wobei der Name ohne v-Präfix übergeben wird. Die Direktive steht danach in der gesamten Anwendung unter dem Präfix v- zur Verfügung, zum Beispiel als v-focus im Template jeder Komponente.
2Welche Lifecycle-Hooks besitzt eine Custom Directive?
Sieben Hooks: created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount und unmounted. Sie sind eng an den Lifecycle der Komponente gekoppelt, in der das Element mit der Direktive gerendert wird.
3Was enthält das binding-Objekt einer Direktive?
value für den aktuellen Wert, oldValue für den Wert aus dem vorherigen Render, arg für ein optionales Argument nach einem Doppelpunkt, sowie modifiers als Objekt mit boolschen Flags für angehängte Punkt-Modifier.
4Wie funktioniert die lokale Registrierung in script setup?
Eine Konstante, deren Name mit kleinem v beginnt und danach in PascalCase weitergeht, wie vClickOutside, wird vom Compiler automatisch als v-click-outside im Template erkannt, ohne explizite Registrierung in einer directives-Option.
5Warum sollte man Event-Listener im unmounted-Hook entfernen?
Weil ein in mounted registrierter Listener sonst bei jedem Aushängen der Komponente am document oder window hängen bleibt. Über viele Ein- und Aushänge-Zyklen sammeln sich so nicht aufgeräumte Listener an, die Speicher verbrauchen und die Anwendung verlangsamen.
6Wann ist eine Direktive die bessere Wahl als ein Composable?
Wenn die Aufgabe direkten, imperativen Zugriff auf ein rohes DOM-Element braucht, etwa Fokus setzen, einen Event-Listener auf dem Element registrieren, oder eine externe, nicht Vue-basierte Bibliothek an das Element binden.
7Kann eine Direktive Argumente und Modifier entgegennehmen?
Ja. Ein Argument nach einem Doppelpunkt wie in v-mydirective:color="'red'" landet in binding.arg, und jeder per Punkt angehängte Modifier wie in v-mydirective.once erscheint als true in binding.modifiers.
8Wie typisiert man eine Custom Directive in TypeScript?
Mit dem generischen Typ Directive aus vue, zum Beispiel Directive für eine Direktive, die an ein HTMLElement gebunden wird und einen booleschen Wert erwartet.
9Warum feuert updated auch dann, wenn sich der Wert der Direktive gar nicht geändert hat?
Weil updated an den Re-Render des gesamten Host-Vnodes gekoppelt ist, nicht nur an eine Änderung des Direktivenwerts. Ein Vergleich von binding.value und binding.oldValue innerhalb des Hooks ist deshalb oft nötig, um unnötige Arbeit zu vermeiden.
10Löst v-click-outside zuverlässig auch bei mehreren Elementen gleichzeitig?
Ja, sofern der Handler pro Element gespeichert wird, etwa als eigenes Property direkt am Element. So bringt jedes Element seinen eigenen Listener-Verweis mit, und mehrere gleichzeitig verwendete Instanzen der Direktive überschreiben sich nicht gegenseitig.