Маршрутизація

Ми отримали нові вимоги для Туру Героїв:

Коли все зробимо, у користувачів буде наступна навігація (тут Heroes - це Герої, Dashboard - це Панель Керування ):

Огляд навігації

Щоб дотримуватись поставлених вимог, додамо Angular-маршрутизатор у застосунок.

У розділі Маршрутизація та Навігація маршрутизатор розглядається більш детально, ніж це зроблено на поточній сторінці.

Запустіть живий приклад для цієї частини.

Щоб у живому прикладі спостерігати за зміною URL у адресному рядку браузера, відкрийте його знову у редакторі Plunker клацаючи на іконці вгорі праворуч, потім розкрийте вікно передогляду клацнувши синю кнопку 'X' у верхньому правому куті.

pop out the window
pop out the window

На чому ми зупинились

Перед тим як продовжити наш Тур Героїв, давайте перевіримо, чи маємо ми наступну структуру після додавання сервісів героя та його компонента. Якщо ні, потрібно повернутись та пройти кроки з попередньої частини.

angular-tour-of-heroes
app
app.component.ts
app.module.ts
hero.service.ts
hero.ts
hero-detail.component.ts
main.ts
mock-heroes.ts
node_modules ...
index.html
package.json
styles.css
systemjs.config.js
tsconfig.json

Забезпечте запуск та транспіляцію застосунка

Відкрийте вікно терміналу/консолі. Ми хочемо запустити компілятор TypeScript, запустити веб-сервер та мати нагляд за зміною файлів. Все це забезпечить наступна команда:

npm start

Таким чином застосунок працюватиме, коли ми продовжимо будувати Тур Героїв.

План дій

Ось наш план:

Маршрутизація — це те са́ме, що і навігація. Маршрутизатор — це механізм для навігації між різними візуальними поданнями.

Розділимо AppComponent

Наш поточний застосунок завантажує AppComponent і зразу ж відображає список героїв.

Наш переписаний застосунок повинен представляти собою оболонку з вибором візуальних подань (Dashboard та Heroes) і початково перекидати до одного з них.

AppComponent повинен забезпечувати лише навігацію. Давайте перенесемо функцію показу Героїв із AppComponent у його власний файл HeroesComponent.

HeroesComponent

AppComponent по-суті зараз забезпечує показ Героїв. Замість перенесення будь-чого з AppComponent, ми його просто перейменуємо на HeroesComponent та створимо новий файл для AppComponent.

Що будемо перейменовувати:

app/heroes.component.ts (показ того, що перейменували)

@Component({ selector: 'my-heroes', }) export class HeroesComponent implements OnInit { }

Створення AppComponent

Новий AppComponent буде оболонкою застосунка. Він матиме декілька навігаційних лінків вгорі, а внизу показуватиме ті сторінки, на які ми переходимо.

Перші кроки:

Наш перший чорновик має наступний вигляд:

