Provide und Inject
Auftrag
Bis Teil 6 einen Login bringt, gilt Martina Hostettler mit der Mitgliedernummer M-117 als angemeldetes Testmitglied. Die App soll das schon jetzt nutzen.
Als angemeldetes Mitglied möchte ich, dass meine Mitgliedernummer im Formular schon steht, damit ich sie nicht jedes Mal eintippen muss.
Die Akzeptanzkriterien lauten:
- Im Kopf steht, wer angemeldet ist.
- Das Formular füllt die Mitgliedernummer des angemeldeten Mitglieds aus, und sie bleibt änderbar.
- Die Komponenten zwischen
Appund dem Formular wissen nichts vom Mitglied. - Der Dialog steht im DOM am Ende der Seite und überdeckt alles.
Lernziele
Nach diesem Kapitel kannst du
- erklären, wann das Durchreichen von Props mühsam wird,
- Werte mit
providebereitstellen und mitinjectholen, - Schlüssel für
provideundinjectin einer eigenen Datei festlegen, - die Regeln für den Entwurf von Komponenten anwenden,
- Inhalte mit
Teleportan eine andere Stelle der Seite verschieben.
Props durchreichen
Abschnitt betitelt „Props durchreichen“App kennt das angemeldete Mitglied, das Formular braucht seine Nummer. Dazwischen liegt die Seite des Dings. Mit Props muss jede Komponente auf dem Weg das Mitglied entgegennehmen und weitergeben, auch wenn sie selbst nichts damit anfängt. Schau dir an, wie das Mitglied im Beispiel zum Formular kommt.
App.vue
<script setup>import ItemPage from './ItemPage.vue'
const member = { number: 'M-117', name: 'Martina Hostettler' }</script>
<template> <ItemPage :member="member" /></template>ItemPage.vue
<script setup>import ReservationForm from './ReservationForm.vue'
// ItemPage does not use member itself, it only passes it ondefineProps({ member: { type: Object, required: true } })</script>
<template> <h1>Bohrmaschine</h1> <ReservationForm :member="member" /></template>ReservationForm.vue
<script setup>import { ref } from 'vue'
const props = defineProps({ member: { type: Object, required: true } })
const memberNumber = ref(props.member.number)</script>
<template> <label for="member">Mitgliedernummer</label> <input id="member" v-model="memberNumber"></template>ItemPage deklariert die Prop member nur, um sie weiterzureichen. Bei zwei Ebenen geht das noch. Bei fünf Ebenen hängen fünf Komponenten an einem Wert, den nur die letzte braucht, und eine Änderung am Mitglied zieht sich durch alle Dateien. Dieses Durchreichen heisst auf Englisch Prop Drilling.
provide und inject
Abschnitt betitelt „provide und inject“Mit provide stellt eine Komponente einen Wert für alle Komponenten unter ihr bereit. Jede davon holt ihn mit inject, egal wie tief sie liegt. Die Komponenten dazwischen merken nichts davon.
App.vue
<script setup>import { provide, readonly, ref } from 'vue'
import ItemPage from './ItemPage.vue'import { memberKey } from './keys.js'
const member = ref({ number: 'M-117', name: 'Martina Hostettler' })
// Every component below App can inject the member, readonly keeps the changes in Appprovide(memberKey, readonly(member))</script>
<template> <ItemPage /></template>ItemPage.vue
<script setup>import ReservationForm from './ReservationForm.vue'</script>
<template> <h1>Bohrmaschine</h1> <ReservationForm /></template>keys.js
// A symbol as the key, so no other provide can use the same name by accidentexport const memberKey = Symbol('member')ReservationForm.vue
<script setup>import { inject, ref } from 'vue'
import { memberKey } from './keys.js'
// Without a provider, inject returns the default value nullconst member = inject(memberKey, null)const memberNumber = ref(member?.value.number ?? '')</script>
<template> <p v-if="member">Angemeldet als {{ member.name }}</p> <label for="member">Mitgliedernummer</label> <input id="member" v-model="memberNumber"></template>Der Schlüssel ist ein Symbol aus der Datei keys.js. Zwei Symbole sind nie gleich, auch wenn sie dieselbe Beschreibung haben. So kann kein anderes provide den Wert aus Versehen überschreiben, und wer den Wert braucht, importiert den Schlüssel aus derselben Datei. Das zweite Argument von inject ist der Standardwert, falls keine Komponente darüber den Wert bereitstellt.
App stellt ein Ref bereit, darum bleibt der Wert reaktiv. Ändert sich das Mitglied, etwa nach dem Login in Teil 6, sehen alle Komponenten den neuen Wert. readonly sorgt dafür, dass nur App ihn ändern kann. Soll eine Komponente weiter unten etwas ändern dürfen, stellt App zusätzlich eine Funktion bereit, etwa zum Abmelden, und behält so die Kontrolle über den Wert.
Regeln für Komponenten
Abschnitt betitelt „Regeln für Komponenten“Mit Props, Events, Slots und provide hast du alle Wege, auf denen Komponenten zusammenarbeiten. Einige Regeln helfen, sie richtig zu wählen.
- Props gehen nach unten, Events nach oben. Jeder Wert hat eine Komponente, die ihn besitzt und ändert.
- Präsentationskomponenten wie
ItemCard,SearchBarundBaseDialogzeigen nur, was sie bekommen, und melden, was passiert. Containerkomponenten wieApp,CatalogViewundItemViewbesitzen Daten und entscheiden. Präsentationskomponenten lassen sich wiederverwenden und leicht testen. - Schnittstellen bleiben klein. Eine Komponente bekommt nur die Werte, die sie braucht, und kennt nur die Events, die sie wirklich auslöst.
provideundinjectpassen für Werte, die viele Komponenten tief im Baum brauchen, wie das angemeldete Mitglied, die Sprache oder ein Farbschema. Zwischen Eltern und Kind bleiben Props deutlicher. Für Zustand, den viele Teile der App ändern, kommt in Teil 7 Pinia.
Teleport
Abschnitt betitelt „Teleport“Der Dialog steht im Template des Formulars, also im DOM mitten in der Seite. Hat ein Vorfahre overflow: hidden oder transform, schneidet er den Dialog ab oder hält ihn gefangen, auch mit position: fixed. <Teleport to="body"> rendert den Inhalt stattdessen am Ende von body. Für Vue bleibt er ein Teil der Komponente, Props, Events und inject funktionieren wie vorher.
<script setup>import { ref } from 'vue'
const open = ref(false)const teleported = ref(true)</script>
<template> <label><input v-model="teleported" type="checkbox"> Mit Teleport</label> <div class="card"> <button type="button" @click="open = true">Dialog öffnen</button> <!-- disabled keeps the content in place, the checkbox switches it --> <Teleport to="body" :disabled="!teleported"> <div v-if="open" class="backdrop" @click.self="open = false"> <div class="dialog" role="dialog" aria-modal="true" aria-label="Hinweis"> <p>Der Dialog liegt über der ganzen Seite.</p> <button type="button" @click="open = false">Schliessen</button> </div> </div> </Teleport> </div></template>
<style>/* A card that clips its content, as cards often do */.card { overflow: hidden; height: 6rem; margin-top: 0.5rem; padding: 0.5rem; border: 1px solid #c8c8c8; transform: translateZ(0);}.backdrop { position: fixed; inset: 0; display: grid; place-items: center; background: rgb(0 0 0 / 40%);}.dialog { padding: 1rem; border-radius: 8px; background: #ffffff;}</style>Ohne Teleport bleibt der Dialog in der Karte und wird abgeschnitten, mit Teleport liegt er über der ganzen Seite. Das Ziel muss beim Rendern schon existieren. body ist immer da, ein eigenes Element wie <div id="modals"> muss in index.html stehen.
Exkurs (optional) Dynamische Komponenten und KeepAlive
<component :is="current"> rendert die Komponente, die in current steht. Ein Wechsel des Werts tauscht die Komponente aus. <KeepAlive> darum herum behält die gerade nicht sichtbare Komponente samt ihrem Zustand, statt sie zu zerstören.
App.vue
<script setup>import { shallowRef } from 'vue'
import NoteTab from './NoteTab.vue'import RulesTab from './RulesTab.vue'
// shallowRef, because a component itself should not become reactiveconst current = shallowRef(NoteTab)</script>
<template> <button type="button" @click="current = NoteTab">Notiz</button> <button type="button" @click="current = RulesTab">Regeln</button> <!-- KeepAlive keeps the component that is not shown, with its state --> <KeepAlive> <component :is="current" /> </KeepAlive></template>NoteTab.vue
<script setup>import { ref } from 'vue'
const note = ref('')</script>
<template> <p> <label for="note">Notiz zur Anfrage</label> <textarea id="note" v-model="note"></textarea> </p></template>RulesTab.vue
<template> <p>Eine Ausleihe dauert höchstens sieben Tage.</p></template>Tipp eine Notiz, wechsle zu den Regeln und zurück, die Notiz ist noch da. Ohne KeepAlive ist sie weg. shallowRef hält die Komponente, ohne sie selbst reaktiv zu machen. In Teil 6 rendert Vue Router die Seiten nach demselben Prinzip, und KeepAlive lässt sich dort ebenso einsetzen. Die Fallbeispiel-App behält den Suchtext stattdessen in App, weil so sichtbar bleibt, wo er liegt.
Im Code
Abschnitt betitelt „Im Code“Arbeite mit dem Beispiel zu provide und inject. Lies den Code und schreib auf, was du erwartest, bevor du etwas änderst.
- Was zeigt das Formular? Woher kommen der Name und die Nummer?
- Kommentiere
provideinAppaus. Was zeigt das Formular jetzt, und warum erscheint keine Warnung? - Entferne zusätzlich den Standardwert
nullininject. Was meldet die Konsole? - Wechsle zum Beispiel mit Teleport, nimm den Haken bei «Mit Teleport» weg und öffne den Dialog. Wo steht er jetzt im DOM?
Aufgaben
Abschnitt betitelt „Aufgaben“Die Aufgaben geben das Testmitglied durch den Baum und verschieben den Dialog. Du findest das Projekt im Kursrepo im Ordner exercises/04-components/provide-inject/de. Kopiere den Ordner, installiere die Pakete und starte die Tests. Der Schlüssel memberKey steht schon in src/keys.js.
npm installnpm testAufgabe Einstieg
Stell in App.vue das Mitglied mit provide und dem Schlüssel memberKey bereit, schreibgeschützt mit readonly. AppHeader holt es mit inject und dem Standardwert null und zeigt in einem Element mit der Klasse member «Angemeldet als Martina Hostettler». Ohne Mitglied fehlt die Zeile.
Lösung
// The test member until the login of part 6const member = ref({ number: 'M-117', name: 'Martina Hostettler' })
// Every component below App can inject the member, readonly keeps the changes in Appprovide(memberKey, readonly(member))<script setup>import { inject } from 'vue'
import { memberKey } from '@/keys.js'
// Without a provider, inject returns the default value nullconst member = inject(memberKey, null)</script>
<template> <header class="site"> <a href="#/" class="brand">Lorraine Commons</a> <p v-if="member" class="member">Angemeldet als {{ member.name }}</p> </header></template>Im Template packt Vue das Ref aus, darum genügt member.name. Ohne App darüber, etwa in einem Test, liefert inject den Standardwert null, und v-if lässt die Zeile weg.
Aufgabe Vertiefung
ReservationForm holt das Mitglied ebenfalls mit inject und füllt das Feld der Mitgliedernummer damit aus. Ohne Mitglied bleibt das Feld leer. ItemPage bekommt keine Prop für das Mitglied.
Lösung
const member = inject(memberKey, null)// The field starts with the number of the signed-in member and stays editableconst memberNumber = ref(member?.value.number ?? '')member?.value.number liest die Nummer, wenn es ein Mitglied gibt, sonst greift ?? ''. Das Feld bekommt nur den Startwert, danach gehört sein Inhalt dem Formular. Ein Mitglied, das für jemand anderen reserviert, kann die Nummer also ändern.
Aufgabe Knobelei
Der Dialog nach dem Senden soll im DOM am Ende der Seite stehen und nicht im Formular. Ändere BaseDialog so, dass er seinen Inhalt mit Teleport an das Ende von body verschiebt. Der Dialog zeigt weiter das Ding und die Mitgliedernummer und lässt sich schliessen.
Lösung
<template> <!-- The dialog leaves the component and ends up at the end of the page --> <Teleport to="body"> <div class="backdrop"> <div class="dialog" role="dialog" aria-modal="true" :aria-labelledby="titleId"> <h2 :id="titleId"> <slot name="title">Hinweis</slot> </h2> <slot /> <div class="actions"> <slot name="actions"> <button type="button" @click="$emit('close')">Schliessen</button> </slot> </div> </div> </div> </Teleport></template>Weil Teleport in BaseDialog steht, landet jeder Dialog der App am Ende der Seite, ohne dass die Formulare etwas davon wissen. Slots, Props und das Event close funktionieren wie vorher, denn für Vue bleibt der Inhalt ein Teil der Komponente.
Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
inject steht in einer Funktion für ein Event, und Vue warnt, dass es nur im Setup funktioniert. | inject zuoberst in <script setup> aufrufen und den Wert in einer Konstante behalten. |
| Zwei Teile der App verwenden denselben Text als Schlüssel und überschreiben sich. | Schlüssel als Symbol in einer eigenen Datei festlegen. |
| Eine Änderung des bereitgestellten Werts kommt unten nicht an. | Ein Ref oder ein reaktives Objekt bereitstellen, keinen einfachen Wert. |
| Eine Komponente weit unten ändert den bereitgestellten Wert, und niemand weiss, woher die Änderung kommt. | Den Wert mit readonly bereitstellen und eine Funktion zum Ändern dazugeben. |
Vue meldet Failed to locate Teleport target with selector "#modals". | Das Ziel muss beim Rendern existieren. body verwenden oder das Element in index.html ergänzen. |
Ein KI-Assistent legt das Mitglied in window ab oder greift über $root darauf zu. | Werte für den ganzen Baum mit provide bereitstellen, für viel geteilten Zustand später Pinia verwenden. |
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Wann lohnen sich
provideundinjectstatt Props? - Warum ist ein
Symbolein guter Schlüssel? - Wie stellst du sicher, dass nur
Appdas Mitglied ändert? - Was bewirkt
Teleport, und was bleibt gleich?
Antworten
- Wenn ein Wert tief im Baum gebraucht wird und die Komponenten dazwischen ihn nur durchreichen würden.
- Zwei Symbole sind nie gleich, also kann kein anderes
providedenselben Schlüssel aus Versehen verwenden. Appstellt das Mitglied mitreadonlybereit. Soll jemand weiter unten es ändern, bekommt er eine Funktion vonApp.- Der Inhalt landet im DOM an einer anderen Stelle, etwa am Ende von
body. Für Vue bleibt er Teil der Komponente, mit Props, Events, Slots undinject.
Auftrag
Die App besteht jetzt aus Komponenten, und jede hat eine klare Aufgabe. Am Tresen ist trotzdem ein Ärgernis geblieben. «Nach dem Neuladen sind meine Favoriten weg, und die Suche fängt immer von vorne an», erzählt ein Mitglied. Darum geht es in Teil 5. Vorher prüfst du mit dem Kompetenzraster, ob du für Teil 5 bereit bist.
Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.