Декілька компонентів

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

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

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

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

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

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

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

npm start

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

Зробимо компонент для детальної інформації про героя

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

Наш поточний компонент порушує Принцип єдиного обов'язку. Це лише підручник, але й тут ми можемо робити правильні речі — особливо, якщо робити їх правильними не складно, ми зробимо це в процесі побудови нашого Angular-застосунка.

Давайте відокремимо компонент для детальної інформації.

Відокремлення компонента для детальної інформації про героя

Додайте новий файл з назвою hero-detail.component.ts у теку app, та створіть HeroDetailComponent з таким вмістом:

app/hero-detail.component.ts (початкова версія)

import { Component, Input } from '@angular/core'; @Component({ selector: 'my-hero-detail', }) export class HeroDetailComponent { }

Угода іменування

Нам подобається чітко ідентифікувати: які із класів є компонентами, а які із файлів містять компоненти.

Зауважте, що ми маємо AppComponent у файлі з назвою app.component.ts, а наш новий HeroDetailComponent знаходиться у файлі з назвою hero-detail.component.ts.

Усі назви наших компонентів закінчуються на "Component". Усі назви наших файлів компонентів закінчуються на ".component".

Ми пишемо кожну літеру у назвах файлів у нижньому регістрі через дефіз (цей стиль іменування відомий також як kebab-case), так що ми не турбуємось про чутливість до регістра на сервері або у системі контролю версій.

Як показано в прикладі, ми починаємо з імпорту декораторів Component та Input, оскільки скоро вони будуть потрібні.

Ми створюємо метадані за допомогою декоратора @Component, де вказуємо селектор my-hero-detail, який ідентифікує елемент цього компонента. Потім ми експортуємо цей клас щоб зробити його доступним для інших компонентів.

Коли завершимо, зробимо імпорт цього класа у AppComponent, та створимо відповідний елемент <my-hero-detail>.

Шаблон для детальної інформації про героя

На даний момент, візуальне подання для списку героїв та для детальної інформації про героя скомбіновано в одному шаблоні у AppComponent. Давайте виріжемо вміст для детальної інформації про героя з AppComponent, та вставимо його у відповідну властивість шаблона HeroDetailComponent.

Попереді, ми прив'язувались до властивості selectedHero.name у класі AppComponent. Наш HeroDetailComponent буде мати властивість hero, але не властивість selectedHero. Отже ми замінимо selectedHero на hero у нашому новому шаблоні. Це єдина наша зміна. Результат буде приблизно таким:

app/hero-detail.component.ts (шаблон)

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> `

Тепер наш шаблон з детальною інформацією існує лише у HeroDetailComponent.

Додамо властивість hero

Давайте додамо властивість hero, про яку ми говорили, до компонента класа.

hero: Hero;

Ух-ох. Ми оголосили властивість hero з типом даних Hero, але наш клас Hero знаходиться за межами файла app.component.ts. Ми маємо два компонента, кожен у своєму власному файлі повинен посилатись на клас Hero.

Ми вирішемо цю проблему через винисення класа Hero з файлу app.component.ts в окремий файл hero.ts.

app/hero.ts

export class Hero { id: number; name: string; }

Ми експортуємо клас Hero із hero.ts, оскільки нам треба посилатись на нього в обох файлах компонентів. Додайте наступний вираз імпорту вгорі обох файлів: app.component.ts та hero-detail.component.ts.

import { Hero } from './hero';

Властивість hero є вхідною

Того героя, якого HeroDetailComponent буде показувати, треба ще передати ззовні. Хто зможе це зробити? Батьківський AppComponent!

AppComponent знає якого героя треба показувати: того героя, який був вибраний зі списку. Вибір користувача зберігається у властивості selectedHero.

Ми скоро оновимо шаблон AppComponent так, щоб він прив'язував свою властивість selectedHero до властивості hero нашого HeroDetailComponent. Прив'язка може бути такою:

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

Зверніть увагу, що властивість hero є цільовою для прив'язки властивостей — вона знаходиться у квадратних дужках, ліворуч від (=).

Angular наполягає, що оголошена цільова властивість повинна бути вхідною (input) властивістю. Якщо ми цього не зробимо, Angular не прийме прив'язку й кине помилку.

Ми пояснюємо більш детально про вхідні властивості тут, і там же ми пояснемо чому цільові властивості вимагають такого спеціального тлумачення, а властивості джерела - ні.

Існує декілька способів щоб оголосити властивість hero вхідною. Зробимо це у спосіб, якому ми віддаємо перевагу: за допомогою анотації властивості hero декоратором @Input, імпортованим раніше.

@Input() hero: Hero;

Дізнайтесь більше про декоратор @Input() у розділі Атрибути директив.

Оновимо AppModule

Повернемось до AppModule, це кореневий модуль застосунка, та навчимо його використовувати HeroDetailComponent.

Спочатку ми імпортуємо HeroDetailComponent щоб мати можливість посилатись на нього.

import { HeroDetailComponent } from './hero-detail.component';

Потім додамо HeroDetailComponent у масив declarations декоратора NgModule. Цей масив містить список компонентів, pipes, та директив, які ми створили і які відносяться до нашого модуля застосунку.

@NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

Оновимо AppComponent

Тепер застосунок знає про наш HeroDetailComponent, знаходить його розташування у шаблоні AppComponent, звідки ми видалили частину, яка відповідає за детальну інформацію героя, та додали тег елемента, представляючий HeroDetailComponent.

<my-hero-detail></my-hero-detail>

my-hero-detail є тією назвою, яку ми встановили у пункті selector метаданих HeroDetailComponent.

Два компоненти не координуються, допоки ми не прив'яжемо властивість selectedHero класа AppComponent до властивості hero елемента HeroDetailComponent, наприклад так:

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

Шаблон класа AppComponent повинен тепер бути приблизно таким:

app.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> `,

Завдячуючи прив'язці, HeroDetailComponent повинен отримувати героя із AppComponent та показувати детальну інформацію цього героя під списком. Детальна інформація повинна оновлюватись кожен раз, коли користувач вибирає нового героя.

Все працює!

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

Що є фундаментально-новим? — Ми можемо використовувати HeroDetailComponent для показу детальної інформації героя в будь-якому шаблоні застосунка.

Ми створили наш перший спільний компонент!

Переглянемо структуру застосунка

Давайте пересвідчимось, що ми маємо наступну структуру після усіх наших рефакторингів у цьому розділі:

angular-tour-of-heroes
app
app.component.ts
app.module.ts
hero.ts
hero-detail.component.ts
main.ts
node_modules ...
index.html
package.json
tsconfig.json

Тут зібрано усі файли з кодом, який ми обговорювали в цьому розділі.

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; } import { Component } from '@angular/core'; import { Hero } from './hero'; const HEROES: Hero[] = [ { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ]; @Component({ selector: 'my-app', 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> `, styles: [` .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } `] }) export class AppComponent { title = 'Tour of Heroes'; heroes = HEROES; selectedHero: Hero; onSelect(hero: Hero): void { this.selectedHero = hero; } } export class Hero { id: number; name: string; } 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'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

Що ми пройшли

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

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

Йдемо далі

Наш Тур Героїв став універсальнішим зі спільними компонентами.

Ми все ще отримуємо дані з макету всередині AppComponent. Так не буде завжди. Нам слід зробити рефакторинг коду, щоб мати доступ до даних з окремого сервіса, та зробити його спільним поміж компонентами, які потребують даних.

Ми вивчимо як можна створювати сервіси у наступному розділі підручника.

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

Сервіси