import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <my-heroes></my-heroes> ` }) export class AppComponent { title = 'Tour of Heroes'; } import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroesComponent } from './heroes.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent, HeroesComponent ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { }

Застосунок все ще працює та показує героїв. Після рефакторингу AppComponent тепер запрацював і HeroesComponent! Ми нічого не зламали.

Додамо маршрутизацію

Ми готові зробити наступний крок. Замість автоматичного відображення героїв, нам би хотілось їх показувати після клацання користувачем певної конопки.

Нам потрібен Angular-маршрутизатор.

Angular-маршрутизатор є зовнішнім, опціональним Angular-модулем, що має назву RouterModule. Маршрутизатор є комбінацією декількох сервісів (в нашому випадку це RouterModule), декількох директив (в нашому випадку це RouterOutlet, RouterLink, RouterLinkActive), та конфігурації (в нашому випадку це Routes). Спочатку налаштуємо маршрути.

Додамо тег base

Відкриємо index.html та додамо <base href="/"> вгорі у секції <head>.

index.html (base-href)

<head> <base href="/">
Важливість base href

Ознайомтесь з частиною base href у розділі Маршрутизація та Навігація щоб зрозуміти чому це має значення.

Налаштування маршрутів

Наш застосунок покищо немає маршрутів. Почнемо зі створення конфігурації для цих маршрутів.

Маршрути говорять маршрутизатору яке із візуальних подань показувати, коли користувач клацне лінк або вставить URL в адресний рядок браузера.

Давайте визначимо наш перший маршрут для компонента героїв:

app/app.module.ts (маршрут героїв)

import { RouterModule } from '@angular/router'; RouterModule.forRoot([ { path: 'heroes', component: HeroesComponent } ])

В даний момент ми маємо лише одне визначення маршруту, але незабаром додамо їх більше.

Такі визначення маршрутів мають наступні частини:

Дізнайтесь більше про визначення маршрутів у розділі Маршрутизація та Навігація.

Зробимо маршрутизатор доступним

Ми встановили початкову конфігурацію маршруту. Тепер додамо конфігурацію RouterModule до масиву imports у AppModule.

app/app.module.ts (маршрутизація застосунка)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroesComponent } from './heroes.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot([ { path: 'heroes', component: HeroesComponent } ]) ], declarations: [ AppComponent, HeroDetailComponent, HeroesComponent ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { }

Ми використовуємо метод forRoot, оскільки сконфігурований маршрутизатор розміщено у корені застосунка. Метод forRoot дає нам провайдерів сервісу Router з директивами, потрібними для маршрутизації, та виконує початковий перехід, базуючись на поточному URL у браузері.

Вікно маршрутизатора

Якщо ми вставимо шлях /heroes у адресний рядок браузера, маршрутизатор повинен віднести його до маршруту heroes та показати HeroesComponent. Але де?

Ми повинні сказати йому де саме, додавши елемент <router-outlet> внизу шаблона. RouterOutlet є однією з директив наданих RouterModule. Маршрутизатор показує кожен компонент зразу під <router-outlet>, коли ми переходимо між різними сторінками застосунка.

Насправді ми не очікуємо вставки URL маршруту в адресний рядок браузера. Ми додамо тег лінка до шаблону, який, при клацанні, спричиняє перехід до HeroesComponent.

Змінений шаблон має наступний вигляд:

app/app.component.ts (template-v2)

template: ` <h1>{{title}}</h1> <a routerLink="/heroes">Heroes</a> <router-outlet></router-outlet> `

Зверніть увагу, що прив'язка routerLink знаходиться в межах тегу лінка <a>. Ми прив'язуємо директиву RouterLink (іншу директиву RouterModule) до рядка, який говорить маршрутизатору куди переходити, коли користувач клацне лінк.

Оскільки наш лінк не динамічний, ми визначили інструкції маршрутизації за допомогою одноразової прив'язки до нашого маршруту path. Дивлячись знову на конфігурацію маршруту, ми пересвідчуємось, що '/heroes' є шляхом маршруту до HeroesComponent.

Дізнайтесь більше про лінки динамічного маршрутизатора та масив параметрів лінка у розділі Маршрутизація та Навігація.

Оновилась сторінка браузера, ми бачимо лише заголовок застосунка та лінк Heroes, ми не бачимо списку героїв.

В адресному рядку браузера показується /. Шлях маршрута HeroesComponent ми визначили як /heroes, а не /. Ми не маємо маршрута, який відповідає за шлях /, отже тут немає що показувати. Це те, що ми хочемо виправити.

Ми клацаємо навігаційний лінк Heroes, адресний рядок браузера оновлюється до /heroes, і зараз ми бачимо список героїв. Нарешті ми до нього дійшли!

В даний момент, AppComponent має наступний вигляд:

app/app.component.ts (v2)

import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <a routerLink="/heroes">Heroes</a> <router-outlet></router-outlet> ` }) export class AppComponent { title = 'Tour of Heroes'; }

AppComponent зараз закріплено за маршрутизатором та за показом візуального подання маршруту. По цій причині, та щоб розрізняти його від інших типів компонентів, ми називаємо цей тип компонентів як Компонент Маршрутизатора.

Додамо Панелі Керування

Маршрутизація має сенс лише коли ми маємо декілька візуальних подань. Нам потрібне інше візуальне подання.

Створіть заготовку DashboardComponent, яка дасть нам можливість переходити звідкись та кудись.

app/dashboard.component.ts (v1)

import { Component } from '@angular/core'; @Component({ selector: 'my-dashboard', template: '<h3>My Dashboard</h3>' }) export class DashboardComponent { }

Ми допишемо та зробимо кориснішим цей клас пізніше.

Додамо конфігурацію до маршуту для лінка Панель Керування

