Przejdź do treści
School IT / Przestrzeń do nauki

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.

Angular 22 Routes router-outlet routerLink routerLinkActive redirectTo ** Router.navigate 50 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zdefiniujesz trasy, które łączą adres z komponentem.
  • Wskażesz miejsce wyświetlania stron znacznikiem <router-outlet />.
  • Zbudujesz menu z routerLink i podświetlisz aktywną pozycję przez routerLinkActive.
  • 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.

TEORIA

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.

ElementKodRola
Trasy{ path: 'kontakt', component: Kontakt }który adres → który komponent; tablica typu Routes
Rejestracja trasprovideRouter(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
src/app/app.routes.ts — w zwykłym projekcie
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.

PRZYKŁAD 1

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.

src/app/nowa-34-1/start.ts — pełny plik
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 {}
src/app/nowa-34-1/wydarzenia.ts — pełny plik
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 {}
src/app/nowa-34-1/kontakt.ts — pełny plik
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 {}
src/app/nowa-34-1/trasy.ts — pełny plik
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 },
];
src/app/nowa-34-1/demo.ts — pełny plik
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
    selector: 'app-demo',
    imports: [RouterOutlet, RouterLink],
    templateUrl: './demo.html'
})
export class Demo {}
src/app/nowa-34-1/demo.html — pełny plik
<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>
po kliknięciu „Wydarzenia” — adres /nowa-34-1/wydarzenia
[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?

PRZYKŁAD 2

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.

src/app/nowa-34-2/trasy.ts — pełny plik
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 },
];
src/app/nowa-34-2/demo.ts — pełny plik
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 {}
src/app/nowa-34-2/demo.html — pełny plik
<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>
wynik
█ 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.)

PRZYKŁAD 3

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.

src/app/nowa-34-3/nie-znaleziono.ts — pełny plik
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 {}
src/app/nowa-34-3/trasy.ts — pełny plik
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 },
];
src/app/nowa-34-3/demo.ts — pełny plik
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 {}
src/app/nowa-34-3/demo.html — pełny plik
<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>
po kliknięciu „Galeria” — adres /nowa-34-3/galeria
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?

PRZYKŁAD 4

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.

src/app/nowa-34-4/zapis.ts — pełny plik
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 });
    }
}
src/app/nowa-34-4/dziekujemy.ts — pełny plik
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 {}
src/app/nowa-34-4/trasy.ts — pełny plik
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 },
];
src/app/nowa-34-4/demo.ts — pełny plik
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
    selector: 'app-demo',
    imports: [RouterOutlet],
    templateUrl: './demo.html'
})
export class Demo {}
src/app/nowa-34-4/demo.html — pełny plik
<div class="p-3">
  <div class="border rounded p-3" style="max-width: 30rem">
    <router-outlet />
  </div>
</div>
po wpisaniu nicku i Enter — adres …/dziekujemy
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?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenie
Routestyp 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
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
<router-outlet /> bez RouterOutlet w importsNG8001: ’router-outlet’ is not a known element.
routerLink bez RouterLink w importsBrak błędu kompilacji (to zwykły atrybut), ale kliknięcie nic nie robi. Zawsze sprawdź imports.
href="kontakt" zamiast routerLinkStrona 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 pathMatchW konsoli: NG04014: … please provide 'pathMatch’. Dopisz pathMatch: 'full'.
Trasa ** przed innymiKażdy adres pokazuje „nie znaleziono”. ** zawsze na końcu.
ZADANIA

Zadania

ZAD 1Czwarta strona★☆☆

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.

ZAD 2Stopka i nagłówek★☆☆

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 />.

ZAD 3Własna strona 404★★☆

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.

ZAD 4Przewodnik po szkole★★☆

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".

ZAD 5Quiz na trzech ekranach★★★

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.

ZAD 6Formularz w dwóch krokach★★★

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.

PODSUMOWANIE

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.
  • routerLink zmienia stronę bez przeładowania, a routerLinkActive podświetla aktywny link.
  • redirectTo z pathMatch: '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.

Postęp zapisuje się w tej przeglądarce.