Verschachtelte Routen und Layouts
Auftrag
Das Team am Tresen braucht einen eigenen Bereich in der App.
Als Freiwillige oder Freiwilliger am Tresen möchte ich die Ausleihen und die Rückgaben unter einer gemeinsamen Adresse finden, damit ich schnell zwischen den beiden wechseln kann.
Die Akzeptanzkriterien lauten:
- Unter
/teamstehen die Dinge, die ausgeliehen sind, die nächste Rückgabe zuerst. - Unter
/team/returnserfasst das Team eine Rückgabe, und der Katalog zeigt das Ding danach als verfügbar. - Beide Seiten teilen eine Überschrift und eine Navigation, die zeigt, wo man ist.
Lernziele
Nach diesem Kapitel kannst du
- Kindrouten mit
childrenfestlegen, - ein Layout mit einer eigenen
RouterViewbauen, - eine Kindroute mit leerem Pfad als Startseite eines Bereichs einsetzen,
- aktive und genau aktive Links unterscheiden,
- mit benannten Views mehrere Komponenten auf eine Route verteilen.
Kindrouten
Abschnitt betitelt „Kindrouten“Eine Route kann Kindrouten haben. Sie stehen in children, und ihre Pfade hängen am Pfad der Elternroute, returns unter /team ergibt /team/returns. Die Komponente der Elternroute erscheint in der RouterView von App. Die Komponente der Kindroute erscheint in einer zweiten RouterView in der Elternkomponente.
App.vue
<script setup>import { RouterLink, RouterView, useRoute } from 'vue-router'
const route = useRoute()</script>
<template> <nav> <RouterLink to="/">Katalog</RouterLink> <RouterLink to="/team">Team</RouterLink> </nav> <p>Adresse: {{ route.fullPath }}</p> <RouterView /></template>
<style scoped>nav { display: flex; flex-wrap: wrap; gap: 1rem;}/* Also active on every page below the link, here on the pages of the team */.router-link-active { font-weight: 700;}</style>CatalogView.vue
<template> <h1>Katalog</h1> <p>60 Dinge zum Ausleihen.</p></template>main.js
import { createApp } from 'vue'
import App from './App.vue'import router from './router.js'
createApp(App).use(router).mount('#app')router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import CatalogView from './CatalogView.vue'import TeamLayout from './TeamLayout.vue'import TeamLoansView from './TeamLoansView.vue'import TeamReturnsView from './TeamReturnsView.vue'
export default createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: CatalogView }, { path: '/team', component: TeamLayout, // The children appear in the RouterView of TeamLayout children: [ // The empty path shows the loans at /team itself { path: '', component: TeamLoansView }, { path: 'returns', component: TeamReturnsView }, ], }, ],})TeamLayout.vue
<script setup>import { RouterLink, RouterView } from 'vue-router'</script>
<template> <h1>Team am Tresen</h1> <nav> <RouterLink to="/team">Ausleihen</RouterLink> <RouterLink to="/team/returns">Rückgaben</RouterLink> </nav> <!-- The page of the child route appears here, inside the layout --> <RouterView /></template>
<style scoped>nav { display: flex; gap: 1rem;}/* Only the link to exactly this page, the loans link is active on every child */.router-link-exact-active { text-decoration: underline 3px;}</style>TeamLoansView.vue
<template> <h2>Ausleihen</h2> <ul> <li>Zelt für vier Personen, zurück ab 12.11.2026</li> <li>Festbankgarnitur, zurück ab 3.11.2026</li> </ul></template>TeamReturnsView.vue
<template> <h2>Rückgaben</h2> <p>Hier erfasst das Team, was zurückkommt.</p></template>Die Kindroute mit dem leeren Pfad '' gehört zur Adresse /team selbst und ist die Startseite des Bereichs. Ohne sie zeigte /team nur die Überschrift und eine leere Stelle. Ein Kindpfad mit / am Anfang gälte laut Dokumentation ab der Wurzel, darum stehen Kindpfade ohne Schrägstrich.
Layouts
Abschnitt betitelt „Layouts“Die Elternkomponente eines Bereichs ist sein Layout. TeamLayout hat die Überschrift und die Navigation, die alle Seiten des Teams teilen, und eine RouterView für die Seite, die gerade dran ist. Wechselt jemand von den Ausleihen zu den Rückgaben, bleibt das Layout stehen, und nur der Inhalt darin wechselt.
Die Abbildung zeigt alle Routen der Fallbeispiel-App nach diesem Teil. Die gestrichelten Kästchen laden erst bei Bedarf, und für den Bereich des Teams braucht es eine Anmeldung. Beides kommt in den nächsten zwei Kapiteln.
Aktive und genau aktive Links
Abschnitt betitelt „Aktive und genau aktive Links“Im Beispiel ist der Link «Team» im Kopf auf beiden Seiten des Bereichs fett. Er ist aktiv, weil die aktuelle Route unter seiner liegt. Der Link «Ausleihen» zeigt auf die leere Kindroute und gilt deshalb auf allen Seiten des Bereichs als aktiv. Genau aktiv ist dagegen nur der Link zur Seite, die gerade offen ist, und nur er bekommt router-link-exact-active und aria-current="page". Die Navigation im Kopf gestaltet darum router-link-active, die Navigation im Bereich router-link-exact-active.
Benannte Views
Abschnitt betitelt „Benannte Views“Eine Seite kann mehr als eine Stelle für Komponenten haben, etwa eine Spalte neben dem Inhalt. Eine RouterView mit einem Namen ist eine solche zweite Stelle. Eine Route füllt sie mit components statt component, einem Objekt mit einer Komponente für jeden Namen und default für die RouterView ohne Namen.
App.vue
<script setup>import { RouterLink, RouterView } from 'vue-router'</script>
<template> <nav> <RouterLink to="/">Katalog</RouterLink> <RouterLink to="/team">Team</RouterLink> </nav> <div class="columns"> <RouterView /> <!-- A second, named place, it stays empty when the route has nothing for it --> <RouterView name="aside" /> </div></template>
<style scoped>nav,.columns { display: flex; flex-wrap: wrap; gap: 1rem;}</style>CatalogView.vue
<template> <section> <h1>Katalog</h1> <p>60 Dinge zum Ausleihen.</p> </section></template>main.js
import { createApp } from 'vue'
import App from './App.vue'import router from './router.js'
createApp(App).use(router).mount('#app')router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import CatalogView from './CatalogView.vue'import TeamHints from './TeamHints.vue'import TeamLoansView from './TeamLoansView.vue'
export default createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: CatalogView }, // components instead of component, one component for every named view { path: '/team', components: { default: TeamLoansView, aside: TeamHints } }, ],})TeamHints.vue
<template> <aside class="hints"> <h2>Hinweise</h2> <p>Das Pfand kommt bei der Rückgabe zurück.</p> </aside></template>
<style scoped>.hints { padding: 0 1rem; border-left: 3px solid #1f6b4f;}</style>TeamLoansView.vue
<template> <section> <h1>Ausleihen</h1> <p>Zelt für vier Personen, zurück ab 12.11.2026</p> </section></template>Im Katalog bleibt die Spalte leer, denn seine Route hat nichts für sie. Benannte Views lohnen sich, wenn mehrere Routen dieselbe Seite mit verschiedenen Teilen füllen. Die Fallbeispiel-App braucht keine, die Knobelei zeigt sie trotzdem einmal im Einsatz.
Daten für die Seiten des Routers
Abschnitt betitelt „Daten für die Seiten des Routers“Bis Teil 5 gab App den Seiten ihre Daten als Props. Jetzt zeigt der Router die Seiten, und App steht nicht mehr im Template vor ihnen. In der Fallbeispiel-App stellt App darum den Katalog, die Favoriten und das angemeldete Mitglied mit provide bereit, wie das Mitglied in Kapitel 4.5, und jede Seite holt sich mit inject, was sie braucht. Zum Katalog gehört die Funktion markReturned, mit der die Seite für Rückgaben ein Ding als verfügbar markiert. Bis Teil 7 gilt das nur im eigenen Browser, dann geht jede Rückgabe an den Server des Vereins, und Pinia hält den gemeinsamen Zustand.
Im Code
Abschnitt betitelt „Im Code“Arbeite mit den Beispielen dieses Kapitels. Lies den Code und schreib auf, was du erwartest, bevor du etwas änderst.
- Öffne im Beispiel mit den Kindrouten die Rückgaben. Welche Links sind fett, welche unterstrichen, und warum?
- Nimm
RouterViewausTeamLayout. Was zeigt/teamjetzt? - Schreib den Pfad der Rückgaben als
/returnsund pass den Link im Layout an. Unter welcher Adresse erscheinen die Rückgaben, und erscheint das Layout noch? - Öffne im Beispiel mit den benannten Views abwechselnd den Katalog und das Team. Was geschieht mit der Spalte?
Aufgaben
Abschnitt betitelt „Aufgaben“Die Aufgaben bauen den Bereich des Teams aus dem Auftrag. Du findest das Projekt im Kursrepo im Ordner exercises/06-routing/nested-routes/de. Kopiere den Ordner, installiere die Pakete und starte die Tests.
npm installnpm testAufgabe Einstieg
Mach aus der flachen Seite TeamView das Layout TeamLayout mit der Überschrift «Team am Tresen» und einer RouterView. Verschieb die Liste der Ausleihen mit ihrer Überschrift in TeamLoansView. In src/router/index.js bekommt /team das Layout als Komponente und eine Kindroute mit leerem Pfad und dem Namen team-loans.
Lösung
{ path: '/team', // The layout fills the RouterView of App, the hints the RouterView named aside components: { default: TeamLayout, aside: TeamHints }, // The children appear in the RouterView of TeamLayout children: [ { path: '', name: 'team-loans', component: TeamLoansView }, { path: 'returns', name: 'team-returns', component: TeamReturnsView }, ], },Die Lösung enthält schon die zweite Kindroute und die Spalte aus der Knobelei. Für den Einstieg steht dort component: TeamLayout und nur die Kindroute mit dem leeren Pfad. TeamLayout trägt die Überschrift und eine RouterView, TeamLoansView die Liste.
Aufgabe Vertiefung
Ergänze die Kindroute returns mit dem Namen team-returns für TeamReturnsView und im Layout eine Navigation <nav class="team-nav"> mit Links zu «Ausleihen» und «Rückgaben». Der Link zur aktuellen Seite des Bereichs ist fett.
Lösung
<script setup>import { RouterLink, RouterView } from 'vue-router'</script>
<template> <h1>Team am Tresen</h1> <nav class="team-nav" aria-label="Bereich des Teams"> <RouterLink :to="{ name: 'team-loans' }">Ausleihen</RouterLink> <RouterLink :to="{ name: 'team-returns' }">Rückgaben</RouterLink> </nav> <!-- The page of the child route appears here, inside the layout --> <RouterView /></template>
<style scoped>.team-nav { display: flex; gap: 1rem;}/* Only the link to exactly this page, the link to the loans is active on every child */.team-nav a[aria-current='page'] { font-weight: 700;}</style>Der Selektor prüft aria-current, das nur der genau aktive Link hat. Mit router-link-active wäre der Link zu den Ausleihen auch auf den Rückgaben fett, weil er auf die leere Kindroute zeigt.
Aufgabe Knobelei
Neben allen Seiten des Teams soll TeamHints in einer eigenen Spalte stehen, im Katalog aber nicht. Ergänze in App.vue eine RouterView mit dem Namen aside und gib der Route /team die Hinweise als zweite Komponente.
Lösung
<div class="columns"> <main> <RouterView /> </main> <!-- A second, named place, empty when the route has nothing for it --> <RouterView name="aside" /> </div>In der Route ersetzt components: { default: TeamLayout, aside: TeamHints } die einzelne Komponente. Die Kindrouten erben die Spalte, weil sie zur Elternroute gehört. Die Route des Katalogs hat keine Komponente für aside, und die benannte RouterView bleibt dort leer.
Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
| Auf einer Kindroute erscheint nur das Layout. | Im Layout fehlt <RouterView />. |
Unter /team bleibt der Platz für die Seite leer. | Eine Kindroute mit leerem Pfad '' zeigt die Startseite des Bereichs. |
Eine Kindseite liegt unter /returns statt unter /team/returns. | Den Kindpfad ohne / am Anfang schreiben, sonst gilt er ab der Wurzel. |
| Der Link zur Startseite des Bereichs bleibt auf allen Seiten hervorgehoben. | In der Navigation des Bereichs router-link-exact-active oder aria-current gestalten. |
Ein KI-Assistent schaltet die Seiten des Bereichs mit v-if und einer Variablen um. | Mit Kindrouten hat jede Seite eine eigene Adresse, die man teilen und neu laden kann. |
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Wo erscheint die Komponente einer Kindroute?
- Wozu dient eine Kindroute mit leerem Pfad?
- Worin unterscheiden sich
router-link-activeundrouter-link-exact-active? - Woher bekommen die Seiten des Routers in der Fallbeispiel-App ihre Daten?
Antworten
- In der
RouterViewder Elternkomponente, also im Layout des Bereichs. - Sie zeigt die Startseite des Bereichs unter der Adresse der Elternroute selbst.
- Aktiv ist ein Link auch, wenn die aktuelle Route unter seiner liegt. Genau aktiv ist nur der Link zur aktuellen Seite, und nur er bekommt
aria-current="page". Appstellt Katalog, Favoriten und Mitglied mitprovidebereit, und jede Seite holt sie sich mitinject.
Auftrag
Der Bereich des Teams steht. Martina Hostettler meldet sich trotzdem. «Den Bereich kann im Moment jedes Mitglied öffnen. Und gestern hat ein Mitglied ein halb ausgefülltes Formular verloren, weil es auf einen Link geklickt hat.» Für beides gibt es Navigation Guards, das nächste Kapitel.
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.