Skip to content

Kapitel 39: Vue 3 Fortgeschrittene Konzepte

📙 Lernziel: Benutzerdefinierte Direktiven, Teleport, Suspense, dynamische Komponenten meistern!


39.1 Benutzerdefinierte Direktiven

Was sind benutzerdefinierte Direktiven? Eigene Direktiven können als wiederverwendbare Funktionen erstellt werden.

Globale Direktive (main.js):

javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// Globale Direktive registrieren
app.directive('fokus', {
  mounted(el) {
    el.focus()
  }
})

app.mount('#app')

Lokale Direktive (in Komponente):

vue
<script setup>
// Lokale Direktive (Composition API)
const vFokus = {
  mounted(el) {
    el.focus()
  }
}

// Oder als Funktion (Shortand)
const vHighlight = (el, binding) => {
  el.style.backgroundColor = binding.value || 'yellow'
}
</script>

<template>
  <div>
    <input v-fokus placeholder="Autofokus" />
    <p v-highlight="'lightblue'">Hervorgehoben</p>
  </div>
</template>

Hook-Funktionen für Direktiven:

  • beforeMount: Bevor Element eingehängt wird
  • mounted: Nachdem Element eingehängt wurde
  • beforeUpdate: Bevor Element aktualisiert wird
  • updated: Nachdem Element aktualisiert wurde
  • beforeUnmount: Bevor Element entfernt wird
  • unmounted: Nachdem Element entfernt wurde

Beispiel (erweiterte Direktive):

vue
<script setup>
const vPotion = {
  mounted(el, binding) {
    el.style.position = 'fixed'
    el.style.top = binding.value.top + 'px'
    el.style.left = binding.value.left + 'px'
  },
  updated(el, binding) {
    el.style.top = binding.value.top + 'px'
    el.style.left = binding.value.left + 'px'
  }
}
</script>

<template>
  <div v-potion="{ top: 100, left: 50 }">
    Ich bin fixiert!
  </div>
</template>

39.2 Teleport

Was ist Teleport? Teleport ermöglicht es, Elemente in einen anderen Bereich des DOMs zu "teleportieren".

Anwendungsfall:

  • Modal-Dialoge
  • Tooltips
  • Benachrichtigungen

Beispiel (Modal):

vue
<script setup>
import { ref } from 'vue'

const isModalOpen = ref(false)

const openModal = () => {
  isModalOpen.value = true
}

const closeModal = () => {
  isModalOpen.value = false
}
</script>

<template>
  <div>
    <button @click="openModal">Modal öffnen</button>
    
    <!-- Teleport to body -->
    <teleport to="body">
      <div v-if="isModalOpen" class="modal">
        <div class="modal-content">
          <h2>Modal Titel</h2>
          <p>Dies ist ein Modal-Fenster.</p>
          <button @click="closeModal">Schließen</button>
        </div>
      </div>
    </teleport>
  </div>
</template>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
  max-width: 500px;
  width: 100%;
}
</style>