Повернемось до app.module.ts та навчимо його переходити по лінку Панель Керування (Dashboard).

Імпортуємо компонент для Панелі Керування та додамо наступне визначення маршруту до масиву визначень.

app/app.module.ts (маршрут для Панелі Керування)

{ path: 'dashboard', component: DashboardComponent },

Також імпортуємо та додамо DashboardComponent до нашого масиву declarations у AppModule.

app/app.module.ts (dashboard)

declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent ],

Переспрямування

Ми хочемо щоб застосунок показував Панель Керування, коли він стартує, також ми хочемо бачити гарний URL у адресному рядку браузера, скажімо — /dashboard. Пам'ятайте, що браузер починає з адреси /.

Ми можемо використовувати спеціальний маршрут переспрямування. Додайте наступне до масива визначень маршрутів:

app/app.module.ts (redirect)

{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },

Дізнайтесь більше про переспрямування у розділі Маршрутизація та Навігація.

Додамо навігацію до шаблона

Нарешті, додамо навігаційний лінк Dashboard (Панель керування) до шаблона, прямо над лінком Heroes.

app/app.component.ts (template-v3)

template: ` <h1>{{title}}</h1> <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/heroes">Heroes</a> </nav> <router-outlet></router-outlet> `

Ми вклали два лінки всередині тегів <nav>. Вони покищо нічого не роблять, але вони будуть корисними трохи пізніше, коли ми додаватимемо стилі лінкам.

Щоб побачити ці зміни у вашому браузері, перейдіть у корінь застосунка (/) та перзавантажте сторінку. Застосунок показує Панель Керування і ми можемо переходити між панеллю керування та героями.

Панель керування з найкращими героями

Давайте підправимо панель керування, щоб зразу показувались перші чотири героя.

Замініть у метаданих властивість template на templateUrl, яка вказуватиме на новий файл шаблона.

Для властивості moduleId встановіть значення module.id щоб у даному компоненті можна було указувати відносні шляхи до модуля при завантаженні templateUrl.

app/dashboard.component.ts (metadata)

@Component({ moduleId: module.id, selector: 'my-dashboard', templateUrl: './dashboard.component.html', })

Створимо файл з наступним вмістом:

app/dashboard.component.html

<h3>Top Heroes</h3> <div class="grid grid-pad"> <div *ngFor="let hero of heroes" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </div> </div>

Ми знову використовуємо *ngFor для перебору списку героїв та показу їхніх імен. Ми використали додатковий елемент <div> щоб спростити роботу зі стилями пізніше.

Поширення HeroService

Ми б хотіли повторно використовувати HeroService для поширення масиву heroes серед компонентів.

Нагадаємо, що раніше у цьому розділі ми видалили HeroService із масиву providers у HeroesComponent, та додали його у масив providers, що знаходиться у AppModule.

Це переміщення дозволяє нам створювати єдиний примірник (англ. singleton) HeroService, який стає доступним для усіх компонентів у застосунку. Angular буде впроваджувати HeroService так, що ми зможемо використовувати його у DashboardComponent.

Отримаємо героїв

Відкриємо dashboard.component.ts та додамо необхідний вираз import.

app/dashboard.component.ts (imports)

import { Component, OnInit } from '@angular/core'; import { Hero } from './hero'; import { HeroService } from './hero.service';

Тепер допишемо нашу заготовку класу DashboardComponent наступним чином:

app/dashboard.component.ts (class)

export class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } ngOnInit(): void { this.heroService.getHeroes() .then(heroes => this.heroes = heroes.slice(1, 5)); } }

Ми вже бачили подібну логіку раніше у HeroesComponent:

На цій Панелі Керування ми вибрали чотири героя (2го, 3го, 4го, та 5го) за допомогою методу Array.slice.

Після перезавантаження браузера, бачимо чорити героя на новій Панелі Керування.

Перехід до детальної інформації про Героя

Хоча ми показуємо деталі про вибраного героя внизу HeroesComponent, покищо ми не можемо переходити до HeroDetailComponent у трьох випадках, зазначених у наших вимогах:

  1. із Панелі Керування до вибраного героя.
  2. зі списку Heroes до вибраного героя.
  3. з "глибокого лінка" URL, вставленого у адресний рядок браузера.

Додавання маршруту для детальної інформації про героя — здається, з цього варто починати.

