Zum Inhalt springen

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:

  1. Unter /team stehen die Dinge, die ausgeliehen sind, die nächste Rückgabe zuerst.
  2. Unter /team/returns erfasst das Team eine Rückgabe, und der Katalog zeigt das Ding danach als verfügbar.
  3. Beide Seiten teilen eine Überschrift und eine Navigation, die zeigt, wo man ist.

Lernziele

Nach diesem Kapitel kannst du

  • Kindrouten mit children festlegen,
  • ein Layout mit einer eigenen RouterView bauen,
  • 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.

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

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

CatalogView.vue
<template>
<h1>Katalog</h1>
<p>60 Dinge zum Ausleihen.</p>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

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

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

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

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.

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 Routen der Fallbeispiel-App Ein Baum mit App und ihrer RouterView oben. Darunter die Routen / mit CatalogView, /items/:id mit ItemView, /favorites mit FavoritesView, /association mit AssociationView, /login mit LoginView, /team mit TeamLayout und die Catch-all-Route mit NotFoundView. TeamLayout hat eine eigene RouterView mit den Kindrouten für die Ausleihen und die Rückgaben. Neben /team steht, dass der Bereich nur für das Team ist und erst bei Bedarf geladen wird, neben der Catch-all-Route, dass sie alle anderen Adressen fängt. App RouterView / CatalogView /items/:id(\d+) ItemView /favorites FavoritesView /association AssociationView /login LoginView /team TeamLayout nur Team, lädt bei Bedarf '' TeamLoansView Ausleihen returns TeamReturnsView Rückgaben /:pathMatch(.*)* NotFoundView alle anderen Adressen

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.

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.

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

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

CatalogView.vue
<template>
<section>
<h1>Katalog</h1>
<p>60 Dinge zum Ausleihen.</p>
</section>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

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

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

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.

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.

Arbeite mit den Beispielen dieses Kapitels. Lies den Code und schreib auf, was du erwartest, bevor du etwas änderst.

  1. Öffne im Beispiel mit den Kindrouten die Rückgaben. Welche Links sind fett, welche unterstrichen, und warum?
  2. Nimm RouterView aus TeamLayout. Was zeigt /team jetzt?
  3. Schreib den Pfad der Rückgaben als /returns und pass den Link im Layout an. Unter welcher Adresse erscheinen die Rückgaben, und erscheint das Layout noch?
  4. Öffne im Beispiel mit den benannten Views abwechselnd den Katalog und das Team. Was geschieht mit der Spalte?

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.

Terminal-Fenster
npm install
npm test

Aufgabe 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
index.js
{
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
TeamLayout.vue
<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
App.vue
<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.

FehlerWas 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

  1. Wo erscheint die Komponente einer Kindroute?
  2. Wozu dient eine Kindroute mit leerem Pfad?
  3. Worin unterscheiden sich router-link-active und router-link-exact-active?
  4. Woher bekommen die Seiten des Routers in der Fallbeispiel-App ihre Daten?
Antworten
  1. In der RouterView der Elternkomponente, also im Layout des Bereichs.
  2. Sie zeigt die Startseite des Bereichs unter der Adresse der Elternroute selbst.
  3. 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".
  4. App stellt Katalog, Favoriten und Mitglied mit provide bereit, und jede Seite holt sie sich mit inject.

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.