W tym artykule chciałbym skupić się i zgłębić temat nawigowania w Angularze. Jak wiemy, nawigacja w rozwiązaniach typu Single Page Applications (SPAs) różni się nieco od klasycznego podejścia.
Standardowo podczas nawigacji aplikacje wysyłały request do serwera, by ten zwrócił im odpowiedni szablon HTML. Działo się tak co każdą zmianę URLa w aplikacji.
SPA wyróżnia to, że przeglądarka dostaje od serwera tylko jedną stronę w postaci index.html. Jak więc rozwiązać problem nawigowania w takiej aplikacji?
Na szczęście nie musimy nad tym wiele główkować, ponieważ Angular ma dla nas gotowe rozwiązanie. Gdy użytkownik naszej aplikacji zmienia ścieżkę, client-side router przejmuje stery i aktualizuje to, co widzimy, bez przeładowywania całości strony.
Podejście deklaratywne- template’y html
Dyrektywa RouterLink jest deklaratywnym podejściem do nawigacji, które pozwala bez przeszkód używać standardowego anchor taga <a> w Angularze.
Jak używać routerLink?
Użycie naszej deklaratywnej metody jest proste, zastępujemy klasyczny atrybut href dyrektywą routerLink z odpowiednią ścieżką.
<!-- Standardowe podejście -->
<a href="https://angular.love/roadmap">Roadmap</a>
By wykorzystać tą metodę musimy jednak pamiętać o imporcie RouterLinka z biblioteki @angular/router
<!-- Deklaratywne podejście -->
import {RouterLink} from '@angular/router';
…
<a routerLink="roadmap">Roadmap</a>
W tym miejscu należy zaznaczyć, że routerLink nie służy do nawigacji zewnętrznej i działa tylko w obrębie routingu naszej Angularowej aplikacji.
Udogodnienia dyrektywy deklaratywnej
Dyrektywa ta ma wiele udogodnień. Jednym z najbardziej podstawowych jest oczywiście możliwość wykorzystania relatywnych URL’i. Pozwala to uniknąć sztywnych, statycznych linków, które powodują przeładowywanie strony i utratę state’u naszej aplikacji. Dzięki wykorzystaniu tego rozwiązania, nasza aplikacja jest elastyczna a my unikamy błędów przy dokonywaniu zmian środowiskowych. Kod będzie działał poprawnie niezależnie od tego jaki jest baseUrl aplikacji.
<!-- absolute url -->
<a href="https://www.angular.love/roadmap">Roadmap</a>
<!-- relative url -->
<a href="/roadmap">Roadmap</a>
W Angularze możemy zapisywać takie relatywne URL’e na dwa sposoby. Zarówno pierwszy jak i drugi jest poprawny.
<a routerLink="roadmap">Roadmap</a>
<a [routerLink]="['roadmap']">Roadmap</a>
Array jako parametr
Drugi sposób, w którym wykorzystujemy tablicę (array), pozwala nam na dynamiczne budowanie kolejnych części naszego URL’a. Segmenty możemy podawać po przecinku i mogą być one zarówno string’ami jak i number’ami. Najłatwiej sposobem na zrozumienie tego będzie spojrzenie na przykład poniżej.
<!-- przy założeniu, że id to string "router-link" po kliknięciu link mógłby wyglądał tak https://angular.love/roadmap/router-link -->
<a [routerLink]="['roadmap', id]">Roadmap</a>
Dodatkowo dostajemy możliwość wyboru czy nasza ścieżka ma być relatywna do naszego obecnego URL’a, w którym się znajdujemy, czy do root domain. Ponownie najlepiej zobaczyć przykład kodu, by w pełni to zrozumieć.
<!-- Zakładamy, że użytkownik znajduje się pod ścieżką /settings i chce przejść na /settings/notifications -->
<!-- Link relatywny -->
<a routerLink="notifications"> Notifications </a>
<!-- Link absolutny – działa niezależnie od aktualnej lokalizacji -->
<a routerLink="/settings/notifications"> Notifications </a>
<!-- Przykład routerLink wykorzystującego string -->
<a [routerLink]="'/settings' + '/notifications'">Notifications</a>
<!-- Przykład routerLink z wykorzystaniem tablicy -->
<a [routerLink]="['/settings', 'notifications']"> Notifications </a>
<!-- Statyczna ścieżka -->
<a routerLink="/team/123/user/456"> User 456</a>
<!-- Dynamiczne segmenty trasy -->
<a [routerLink]="['/team', teamId, 'user', userId]"> Current User </a>
Dodatkowe parametry – query params i fragmenty
Nasze linki mogą także nieść w sobie dodatkowe informacje w postaci parametrów zapytań (query params) oraz fragmentów. Query params zazwyczaj są przez nas wykorzystywane by zarządzać stanem strony lecz nie zmieniać jej struktury. Fragment czasem nazywany kotwicą jest wykorzystywany do precyzyjnej nawigacji do konkretnego elementu na stronie (po ID), zamiast tylko do samej ścieżki routingu. Spójrz na przykłady poniżej, które ułatwią poznanie tych dwóch zagadnień.
<!-- Po przejściu URL będzie wyglądał tak: /notifications?debug=true&message=new -->
<a routerLink="notifications" [queryParams]="{ debug: true, message: 'new' }">Notifications</a>
<!-- Po przejściu URL będzie wyglądał tak: /notifications#desktop -->
<a routerLink="notifications" fragment="desktop">Notifications</a>
Dodawanie klas do aktywnych linków – RouterLinkActive
W naszych aplikacjach bardzo często potrzebujemy wykrywać i dynamicznie ostylowywać linki odpowiadające ścieżce, w której aktualnie znajduje się użytkownik. Angular daje nam do tego gotową dyrektywę w postaci RouterLinkActive, którą oczywiście musimy zaimportować z biblioteki @angular/router.
Oprócz dodawania odpowiedniej klasy do aktywnego linku możemy zadbać w łatwy sposób o dostępność, dodając ariaCurrentWhenActive. Atrybut ten informuje użytkowników screen readerów i innych podobnych urządzeń o obecnej pozycji w nawigacji poprzez dodanie aria-current. Poniżej prosty przykład prosto z oficjalnej dokumentacji
<nav>
<a
class="button"
routerLink="/about"
routerLinkActive="active-button"
ariaCurrentWhenActive="page"
>
About
</a>
|
<a
class="button"
routerLink="/settings"
routerLinkActive="active-button"
ariaCurrentWhenActive="page"
>
Settings
</a>
</nav>
/* Aktywny link będzie miał tekst koloru czerwonego */
.active-button{
color: red;
}
Nie musimy jednak ograniczać się wyłącznie do jednej klasy, możemy dodać ich wiele do naszego linku poprzez zastosowanie tablicy.
<a routerLink="/user/bob" [routerLinkActive]="['class1', 'class2']">Bob</a>
Należy jednak pamiętać o tym, że RouterLinkActive będzie “aktywny” również wtedy, gdy adres URL jest dzieckiem lub dalszym członkiem linku. Chcąc pozbyć się tego zachowania, możemy dodać dyrektywę routerLinkActiveOptions z obiektem konfiguracyjnym zawierającym informację o konieczności dokładnego dopasowania. Domyślnie Angular dopasowuje ścieżki bez tej opcji, link /user byłby aktywny dla każdej podstrony zaczynającej się od /user.
<!-- Przy założeniu, że użytkownik znajduje się pod ścieżką:
/user/jane/role/admin -->
<!-- Będzie aktywny -->
<a [routerLink]="['/user/jane']" routerLinkActive="active-link"> User </a>
<!-- Będzie aktywny -->
<a [routerLink]="['/user/jane/role/admin']" routerLinkActive="active-link"> Role </a>
<!-- Nie będzie aktywny -->
<a
[routerLink]="['/user']"
routerLinkActive="active-link"
[routerLinkActiveOptions]="{ exact: true }"
>
User
</a>
Dyrektywa może być także przypisana do elementu nadrzędnego, co pozwoli na swobodne nadawanie stylów w wybranym przez nas miejscu. Znów posłużę się przykładem z Angular.dev
<div routerLinkActive="active-link" [routerLinkActiveOptions]="{exact: true}">
<a routerLink="/user/jim">Jim</a>
<a routerLink="/user/bob">Bob</a>
</div>
Nawigacja funkcyjna
Wcześniejsze deklaratywne podejście pozwala nam na obsługę nawigacji w szablonach HTML. Co jednak, gdy potrzebujemy nawigować użytkownika naszej aplikacji w zależności od jakiejś logiki bądź stanu w kodzie itp.? Pozwala nam na to Router, który oferuje szeroką gamę funkcji do kontrolowania tego zachowania w typescriptowym kodzie.
import {Router} from '@angular/router';
@Component({
…
})
export class AppDashboard {
private router = inject(Router);
…
}
Metoda navigate
Wstrzyknięcie Routera umożliwia nam przede wszystkim na proste nawigowanie, korzystając z metody navigate:
navigateToSettings(): void{
this.router.navigate([‘/settings’]);
}
Możemy oczywiście też podać do naszego routera parametry jak route, query lub matrix:
navigateToCategory(category: string): void {
// route parameters
this.router.navigate(['/category', category]);
// query parameters
this.router.navigate(['/category'], {
queryParams: { category: category, sort: 'quantity' },
});
// matrix parameters
this.router.navigate(['/category', { category: category, sort: 'quantity' }]);
}
RelativeTo
Możemy zauważyć, że router.navigate() pomaga nam w prostych i złożonych zagadnieniach, jednak to nie wszystko. Metoda ta umożliwia budowanie dynamicznych nawigacyjnych ścieżek relatywnych do lokalizacji naszego komponentu poprzez użycie klauzuli relativeTo. Znów pozwolę sobie wykorzystać przykład z oficjalnej dokumentacji, który w przejrzysty sposób pokazuje wykorzystanie wyżej wspomnianej klauzuli:
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user-detail',
template: `
<button (click)="navigateToEdit()">Edit User</button>
<button (click)="navigateToParent()">Back to List</button>
`,
})
export class UserDetail {
private route = inject(ActivatedRoute);
private router = inject(Router);
// do rodzeństwa
navigateToEdit() {
// z: /users/123
// do: /users/123/edit
this.router.navigate(['edit'], { relativeTo: this.route });
}
// do rodzica
navigateToParent() {
// z: /users/123
// do: /users
this.router.navigate(['..'], { relativeTo: this.route });
}
}
Przechodzenie do pełnej ścieżki
Zinjectowanie Routera pozwala nam również na wykorzystanie metody navigateByUrl(). Metoda ta umożliwia przejście do wskazanego adresu na podstawie pełnej ścieżki URL bez przekazywania jej jako tablicy. Przydaje się to w sytuacjach, w których dysponujemy kompletnym adresem URL na przykład przy deep linkach lub gdy ścieżka nawigacji pochodzi z zewnętrznego źródła.
router.navigateByUrl('/search?category=books&sortBy=price');
Kontrolowanie zachowania routera
Angular daje nam możliwość kontrolowania zachowania routera podczas przechodzenia między ścieżkami. Służy do tego interfejs NavigationBehaviorOptions, który jest gotowym zestawem narzędzi do wykorzystania w metodach router.navigate() lub router.navigateByUrl().
Pierwszą opcją do wykorzystania jest onSameUrlNavigation. Służy ona do konfiguracji zachowania, w którym użytkownik próbuje przejść na ten sam URL, na którym obecnie się znajduje. Domyślnie Angular nie robi wtedy nic , jednak z opcją ‘reload’ wymusi przejście przez cały proces nawigacji ponownie (np. Guardy, Resolvery). Opcja reload nie odświeża całego komponentu!
this.router.navigate(['stocks'], { onSameUrlNavigation: 'ignore' }); //domyślna opcja, nie zrobi nic jeżeli jesteśmy w tym samym URL’u
this.router.navigate(['stocks'], { onSameUrlNavigation: 'reload' }); //opcja, która pozwoli na przejście przez cały proces nawigacji ponownie
Drugą z nich jest skipLocationChange, która przyjmuje boolean jako argument. Gdy ustawimy tę opcję jako true, nawigacja przekieruje nas do nowej ścieżki, ale nie zapisze jej w historii przeglądarki. Nie zobaczymy też tego url’a na pasku w przeglądarce. Przykładem wykorzystania tej opcji może być sytuacja, w której np. po kliknięciu wstecz nie chcemy, żeby user wrócił do danej ścieżki.
this.router.navigate(['/stocks'], { skipLocationChange: true });
Metodą przeciwną – pozwalającą na nadpisanie obecnej ścieżki w historii jak i na pasku przeglądarki jest replaceUrl. Tak samo jak poprzednia, przyjmuje boolean jako argument. Prostym przykładem zastosowania opcji replaceUrl może być uniknięcie próby powrotu usera do strony logowania po zalogowaniu się.
this.router.navigate(['/stocks'], { replaceUrl: true });
Dynamiczne klasy dzięki isActive
Wiedzą, którą warto przyswoić w kontekście routera jest funkcja isActive. Funkcja ta zwraca booleana owiniętego w computed signal. Ten z kolei informuje nas o tym, czy obecny URL jest aktywny. Przykładowym wykorzystaniem takiego sygnału może być dodanie klasy w template HTML, tak jak miało to miejsce w RouterLinkActive. Dla rozjaśnienia załączam jak zwykle przydatny przykład z dokumentacji.
import {Component, inject} from '@angular/core';
import {isActive, Router} from '@angular/router';
@Component({
template: `
<div [class.active]="isSettingsActive()">
<h2>Settings</h2>
</div>
`,
})
export class Panel {
private router = inject(Router);
isSettingsActive = isActive('/settings', this.router, {
paths: 'subset',
queryParams: 'ignored',
fragment: 'ignored',
matrixParams: 'ignored',
});
}
UrlTree
Chcąc wyczerpać podstawowe tematy nawigowania, nie możemy zapomnieć o UrlTree. Jest to jedna z rzeczy, które dzieją się pod maską Angulara. Moglibyśmy pomyśleć, że URL jest traktowany jako płaski ciąg znaków, jednak jest to błędne myślenie. Adres jest rozbijany na strukturę drzewa, z której możemy wyróżnić segmenty, parametry i dzieci.
// /stocks?showDetails=true#section-top
const urlTree: UrlTree = this.router.parseUrl(this.router.url);
console.log(this.router.url);
console.log(urlTree);
Powyższy, krótki snippet kodu pomaga nam pokazać, jak faktycznie wygląda takie drzewo. Wyniki logów z konsoli możecie zobaczyć poniżej.

// create /team/33/user/11
router.createUrlTree(['/team', 33, 'user', 11]);
// create /team/33;expand=true/user/11
router.createUrlTree(['/team', 33, {expand: true}, 'user', 11]);
RouterLink oprócz przyjmowania tablicy segmentów, może także przyjąć nasz UrlTree. Funkcjonalność ta jest niezwykle przydatna, gdy chcemy wynieść logikę budowania adresu z szablonu HTML do klasy naszego komponentu.
<a [routerLink]="targetUrlTree">
Url Tree
</a>
Podsumowanie
Nawigacja jest fundamentem każdego projektu i znajomość konceptu nawigowania wykorzystywanego w naszym frameworku jest niezbędna do pisania dobrze działających i łatwych w utrzymaniu aplikacji.
W tym artykule przybliżyliśmy różne sposoby nawigowania i potomnych funkcji wykorzystywanych zarówno w szablonach HTML, jak i w kodzie Typescript.Mam nadzieję, że nauczyłeś się czegoś nowego, a Twoje projekty będą korzystały tylko z najlepszych praktyk nawigacji.