Маршрутизація для переходу до деталей про героя

Ми додамо маршрут до HeroDetailComponent у app.module.ts, де сконфігуровано інші наші маршрути.

Новий маршрут є трохи незвичним у тому, що ми повинні сказати HeroDetailComponent якого з героїв показувати. Ми не повинні будь-що говорити HeroesComponent чи DashboardComponent.

В даний момент, батьківський HeroesComponent встановлює властивість hero компонента у об'єкт героя з наступною прив'язкою:

<my-hero-detail [hero]="selectedHero"></my-hero-detail>

Це явно не буде працювати у будь-якому нашому сценарії маршрутизації. Хоча, звичайно ж — не враховуючи наших наступних сценаріїв. Ми не можемо вставити увесь об'єкт героя в URL! Ми б цього не хотіли.

Параметризований маршрут

Ми можемо додати id героя до URL. Коли переходимо до героя, з id рівному 11, ми мабуть очікуємо побачити ось такий URL:

/detail/11

Частина /detail/ цього URL є незмінною. Номер id у цій частині змінюється при переходах між різними героями. Нам потрібно представти цю змінну частину маршуту через параметр (чи токен), який залишатиметься на місці id.

Конфігурація маршуту з параметром

Ми використовуємо наступне визначення маршруту:

app/app.module.ts (hero detail)

{ path: 'detail/:id', component: HeroDetailComponent },

Тут двокрапка (:) у path показує, що :id є місцем, куди підставляється конкретний id героя, при переході до HeroDetailComponent.

Ми завершили з маршрутами застосунка.

Ми не додаватимемо лінк 'Hero Detail' до цього шаблона, оскільки користувачі не будуть клацати навігаційний лінк щоб побачити конкретного героя. Вони клацатимуть на герої, де цього героя буде показано: на Панелі Керування або у списку героїв.

Повернемось до тих героїв, на яких клацають, пізніше. Залишимо їх допоки HeroDetailComponent не буде готовий щоб до нього переходили.

Це потребуватиме повного переписування HeroDetailComponent.

Переглянемо HeroDetailComponent

Перед тим, як писати HeroDetailComponent, давайте переглянемо як він виглядає зараз:

app/hero-detail.component.ts (теперішній вигляд)

import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: 'my-hero-detail', template: ` <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div> <label>id: </label>{{hero.id}} </div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> </div> ` }) export class HeroDetailComponent { @Input() hero: Hero; }

Шаблон не будемо змінювати, героя показуватимемо в той самий спосіб, великі зміни торкнуться лише способу отримання героя.

Ми більше не отримуємо героя через прив'язку властивості у батьківському компоненті. Новий HeroDetailComponent повинен приймати параметр id із params у сервісі ActivatedRoute та використовувати HeroService щоб отримувати героя з таким id.

Спочатку, додамо необхідні імпорти:

// Keep the Input import for now, we'll remove it later: import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Location } from '@angular/common'; import { HeroService } from './hero.service';

Тепер отримаємо сервіси ActivatedRoute, HeroService та Location, впровадивши їх у конструкторі та зберігаючи їхні значення у приватних полях:

app/hero-detail.component.ts (конструктор)

constructor( private heroService: HeroService, private route: ActivatedRoute, private location: Location ) {}

Також імпортуємо оператор switchMap щоб використовувати його пізніше з параметрами Observable маршруту.

app/hero-detail.component.ts (імпорт оператора switchMap)

import 'rxjs/add/operator/switchMap';

Ми говоримо класу, що хочемо впровадити інтерфейс OnInit.

