Angular IDPStrong Junior
🔥 0

Модуль 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',   // не затирати інші параметри
    });
  }
}
Прив'язка параметрів до input — вмикається одним рядком

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), а резолвери лишають для випадків, коли без даних екран взагалі не має сенсу.

≈ 50 хв · +25 XP за урок