Router — wiele stron w jednej aplikacji
Strona klubu ma Start, Wydarzenia i Kontakt — każda pod własnym adresem, z menu, które zostaje na miejscu. W Angularze nie tworzysz do tego osobnych plików HTML: router podmienia komponent w środku strony i zmienia adres w pasku, bez przeładowania.
Czego się dziś nauczysz
- Zdefiniujesz trasy, które łączą adres z komponentem.
- Wskażesz miejsce wyświetlania stron znacznikiem
<router-outlet />. - Zbudujesz menu z
routerLinki podświetlisz aktywną pozycję przezrouterLinkActive. - Dodasz przekierowanie z pustego adresu i stronę „nie znaleziono”.
- Przejdziesz na inną stronę z poziomu kodu metodą
Router.navigate.
Przygotowanie: lekcja 02 (pliki app.routes.ts i app.config.ts), lekcja 24 (formularz), lekcja 33 (inject). Przewidywany czas: 50 min z zadaniami.
Jeden index.html, wiele stron
Aplikacja Angulara to jedna strona HTML. Gdy użytkownik klika „Kontakt”, przeglądarka nie pobiera nowego pliku — Angular podmienia tylko komponent w środku strony i zmienia adres w pasku. Przejścia są natychmiastowe, a każdy ekran ma własny adres, który można dodać do zakładek albo odświeżyć klawiszem F5.
| Element | Kod | Rola |
|---|---|---|
| Trasy | { path: 'kontakt', component: Kontakt } | który adres → który komponent; tablica typu Routes |
| Rejestracja tras | provideRouter(routes) | w app.config.ts — jest tam od ng new |
| Miejsce na stronę | <router-outlet /> | tu router wstawia komponent pasujący do adresu; reszta szablonu zostaje |
| Link | <a routerLink="kontakt"> | zmienia adres bez przeładowania strony |
import { Routes } from '@angular/router';
import { Start } from './start/start';
import { Kontakt } from './kontakt/kontakt';
export const routes: Routes = [
{ path: 'start', component: Start },
{ path: 'kontakt', component: Kontakt },
];
Trasy w laboratorium
W laboratorium każdy przykład ma już własny adres, np. /nowa-34-1. Dlatego trasy przykładu są trasami-dziećmi tego adresu: leżą w pliku trasy.ts przykładu, a w pliku z trasami laboratorium wpis wygląda tak:
{ path: 'nowa-34-1', component: Demo, children: trasy },
Z tego samego powodu linki piszemy bez ukośnika na początku (routerLink="kontakt") — liczą się względem adresu przykładu. W zwykłym projekcie trasy wpisujesz do app.routes.ts, linki możesz pisać z ukośnikiem (routerLink="/kontakt"), a <router-outlet /> stoi w app.html.
href czy routerLink?
Zwykłe href="kontakt" też otworzy stronę, ale przeładuje całą aplikację: strona mignie, a dane w sygnałach znikną. W aplikacji Angulara linki wewnętrzne zawsze piszesz przez routerLink.
Trzy strony i menu
Trzy proste komponenty-strony, plik z trasami i komponent Demo, który pełni rolę głównego komponentu aplikacji: ma menu i <router-outlet />. Strony mają szablon wpisany wprost we właściwości template, żeby nie mnożyć plików.
import { Component } from '@angular/core';
@Component({
selector: 'app-start',
imports: [],
template: `
<h3>Klub informatyczny ZSMI</h3>
<p>Spotykamy się w piątki w sali 12. Zapraszamy!</p>
`
})
export class Start {}
import { Component } from '@angular/core';
@Component({
selector: 'app-wydarzenia',
imports: [],
template: `
<h3>Wydarzenia</h3>
<ul>
<li>Turniej retro — piątek</li>
<li>Noc robotów — sobota</li>
<li>Warsztat druku 3D — środa</li>
</ul>
`
})
export class Wydarzenia {}
import { Component } from '@angular/core';
@Component({
selector: 'app-kontakt',
imports: [],
template: `
<h3>Kontakt</h3>
<p>Opiekun koła: sala 12, przerwa po 3. lekcji.</p>
`
})
export class Kontakt {}
import { Routes } from '@angular/router';
import { Start } from './start';
import { Wydarzenia } from './wydarzenia';
import { Kontakt } from './kontakt';
export const trasy: Routes = [
{ path: 'start', component: Start },
{ path: 'wydarzenia', component: Wydarzenia },
{ path: 'kontakt', component: Kontakt },
];
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-demo',
imports: [RouterOutlet, RouterLink],
templateUrl: './demo.html'
})
export class Demo {}
<div class="p-3">
<nav class="mb-3">
<a class="btn btn-outline-primary me-2" routerLink="start">Start</a>
<a class="btn btn-outline-primary me-2" routerLink="wydarzenia">Wydarzenia</a>
<a class="btn btn-outline-primary" routerLink="kontakt">Kontakt</a>
</nav>
<div class="border rounded p-3" style="max-width: 40rem">
<router-outlet />
</div>
</div>
[Start] [Wydarzenia] [Kontakt]
┌──────────────────────────────┐
│ Wydarzenia │
│ • Turniej retro — piątek │
│ • Noc robotów — sobota │
│ • Warsztat druku 3D — środa │
└──────────────────────────────┘
template: `…` to szablon wpisany w klasie, w odwróconych apostrofach (klawisz pod Esc). Przydaje się przy bardzo małych komponentach; przy dłuższych używaj templateUrl. Na starcie ramka jest pusta, bo adres /nowa-34-1 nie pasuje do żadnej trasy-dziecka. Przełączaj strony i używaj przycisku „wstecz” przeglądarki — działa jak na zwykłej stronie.
Zatrzymaj się
Menu jest w demo.html, poza <router-outlet />. Co by się stało, gdybyś przeniósł menu do komponentu Start?
Pasek Bootstrapa i aktywna pozycja
To samo z paskiem nawigacji Bootstrapa. Komponentów stron nie kopiujemy — trasy.ts importuje je z folderu przykładu 1 (../nowa-34-1/…). routerLinkActive="active" dokleja klasę active do linku, który odpowiada bieżącemu adresowi.
import { Routes } from '@angular/router';
import { Start } from '../nowa-34-1/start';
import { Wydarzenia } from '../nowa-34-1/wydarzenia';
import { Kontakt } from '../nowa-34-1/kontakt';
export const trasy: Routes = [
{ path: 'start', component: Start },
{ path: 'wydarzenia', component: Wydarzenia },
{ path: 'kontakt', component: Kontakt },
];
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-demo',
imports: [RouterOutlet, RouterLink, RouterLinkActive],
templateUrl: './demo.html'
})
export class Demo {}
<div class="p-3">
<nav class="navbar navbar-expand bg-dark rounded px-3 mb-3" style="max-width: 46rem" data-bs-theme="dark">
<span class="navbar-brand">Klub ZSMI</span>
<div class="navbar-nav">
<a class="nav-link" routerLink="start" routerLinkActive="active">Start</a>
<a class="nav-link" routerLink="wydarzenia" routerLinkActive="active">Wydarzenia</a>
<a class="nav-link" routerLink="kontakt" routerLinkActive="active">Kontakt</a>
</div>
</nav>
<div class="border rounded p-3" style="max-width: 46rem">
<router-outlet />
</div>
</div>
█ Klub ZSMI Start Wydarzenia [Kontakt] ← jaśniejsza = active
┌──────────────────────────────┐
│ Kontakt │
│ Opiekun koła: sala 12, … │
└──────────────────────────────┘
data-bs-theme="dark" przełącza pasek w ciemny motyw Bootstrapa — jasne napisy na ciemnym tle. Zajrzyj w F12: przy przejściu zmienia się tylko klasa active przy linkach i zawartość ramki.
Zatrzymaj się
RouterLinkActive wymaga osobnego importu, choć stoi obok routerLink. Usuń go z imports — co pokaże kompilator? (Podpowiedź: routerLinkActive="active" to dla HTML-a zwykły atrybut.)
Strona startowa i „nie znaleziono”
Dwie trasy, które ma prawie każda aplikacja: pusty adres przekierowuje na Start, a nieznany adres pokazuje komunikat zamiast pustej ramki. W menu jest celowo pozycja „Galeria”, dla której nie ma trasy.
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-nie-znaleziono',
imports: [RouterLink],
template: `
<h3 class="text-danger">Nie ma takiej strony</h3>
<p>Sprawdź adres albo wróć na <a routerLink="../start">stronę startową</a>.</p>
`
})
export class NieZnaleziono {}
import { Routes } from '@angular/router';
import { Start } from '../nowa-34-1/start';
import { Wydarzenia } from '../nowa-34-1/wydarzenia';
import { Kontakt } from '../nowa-34-1/kontakt';
import { NieZnaleziono } from './nie-znaleziono';
export const trasy: Routes = [
{ path: '', redirectTo: 'start', pathMatch: 'full' },
{ path: 'start', component: Start },
{ path: 'wydarzenia', component: Wydarzenia },
{ path: 'kontakt', component: Kontakt },
{ path: '**', component: NieZnaleziono },
];
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-demo',
imports: [RouterOutlet, RouterLink, RouterLinkActive],
templateUrl: './demo.html'
})
export class Demo {}
<div class="p-3">
<nav class="navbar navbar-expand bg-dark rounded px-3 mb-3" style="max-width: 46rem" data-bs-theme="dark">
<span class="navbar-brand">Klub ZSMI</span>
<div class="navbar-nav">
<a class="nav-link" routerLink="start" routerLinkActive="active">Start</a>
<a class="nav-link" routerLink="wydarzenia" routerLinkActive="active">Wydarzenia</a>
<a class="nav-link" routerLink="kontakt" routerLinkActive="active">Kontakt</a>
<a class="nav-link" routerLink="galeria" routerLinkActive="active">Galeria</a>
</div>
</nav>
<div class="border rounded p-3" style="max-width: 46rem">
<router-outlet />
</div>
</div>
Nie ma takiej strony (czerwony nagłówek)
Sprawdź adres albo wróć na stronę startową.
pathMatch: 'full' oznacza: „przekieruj tylko wtedy, gdy adres jest dokładnie pusty”. ** to „cokolwiek” — dlatego stoi na końcu: router sprawdza trasy od góry i bierze pierwszą pasującą. Link ../start prowadzi „piętro wyżej” od nieznanej strony, tak jak .. w ścieżkach folderów.
Zatrzymaj się
Przenieś trasę ** na początek tablicy. Co zobaczysz po kliknięciu dowolnej pozycji menu?
Przejście do strony z poziomu kodu
Nie każde przejście to kliknięcie w link. Po wysłaniu formularza zapisu chcemy pokazać stronę z podziękowaniem. Robi to metoda navigate obiektu Router, pobranego przez inject — tak jak serwis w lekcji 33.
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-zapis',
imports: [FormsModule],
template: `
<h3>Zapisz się na turniej</h3>
<form (ngSubmit)="wyslij()">
<input name="nick" class="form-control mb-2" placeholder="Nick" [(ngModel)]="nick">
<button class="btn btn-primary" type="submit" [disabled]="nick().trim() === ''">Zapisz</button>
</form>
`
})
export class Zapis {
private readonly router = inject(Router);
private readonly trasa = inject(ActivatedRoute);
readonly nick = signal('');
wyslij(): void {
if (this.nick().trim() === '') {
return;
}
this.router.navigate(['../dziekujemy'], { relativeTo: this.trasa });
}
}
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-dziekujemy',
imports: [RouterLink],
template: `
<h3 class="text-success">Dziękujemy za zapis!</h3>
<a class="btn btn-outline-secondary" routerLink="../zapis">Zapisz kolejną osobę</a>
`
})
export class Dziekujemy {}
import { Routes } from '@angular/router';
import { Zapis } from './zapis';
import { Dziekujemy } from './dziekujemy';
export const trasy: Routes = [
{ path: '', redirectTo: 'zapis', pathMatch: 'full' },
{ path: 'zapis', component: Zapis },
{ path: 'dziekujemy', component: Dziekujemy },
];
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-demo',
imports: [RouterOutlet],
templateUrl: './demo.html'
})
export class Demo {}
<div class="p-3">
<div class="border rounded p-3" style="max-width: 30rem">
<router-outlet />
</div>
</div>
Dziękujemy za zapis!
[Zapisz kolejną osobę]
ActivatedRoute to „trasa, na której jestem”. Opcja relativeTo sprawia, że ../dziekujemy liczy się od bieżącej strony, tak jak link. W zwykłym projekcie z trasami w app.routes.ts wystarczy this.router.navigate(['/dziekujemy']). Przycisk „wstecz” przeglądarki wraca do formularza, bo navigate dopisuje wpis do historii.
Zatrzymaj się
Pola router i trasa są private, a nick nie. Które pola są potrzebne szablonowi, a które tylko kodowi klasy?
Zestawienie elementów
| Element | Znaczenie |
|---|---|
Routes | typ tablicy tras (z @angular/router) |
{ path: 'a', component: A } | adres a pokazuje komponent A; ścieżka bez ukośnika |
{ path: '', redirectTo: 'start', pathMatch: 'full' } | przekierowanie z pustego adresu |
{ path: '**', component: NieZnaleziono } | każdy inny adres; zawsze ostatnia |
RouterOutlet → <router-outlet /> | miejsce na komponent strony |
RouterLink → routerLink="a" | link bez przeładowania; ../a — piętro wyżej |
RouterLinkActive → routerLinkActive="active" | klasa dla linku bieżącej strony |
inject(Router).navigate(['/a']) | przejście z kodu |
inject(ActivatedRoute) | bieżąca trasa; z relativeTo do adresów względnych |
template: `…` | szablon wpisany w klasie komponentu zamiast templateUrl |
Zanim utkniesz
| Zapis | Problem |
|---|---|
<router-outlet /> bez RouterOutlet w imports | NG8001: ’router-outlet’ is not a known element. |
routerLink bez RouterLink w imports | Brak błędu kompilacji (to zwykły atrybut), ale kliknięcie nic nie robi. Zawsze sprawdź imports. |
href="kontakt" zamiast routerLink | Strona przeładowuje się całkowicie: miga, a dane w sygnałach znikają. |
{ path: '/kontakt', … } | W konsoli: NG04014: Invalid configuration of route '/kontakt’: path cannot start with a slash. |
{ path: '', redirectTo: 'start' } bez pathMatch | W konsoli: NG04014: … please provide 'pathMatch’. Dopisz pathMatch: 'full'. |
Trasa ** przed innymi | Każdy adres pokazuje „nie znaleziono”. ** zawsze na końcu. |
Zadania
Dodaj do przykładu 2 stronę „Galeria” z trzema obrazkami z lekcji 07 i pozycję w menu.
Kryterium sukcesu: nowa pozycja menu podświetla się i pokazuje galerię pod własnym adresem.
Podpowiedź
Trzy zmiany: nowy komponent, nowa trasa w trasy.ts, nowy link w demo.html.
Rozbuduj przykład 3: pod ramką dodaj stopkę z rokiem i linkami „Start” i „Kontakt”, nad menu — nagłówek z nazwą klubu.
Kryterium sukcesu: przechodzenie między stronami zmienia tylko zawartość ramki; linki w stopce działają.
Podpowiedź
Wszystko, co ma zostać, umieść w demo.html poza <router-outlet />.
Zaprojektuj stronę „nie znaleziono” z dużym napisem 404, krótkim komunikatem i dwoma przyciskami: „Strona główna” (link) i „Wróć” (przycisk, który z kodu przechodzi na Start metodą navigate). Dodaj licznik, ile razy w tej sesji ktoś trafił na nieistniejącą stronę — trzymany w serwisie.
Kryterium sukcesu: dowolny błędny adres pokazuje stronę 404; licznik rośnie przy każdym wejściu na nią i nie zeruje się przy przejściach.
Podpowiedź
Serwis z lekcji 33 z sygnałem i metodą zwieksz() wywołaną w konstruktorze komponentu 404.
Aplikacja z czterema stronami: „Pracownie”, „Biblioteka”, „Sala gimnastyczna”, „Sekretariat”. Pusty adres przekierowuje na „Pracownie”. Każda strona ma przyciski „← poprzednia” i „następna →” (linki routerLink) prowadzące do sąsiednich stron.
Kryterium sukcesu: można przejść całą szkołę samymi przyciskami; menu na górze zawsze pokazuje aktywną stronę.
Podpowiedź
Linki do sąsiednich stron z innego komponentu-strony: routerLink="../biblioteka".
Aplikacja: ekran Start z przyciskiem „Rozpocznij”, ekran z pytaniem (radio z trzema odpowiedziami) i ekran wyniku. Przejścia robi router.navigate. Odpowiedź przechowuj w serwisie, żeby ekran wyniku mógł ją odczytać.
Kryterium sukcesu: każdy ekran ma własny adres; ekran wyniku pokazuje, czy odpowiedź była poprawna; z wyniku można wrócić na start.
Podpowiedź
Serwis z sygnałem odpowiedz. Komponent pytania zapisuje do serwisu i nawiguje, komponent wyniku tylko czyta.
Rejestracja na wycieczkę w dwóch krokach: krok-1 (imię i klasa) i krok-2 (wybór wycieczki z listy i zgoda rodzica), potem strona podsumowanie. Dane z obu kroków trzymaj w serwisie. Przycisk „Dalej” w kroku 1 jest aktywny tylko przy poprawnych danych; z kroku 2 można wrócić do kroku 1 bez utraty wpisanych danych.
Kryterium sukcesu: dane przechodzą przez kroki i trafiają do podsumowania; powrót do kroku 1 pokazuje wcześniej wpisane wartości.
Podpowiedź
Pola formularzy łącz z sygnałami serwisu: [(ngModel)]="dane.imie", gdzie dane = inject(Rejestracja). Komponent kroku znika przy przejściu, serwis — nie.
Co trzeba zapamiętać
- Trasy łączą adres z komponentem:
{ path: 'kontakt', component: Kontakt }; ścieżka bez ukośnika. <router-outlet />to miejsce, w którym router wyświetla stronę; reszta szablonu zostaje.routerLinkzmienia stronę bez przeładowania, arouterLinkActivepodświetla aktywny link.redirectTozpathMatch: 'full'na początku,**na końcu tablicy.- Przejście z kodu:
inject(Router).navigate([…]).
Sprawdź się: użytkownik wpisuje w pasku adres strony, której nie ma. Co zobaczy w aplikacji bez trasy **, a co z nią?
Dokumentacja: angular.dev — Routing.