export class HeroDetailComponent implements OnInit {

Всередині хуку ngOnInit, ми використовуємо спостережуваний (англ. observable) params, щоб отримувати значення параметра id із сервісу ActivatedRoute, а також використовуємо HeroService щоб отримувати героя з таким id.

app/hero-detail.component.ts (ngOnInit)

ngOnInit(): void { this.route.params .switchMap((params: Params) => this.heroService.getHero(+params['id'])) .subscribe(hero => this.hero = hero); }

Зверніть увагу як оператор switchMap передає id із params маршрута до методу HeroService.getHero.

Якщо користувач повторно викличе цей компонент, коли запит getHero ще не буде завершено, switchMap скасує старий запит перед повторним викликом HeroService.getHero.

id героя є числом. Параметри маршрутів завжди мають рядковий тип. Отже, ми будемо конвертувати значення параметрів маршруту до числового типу за допомогою JavaScript-оператора "+" (знаку плюс).

Чи повинен я відписуватись?

Router керується ActivatedRoute, він надає та локалізує підписки. Ці підписки скасовуються, коли компонент видаляється, запобігаючи витоку пам'яті, отже нам не потрібно відписуватись від параметрів Observable маршруту.

Додамо HeroService.getHero

Проблема з цим фрагментом коду в тому, що HeroService не має методу getHero! Ми краще полагодимо це швиденько, поки хтось не побачив, що ми поламали усе.

Відкриємо HeroService та додамо метод getHero, який фільтрує список героїв з getHeroes по id:

app/hero.service.ts (getHero)

getHero(id: number): Promise<Hero> { return this.getHeroes() .then(heroes => heroes.find(hero => hero.id === id)); }

Давайте повернимось до HeroDetailComponent щоб виправити невірний код.

Пошук способу повертатись назад

Ми можемо перейти до HeroDetailComponent в декілька способів.

Користувач може клацнути один із двох лінків у AppComponent, або кнопку "назад" у браузері. Додамо третю опцію — метод goBack, який відступатиме назад на один крок в історії браузера, використовуючи сервіс Location, впроваджений нами раніше.

app/hero-detail.component.ts (goBack)

goBack(): void { this.location.back(); }

Прохід далеко назад може повернути нас за межі нашого застосунку. Це прийнятно для демо, але в реальному застосунку треба уникати цього, можливо за допомогою охорони CanDeactivate.

Потім ми підключимо цей метод через прив'язку подій до кнопки Back, яку ми додамо внизу шаблона цього компонента.

<button (click)="goBack()">Back</button>

Зміна шаблона, для додавання кнопки, спонукає нас зробити ще один крок покращення та винести шаблон в окремий файл з назвою hero-detail.component.html:

app/hero-detail.component.html

<div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div> <label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name" /> </div> <button (click)="goBack()">Back</button> </div>

Оновимо метадані компонента змінивши moduleId та templateUrl вказавши їм на той файл, що ми щойно створили.

app/hero-detail.component.ts (metadata)

@Component({ moduleId: module.id, selector: 'my-hero-detail', templateUrl: './hero-detail.component.html', })

Після перезавантаження сторінки, бачимо результат.

Перехід до Панелі Керування героя

Коли ми вибираємо героя на Панелі Керування, застосунок повинен перекидати нас до HeroDetailComponent для огляду та редагування вибраного героя.

Хоча на Панелі Керування герої представлені у вигляді блоків, схожих на кнопки, вони повинні поводити себе як теги лінків. При наведенні на блоки героїв, цільові URL повинні показуватись у статус-барі браузера, а користувачі повинні мати можливість скопіювати їхні лінки або відкрити докладнішу інформацію про героя у новій закладці.

Щоб досягнути цього ефекту, відкрийте dashboard.component.html та замініть теги <div *ngFor...> на <a>. Відкриваючий тег <a> має бути таким:

app/dashboard.component.html (повторювані теги <a>)

<a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4">

Зверніть увагу на прив'язку [routerLink].

На верхньому рівні навігації (на головній сторінці) у шаблоні AppComponent маємо лінки з фіксовано встановленими шляхами цільових маршрутів: /dashboard та /heroes.

А зараз — у шаблоні DashboardComponent, ми прив'язались до виразу, який містить масив параметрів лінка. Цей масив має два елементи: шлях цільового маршруту та параметр маршруту зі значенням id поточного героя.

Два елементи масиву приведені у відповідність із шляхом та токеном :id у параметризованому маршруті для детальної інформації про героя, визначення якого ми додали до app.module.ts раніше у цьому розділі:

app/app.module.ts (hero detail)

{ path: 'detail/:id', component: HeroDetailComponent },

Перезавантажте браузер та виберіть героя на Панелі Керування; застосунок повинен перекинути вас безпосередньо до деталей про цього героя.

Перенесення маршрутів до Модуля Маршрутизації

Майже на 20 рядках AppModule описана конфігурація чотирьох маршрутів. Більшість застосунків мають значно більше маршрутів і вони додають сервіси охорони щоб убезпечити себе від небажаних чи неавторизованих переходів. Переписування маршрутизації може швидко охопити цей модуль та затулити його основну мету — встановлення ключових фактів про застосунок для компілятора Angular.

Ми повинні переписати конфігурацію маршрутизації відокремивши її у свій власний клас. Який саме клас? Поточний RouterModule.forRoot() створює Angular ModuleWithProviders припускаючи, що клас, призначений для маршрутизації, повинен бути певним модулем. Він повинен бути саме Модулем Маршрутизації.

За угодою, назва Модуля Маршрутизації повинна містити слово Routing та узгоджуватись з назвою модуля, де оголошується компонент, та до якого здійснюється перехід.

Створимо файл app-routing.module.ts у тій же теці, що і app.module.ts. Вставимо у нього наступний вміст, взятий із класу AppModule:

app/app-routing.module.ts

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent }, { path: 'detail/:id', component: HeroDetailComponent }, { path: 'heroes', component: HeroesComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}

