Модуль 2 · урок 6 з 15
Роутинг
Роутер вирішує, який компонент показати під яку адресу — і робить це без перезавантаження
сторінки. Для тебе ця тема ближча, ніж здається: саме поведінка SPA-роутингу колись ламала твої
DAP-скрипти, коли сторінка «мінялась», а load більше не спрацьовував.
Конфіг маршрутів
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard').then(m => m.Dashboard),
title: 'Дашборд',
},
{
path: 'tasks',
loadComponent: () => import('./features/tasks/task-list').then(m => m.TaskList),
title: 'Задачі',
},
{
path: 'tasks/:id',
loadComponent: () => import('./features/tasks/task-detail').then(m => m.TaskDetail),
},
{ path: '**', loadComponent: () => import('./shared/not-found').then(m => m.NotFound) },
];
Чотири речі, які тут варто помітити:
loadComponent— компонент вантажиться окремим шматком бандла лише тоді, коли на цю адресу зайшли. Це стандарт: початкова сторінка лишається легкою.:id— параметр адреси. Все після двокрапки — змінна частина.title— Angular сам поставить заголовок вкладки. Дрібниця, яку помічають.'**'— має бути останнім: він ловить усе, що не збіглось раніше.
Куди рендериться компонент
<!-- app.html -->
<app-header />
<div class="layout">
<app-sidebar />
<main class="layout__main">
<router-outlet /> <!-- сюди роутер підставляє поточний компонент -->
</main>
</div>
Ось воно, те місце, де твоя розмітка з модуля 1 зустрічається з роутингом: каркас лишається
на місці, змінюється тільки вміст router-outlet. Саме тому в SPA не блимає екран
і не перезавантажуються стилі.
Дочірні маршрути й вкладений outlet
{
path: 'tasks',
loadComponent: () => import('./features/tasks/tasks-shell').then(m => m.TasksShell),
children: [
{ path: '', loadComponent: () => import('./features/tasks/task-list').then(m => m.TaskList) },
{ path: ':id', loadComponent: () => import('./features/tasks/task-detail').then(m => m.TaskDetail) },
],
}
У TasksShell буде свій <router-outlet />. Так роблять розділи
зі спільною шапкою або фільтрами: обгортка не перемальовується при переході між дітьми.
Читання параметрів
import { Component, inject, input } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({ /* … */ })
export class TaskDetail {
// найпростіший спосіб: параметр приходить як звичайний input
// (потрібен provideRouter(routes, withComponentInputBinding()) у app.config)
id = input.required<string>();
private router = inject(Router);
private route = inject(ActivatedRoute);
close(): void {
this.router.navigate(['/tasks']);
}
applyFilter(status: string): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { status },
queryParamsHandling: 'merge', // не затирати інші параметри
});
}
}
provideRouter(routes, withComponentInputBinding()) у app.config.ts —
і параметри адреси, query-параметри й дані маршруту приходять у компонент як звичайні
input(). Це прибирає підписку на route.paramMap, яку раніше писали в кожному
компоненті-деталі.
Навігація в шаблоні
<a routerLink="/tasks">Усі задачі</a>
<a [routerLink]="['/tasks', task.id]">{{ task.code }}</a>
<a [routerLink]="['/tasks']" [queryParams]="{ status: 'blocked' }">Заблоковані</a>
<a routerLink="/tasks" routerLinkActive="is-current"
[routerLinkActiveOptions]="{ exact: true }">Задачі</a>
routerLinkActive сам вішає клас на активне посилання — це те, що робить підсвітку
в бічному меню. Без exact: true посилання на /tasks лишатиметься активним
і на /tasks/42; інколи це саме те, що треба, інколи — ні.
routerLinkдля переходів усередині застосунку.loadComponentдля кожного екрана.- Стан фільтрів у query-параметрах — тоді посилання можна переслати колезі.
titleна маршруті.
<a href="/tasks">— це перезавантажить увесь застосунок.window.location.href = …замістьrouter.navigate.- Маршрут
'**'не в кінці масиву. - Фільтри лише в памʼяті компонента: після F5 усе злетить.
Дані маршруту й резолвери
{
path: 'admin',
data: { role: 'producer' }, // статичні дані — знадобляться guard'у
resolve: { stats: () => inject(StatsService).load() }, // дані ДО показу екрана
loadComponent: () => import('./features/admin/admin').then(m => m.Admin),
}
Резолвер завантажує дані перед тим, як показати компонент. Виглядає зручно, але має ціну: поки він працює, користувач дивиться на стару сторінку й не розуміє, чи щось узагалі відбувається. Тому в сучасних застосунках частіше показують екран одразу зі скелетоном (модуль 7), а резолвери лишають для випадків, коли без даних екран взагалі не має сенсу.