Wichtig:

  • to Attribut kann CSS-Selektor sein (z.B. body, #modal-container)
  • ✅ Element wird im DOM verschoben, bleibt aber logisch in der Komponente

39.3 Suspense

Was ist Suspense? Suspense ermöglicht das Anzeigen von Ladezuständen während asynchroner Komponenten geladen werden.

Beispiel (Async Komponente):

vue
<!-- AsyncComponent.vue -->
<script setup>
// Simuliere API-Aufruf
const data = await new Promise(resolve => {
  setTimeout(() => {
    resolve({ message: 'Daten geladen!' })
  }, 2000)
})
</script>

<template>
  <div>
    <h2>{{ data.message }}</h2>
  </div>
</template>

Verwendung mit Suspense:

vue
<script setup>
import { defineAsyncComponent } from 'vue'

// Asynchrone Komponente laden
const AsyncComponent = defineAsyncComponent(() =>
  import('./AsyncComponent.vue')
)
</script>

<template>
  <div>
    <suspense>
      <!-- Standard-Inhalt (während Laden) -->
      <template #default>
        <AsyncComponent />
      </template>
      
      <!-- Ladeanzeige -->
      <template #fallback>
        <div class="loading">Lädt...</div>
      </template>
    </suspense>
  </div>
</template>

<style scoped>
.loading {
  text-align: center;
  padding: 50px;
  font-size: 1.2em;
  color: #666;
}
</style>

Wichtig:

  • #default Slot: Die asynchrone Komponente
  • #fallback Slot: Ladeanzeige
  • ✅ Mehrere asynchrone Komponenten können gleichzeitig geladen werden

39.4 Dynamische Komponenten & keep-alive

Dynamische Komponenten:

vue
<script setup>
import { ref, shallowRef } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

const currentComponent = shallowRef(ComponentA)

const switchComponent = () => {
  currentComponent.value = currentComponent.value === ComponentA 
    ? ComponentB 
    : ComponentA
}
</script>

<template>
  <div>
    <button @click="switchComponent">Komponente wechseln</button>
    
    <!-- Dynamische Komponente -->
    <component :is="currentComponent" />
  </div>
</template>

keep-alive (Zustand erhalten):

vue
<script setup>
import { ref, shallowRef } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

const currentComponent = shallowRef(ComponentA)
</script>

<template>
  <div>
    <button @click="currentComponent = ComponentA">A</button>
    <button @click="currentComponent = ComponentB">B</button>
    
    <!-- Keep Alive: Zustand wird beim Wechsel erhalten -->
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>

Lebenszyklus-Hooks für keep-alive:

  • onActivated(): Wenn Komponente aktiviert wird
  • onDeactivated(): Wenn Komponente deaktiviert wird

Beispiel:

vue
<script setup>
import { ref, onActivated, onDeactivated } from 'vue'

const count = ref(0)

onActivated(() => {
  console.log('Komponente aktiviert')
})

onDeactivated(() => {
  console.log('Komponente deaktiviert')
})
</script>

39.5 Modulare Entwicklung & Komponenten-Struktur

Best Practices für Komponenten-Struktur:

1. Einzelne Dateiverantwortung

src/
├── components/           # Wiederverwendbare Komponenten
│   ├── BaseButton.vue
│   ├── BaseInput.vue
│   └── BaseModal.vue
├── views/               # Seiten-Komponenten (Routen)
│   ├── HomeView.vue
│   ├── AboutView.vue
│   └── ContactView.vue
├── composables/         # Composition API Funktionen
│   ├── useCounter.js
│   └── useFetch.js
├── stores/              # Pinia Stores
│   ├── counter.js
│   └── user.js
└── utils/                # Hilfsfunktionen
    └── helpers.js

2. Benennungskonventionen

  • Komponenten: PascalCase (MyComponent.vue)
  • Composables: camelCase mit use Präfix (useCounter.js)
  • Stores: camelCase (counter.js, user.js)

3. Import/Export Organisation

vue
<script setup>
// Absolute Pfade mit Alias (@/ zeigt auf src/)
import BaseButton from '@/components/BaseButton.vue'
import { useCounter } from '@/composables/useCounter'
import { useUserStore } from '@/stores/user'

// Relativ Pfade nur für eng verwandte Dateien
import HelperComponent from './HelperComponent.vue'
</script>

4. Barrierefreieit (Accessibility)

vue
<template>
  <button 
    aria-label="Menü öffnen"
    @click="toggleMenu"
  >
    <span aria-hidden="true">☰</span>
  </button>
  
  <nav v-if="isMenuOpen" aria-label="Hauptnavigation">
    <a href="#content" tabindex="0">Zum Inhalt</a>
    <!-- ... -->
  </nav>
</template>

39.6 Übung: Modal-Komponente mit Teleport

Aufgabe: Erstelle eine wiederverwendbare Modal-Komponente mit Teleport.

Lösung:

1. BaseModal.vue Komponente erstellen:

vue
<!-- components/BaseModal.vue -->
<script setup>
defineProps({
  title: {
    type: String,
    default: 'Modal'
  }
})

const emit = defineEmits(['close'])
</script>

<template>
  <teleport to="body">
    <div class="modal-overlay" @click.self="emit('close')">
      <div class="modal-container">
        <div class="modal-header">
          <h3>{{ title }}</h3>
          <button class="close-btn" @click="emit('close')">×</button>
        </div>
        <div class="modal-body">
          <!-- Slot für Inhalt -->
          <slot />
        </div>
        <div class="modal-footer">
          <slot name="footer">
            <button @click="emit('close')">Schließen</button>
          </slot>
        </div>
      </div>
    </div>
  </teleport>
</template>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-container {
  background: white;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
  max-height: 80vh;
  overflow-y: auto;
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  border-bottom: 1px solid #eee;
}

.close-btn {
  background: none;
  border: none;
  font-size: 1.5em  ;
  cursor: pointer;
}

.modal-body {
  padding: 20px;
}

.modal-footer {
  padding: 15px 20px;
  border-top: 1px solid #eee;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>

2. Verwendung in App.vue:

vue
<script setup>
import { ref } from 'vue'
import BaseModal from './components/BaseModal.vue'

const isModalOpen = ref(false)
</script>

<template>
  <div>
    <button @click="isModalOpen = true">Modal öffnen</button>
    
    <BaseModal 
      v-if="isModalOpen" 
      title="Mein Modal"
      @close="isModalOpen = false"
    >
      <p>Dies ist der Modal-Inhalt!</p>
      
      <template #footer>
        <button @click="isModalOpen = false">Abbrechen</button>
        <button @click="isModalOpen = false">OK</button>
      </template>
    </BaseModal>
  </div>
</template>

✅ Zusammenfassung

In diesem Kapitel hast du gelernt:

  • ✅ Benutzerdefinierte Direktiven erstellen
  • ✅ Teleport für Modal-Dialoge verwenden
  • ✅ Suspense für asynchrone Komponenten
  • ✅ Dynamische Komponenten & keep-alive
  • ✅ Modulare Entwicklung & Best Practices
  • ✅ Praxis: Modal-Komponente mit Teleport

🎯 Nächster Schritt: In Kapitel 40 lernst du das TodoList-Projekt!


← Zurück zu Kapitel 38: CORSWeiter zu Kapitel 40: TodoList Projekt →

Frei für alle Anfänger