На що варто звернути увагу, що типово для Модулів Маршрутизації:

Оновимо AppModule

Тепер видалимо конфігурацію маршрутизації з AppModule та імпортуємо AppRoutingModuleдвох місцях: за допомогою JavaScript-виразу import та через додавання в масив NgModule.imports).

Ось переписаний AppModule, порівняйте його з попереднім станом:

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroesComponent } from './heroes.component'; import { HeroService } from './hero.service'; import { AppRoutingModule } from './app-routing.module'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { } import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot([ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent }, { path: 'detail/:id', component: HeroDetailComponent }, { path: 'heroes', component: HeroesComponent } ]) ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { }

Тепер він став простим та сфокусованим на ідентифікації ключових частин застоснку.

Вибиремо Героя у HeroesComponent

Раніше ми додали можливість для вибору героя на Панелі Керування. Тепер зробимо щось схоже у HeroesComponent.

Шаблон HeroesComponent представляє собою стиль показу "головне/детальне" зі списком героїв вгорі та з деталями про вибраного героя внизу.

app/heroes.component.ts (поточний шаблон)

template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <my-hero-detail [hero]="selectedHero"></my-hero-detail> `,

Наша мета — перенести деталі в окреме візуальне подання та зробити можливим перехід до нього, коли користувач вирішить редагувати вибраного героя.

Видалимо <h1> згори (ми забули про це під час перетворення AppComponent у HeroesComponent).

Видалимо останній рядок у шаблоні з тегом <my-hero-detail>.

Ми більше не показуватимемо цілий HeroDetailComponent. Ми збираємось показувати деталі про героя на його особистій сторінці та переходити до них так, як ми вже переходили з Панелі Керування.

Урізноманітнемось трохи: хоча дотримуватимемось стилю показу "головне/детальне", але скоротимо деталі до "міні" версії без можливості редагування. Коли користувач вибере героя зі списку, ми не будемо переходити на сторінку з деталями. Ми покажемо міні-деталі на цій же сторінці та зробимо кнопку, клацаючи по якій користувач переходитиме на сторінку з усіма деталями.

Додамо міні-деталі

Додамо наступний HTML-фрагмент вгорі шаблона, де раніше використовувався <my-hero-detail>:

<div *ngIf="selectedHero"> <h2> {{selectedHero.name | uppercase}} is my hero </h2> <button (click)="gotoDetail()">View Details</button> </div>

Після клацання на герої, користувач повинен бачити щось на зразок цього:

Mini Hero Detail

Форматування за допомогою uppercase pipe

Зауважте, що ім'я героя показується великими літерами. Це результат роботи uppercase pipe, який ми підставили у прив'язку інтерполяції. Гляньте праворуч після оперетора ( | ).

{{selectedHero.name | uppercase}} is my hero

Pipes є хорошим способом для форматування рядків, валютних сум, дат та іншого показу даних. Angular має кілька pipes, але ми можемо і самі писати їх.

Дізнатись більше про pipes у розділі Pipes.

Перенесемо вміст за межі файла компонента

Ми не завершили. Нам ще належить оновити клас компонента для підтримки переходів до HeroDetailComponent, коли користувач клацає кнопку View Details.

Цей файл компонента дійсно великий. Багато місця займає шаблон та CSS-стилі, у ньому важко знайти логіку компонента.

Для початку, давайте рознесемо шаблон та стилі в окремі файли:

  1. Виріжте-та-вставте вміст шаблону у новий файл heroes.component.html.
  2. Виріжте-та-вставте вміст стилів у новий файл heroes.component.css.
  3. Встановіть властивості templateUrl та styleUrls метаданих компонента щоб можна було посилатись на щойно створені два файли.
  4. Встановіть для властивості moduleId значення module.id, щоб templateUrl та styleUrls були відносними до цього компонента.

Властивість styleUrls містить масив з назвами файлів стилів. Ми можемо додавати до масиву декілька файлів стилів з різних місць, якщо нам це потрібно.

app/heroes.component.ts (переписані метадані)

@Component({ moduleId: module.id, selector: 'my-heroes', templateUrl: './heroes.component.html', styleUrls: [ './heroes.component.css' ] })

Оновимо клас HeroesComponent.

Ми переходимо від HeroesComponent до HeroDetailComponent внаслідок клацання кнопки. Подія клацання прив'язана до методу gotoDetail, який переходить імперативно говорячи маршрутизатору куди потрібно йти.

Такий підхід вимагає деяких змін у класі компонента:

  1. Імпортуємо router з бібліотеки маршрутизатора Angular
  2. Вставимо router в конструктор (разом з HeroService)
  3. У методі gotoDetail будемо викликати router.navigate

app/heroes.component.ts (gotoDetail)

gotoDetail(): void { this.router.navigate(['/detail', this.selectedHero.id]); }

Зверніть увагу, що ми передаємо двохелементний масив параметрів лінка — шлях та параметр маршрута — до методу router.navigate точно так само, як ми це робили раніше з прив'язкою до [routerLink] у DashboardComponent. Ось повна версія переписаного класу HeroesComponent:

app/heroes.component.ts (class)

export class HeroesComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor( private router: Router, private heroService: HeroService) { } getHeroes(): void { this.heroService.getHeroes().then(heroes => this.heroes = heroes); } ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } gotoDetail(): void { this.router.navigate(['/detail', this.selectedHero.id]); } }

Після перезавантаження браузера, поклацайте навігаційні лінки. Тепер ми маємо навігацію у застосунку: від Панелі Керування до деталей про героя й назад, від списку героїв до міні-деталей, й далі — до деталей про героя і назад до героїв. Ми можемо переходити назад й вперед між панеллю керування та героями.

Ми забезпечили усі навігаційні вимоги, що стояли перед нами на початку цього уроку.

Стилізація застосунка

Застосунок функціонує, але є досить потворним. Наш креативний дизайнер надав нам деякі CSS-файли, щоб зробити його трохи кращим.

Панель керування зі стилями

Дизайнер каже, що ми повинні показувати панель керування героїв у простому прямокутнику. Він дав нам для цього ~60 рядків CSS включаючи деякі прості медіа-запити для адаптивності.

Якщо ми вставимо ці ~60 рядків у метадані styles, вони повністю затулять собою логіку компонента. Давайте такого не робити. Редагувати CSS краще в окремому файлі *.css деінде.

Додамо файл dashboard.component.css до теки app та посилатимемось на цей файл у властивості styleUrls масиву метаданих компонента наступним чином:

app/dashboard.component.ts (styleUrls)

styleUrls: [ './dashboard.component.css' ]

Стильні деталі про героя

Дизайнер також дав нам CSS-стилі, призначені для HeroDetailComponent.

Додамо hero-detail.component.css до теки app та посилатимемось на цей файл всередині масиву styleUrls, як це ми робили для DashboardComponent. Давайте також видалимо властивість hero декоратора @Input та його імпорт, поки ми в ньому.

Ось вміст для вищезгаданих CSS-файлів компонента.

label { display: inline-block; width: 3em; margin: .5em 0; color: #607D8B; font-weight: bold; } input { height: 2em; font-size: 1em; padding-left: .4em; } button { margin-top: 20px; font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } button:disabled { background-color: #eee; color: #ccc; cursor: auto; } [class*='col-'] { float: left; padding-right: 20px; padding-bottom: 20px; } [class*='col-']:last-of-type { padding-right: 0; } a { text-decoration: none; } *, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } h3 { text-align: center; margin-bottom: 0; } h4 { position: relative; } .grid { margin: 0; } .col-1-4 { width: 25%; } .module { padding: 20px; text-align: center; color: #eee; max-height: 120px; min-width: 120px; background-color: #607D8B; border-radius: 2px; } .module:hover { background-color: #EEE; cursor: pointer; color: #607d8b; } .grid-pad { padding: 10px 0; } .grid-pad > [class*='col-']:last-of-type { padding-right: 20px; } @media (max-width: 600px) { .module { font-size: 10px; max-height: 75px; } } @media (max-width: 1024px) { .grid { margin: 0; } .module { min-width: 60px; } }

Стилі та навігаційні лінки

Дизайнер дав нам CSS щоб зробити навігаційні лінки в AppComponent більш схожими на ті, які можна вибирати. Ми згрупували такі лінки за допомогою тега <nav>.

Додамо файл app.component.css у теку app з наступним вмістом:

app/app.component.css (стилі для навігації)

h1 { font-size: 1.2em; color: #999; margin-bottom: 0; } h2 { font-size: 2em; margin-top: 0; padding-top: 0; } nav a { padding: 5px 10px; text-decoration: none; margin-top: 10px; display: inline-block; background-color: #eee; border-radius: 4px; } nav a:visited, a:link { color: #607D8B; } nav a:hover { color: #039be5; background-color: #CFD8DC; } nav a.active { color: #039be5; }

Директива routerLinkActive

Маршрутизатор Angular надає директиву routerLinkActive, яку ми можемо використовувати щоб додати клас до навігаційного HTML-елемента, чий маршрут співпадає з активним маршрутом. Все що нам потрібно зробити — визначити стилі для нього. Класно!

app/app.component.ts (лінки активного маршрутизатора)

template: ` <h1>{{title}}</h1> <nav> <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> `,

Спочатку додамо moduleId: module.id до метаданих @Component у AppComponent щоб увімкнути режим відносних URL у поточному модулі для його файлів. Потім додамо властивість styleUrls, яка спрямовує до щойно створеного CSS-файлу наступним чином:

app/app.component.ts (styleUrls)

styleUrls: ['./app.component.css'],

Глобальні стилі застосунка

Коли ми додамо стилі до компонента, ми утримуємо усе, що потрібно компоненту — HTML, CSS, код — разом в одному місці. Усе це досить легко запакувати та повторно використовувати у будь-якому компоненті.

Ми також можемо створити стилі на рівні застосунка за межами будь-яких компонентів.

Наш дизайнер надав нам деякі базові стилі щоб впроваджувати їх у елементах у всьому застосунку. Ось їх уривок:

styles.css (уривок)

/* Master Styles */ h1 { color: #369; font-family: Arial, Helvetica, sans-serif; font-size: 250%; } h2, h3 { color: #444; font-family: Arial, Helvetica, sans-serif; font-weight: lighter; } body { margin: 2em; } body, input[text], button { color: #888; font-family: Cambria, Georgia; } /* . . . */ /* everywhere else */ * { font-family: Arial, Helvetica, sans-serif; }

Створіть файл styles.css, якщо ви ще його не маєте.

Якщо необхідно, також редагуйте index.html щоб посилатись на ці стилі.

index.html (лінк на стилі)

<link rel="stylesheet" href="styles.css">

Тепер оглянемо застосунок. Наша панель керування, герої, та навігаційні лінки є стилізованими!

Огляд навігації

Огляд структури застосунка

Запустіть живий приклад для цієї частини. Давайте пересвідчимось, що ми маємо наступну структуру:

angular-tour-of-heroes
app
app.component.css
app.component.ts
app.module.ts
app-routing.module.ts
dashboard.component.css
dashboard.component.html
dashboard.component.ts
hero.service.ts
hero.ts
hero-detail.component.css
hero-detail.component.html
hero-detail.component.ts
heroes.component.css
heroes.component.html
heroes.component.ts
main.ts
mock-heroes.ts
node_modules ...
index.html
package.json
styles.css
systemjs.config.js
tsconfig.json

Підсумок

Що ми пройшли

Давайте підіб'ємо підсумок того, що ми збудували.

Йдемо далі

Ми вже маємо багато фундаментальних блоків, потрібних для побудови застосунку. Але ми ще не маємо ключової частини: доступу до віддалених даних.

У наступному розділі, замінимо наш макет даних на дані, отримані з віддаленого сервера за допомогою http.

Наступний крок

HTTP