Головне/детальне

Наша історія потребує більше героїв. Ми розширемо наш застосунок Тур Героїв щоб можна було показувати список героїв, дозволимо користувачам вибирати героя та показувати детальну інформацію про нього.

Запустіть живий приклад для цього розділу.

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

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

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

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

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

Показ наших героїв

Створення героїв

Давайте створимо масив з десятьма героями.

app.component.ts (масив героїв)

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' } ];

Тут ми створили масив HEROES з типом даних Hero (клас Hero визначено у Частині 1 Туру Героїв). Нам треба витягувати цей список героїв із веб-сервіса, але спершу давайте пройдемо декілька кроків та покажемо макет героїв.

Представлення героїв

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

app.component.ts (властивість з масивом героїв)

heroes = HEROES;

Ми не маємо визначення типу змінної heroes. TypeScript може автоматично виводити його із масива HEROES (якщо змінній передається масив, значить вона має тип даних "масив").

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

Показ героїв у шаблоні

Наш компонент має властивість heroes. Давайте створимо ненумерований список в нашому шаблоні щоб показати її. Вставимо наступну частину HTML-коду нижче заголовка та вище детальної інформації про героїв.

app.component.ts (шаблон героїв)

<h2>My Heroes</h2> <ul class="heroes"> <li> <!-- each hero goes here --> </li> </ul>

Тепер ми маємо шаблон, який можна заповнити нашими героями.

Роздрукуємо список героїв із ngFor

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

Спершу, змінимо тег <li> додавши вбудовану директиву *ngFor.

app.component.ts (ngFor)

<li *ngFor="let hero of heroes">

Зірочка (*) на початку ngFor необхідна для даного синтаксиса.

Префікс (*) біля ngFor вказує що <li> та його дочірні елементи являють собою головний шаблон.

Директива ngFor перебирає масив heroes, який береться із властивості AppComponent.heroes та видає примірник цього шаблона.

Текст в лапках після ngFor говорить “беріть кожного героя із масиву heroes, зберігайте його у локальній змінній hero, та робіть його доступним для відповідного примірника шаблона”.

Ключове слово let перед "hero" ідентифікує hero як вхідну змінну шаблона. Ми можемо посилатись на цю змінну всередині шаблона щоб отримати доступ до властивостей героїв.

Дізнайтесь більше про ngFor та вхідні змінні шаблона у розділах Показ даних та Синтакс шаблона.

Тепер ми вставимо певний вміст між тегами <li>, де використаємо змінну шаблона hero для показу властивостей героя.

app.component.ts (шаблон ngFor)

<li *ngFor="let hero of heroes"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li>

Коли браузер оновить сторінку, ми побачимо список героїв!

Зробимо стилі для наших героїв

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

Давайте додамо деякі стилі в наш компонент, передавши у властивість styles декоратора @Component наступні CSS-класи:

app.component.ts (стилі)

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; } `]

Зверніть увагу, що ми знову використали косі лапки (back-tick) щоб охопити зразу декілька рядків.

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

Коли ми визначаємо стилі для компонента, вони застосовуються виключно до цього компонента. Тобто наші стилі будуть застосовуватись до AppComponent та не впливатимуть на зовнішні HTML-блоки.

Наш шаблон для показу героїв повинен виглядати приблизно так:

app.component.ts (стилі героїв)

<h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul>

Вибрані герої

Ми маємо список героїв, але показуємо у застосунку лише одного героя. Список та окремий герой не пов'язані у будь-який спосіб. Ми хочемо щоб користувач вибрав героя з нашого списку, і щоб для вибраного героя показувалась детальна інформація. Цей UI-патерн широковідомий як "головне-детальне" (англ. master-detail). В нашому випадку, головне - це список героїв, а детальне - це вибраний герой.

Давайте з'єднаємо головне з детальним через властивість selectedHero компонента AppComponent прив'язавшись до події "клацання".

Подія клацання

Ми змінемо <li> націливши прив'язку подій Angular на подію клацання.

app.component.ts (уривок шаблона)

<li *ngFor="let hero of heroes" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li>

Придивіться до прив'язки подій

(click)="onSelect(hero)"

Круглі дужки ідентифікують подію click (тобто клацання), на елементі <li> як цільову подію. Вираз, праворуч від знаку дорівнює, викликає метод onSelect() класа AppComponent, передаючи вхідну шаблонну змінну hero як аргумент. Це саме та змінна hero, яку ми попереді визначили у ngFor.

Дізнайтесь більше про прив'язку подій у розділах Вхідні дані та Синтаксис шаблона.

Додамо обробник клацання

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

Що повинен робити цей метод? Він повинен брати тих героїв, по яким користувач клацнув, і встановлювати їх до вибраних героїв.

Наш компонент покищо не має “вибраних героїв”. Зараз матиме.

Виокремлення вибраного героя

Нам більше не потрібна статична властивість hero у класі AppComponent. Замініть її властивістю selectedHero:

app.component.ts (selectedHero)

selectedHero: Hero;

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

Тепер додайте метод onSelect, який встановлює властивість selectedHero.

app.component.ts (onSelect)

onSelect(hero: Hero): void { this.selectedHero = hero; }

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

app.component.ts (уривок шаблона)

<h2>{{selectedHero.name}} details!</h2> <div><label>id: </label>{{selectedHero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="selectedHero.name" placeholder="name"/> </div>

Приховування пустих елементів за допомогою ngIf

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

EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]

Пам'ятайте, що у шаблоні ми показуємо selectedHero.name. Властивості name не існує тому, що selectedHero є undefined.

Ми вирішимо цю проблему за допомогою утримування детальної інформації героя за межами DOM, до поки не з'явиться вибраний герой.

Ми обгорнимо HTML-шаблон детальної інформації героя у <div>. Після чого, додамо вбудовану директиву ngIf та підставимо їй значення із властивості selectedHero нашого компонента.

app.component.ts (ngIf)

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

Пам'ятайте, що зірочка (*) на початку ngIf є необхідною частиною цього синтаксиса.

Коли значення для selectedHero відсутнє, директива ngIf видаляє HTML-блок для детальної інформації героя із DOM. А якщо немає HTML-блоку в DOM із прив'язкою до змінної шаблона, то й немає про що турбуватись.

Коли користувач клацне на герою, selectedHero матиме "true", ngIf вкладе HTML-блок для детальної інформації героя в DOM, і лише тоді відбудеться прив'язка до змінної шаблона в цьому блоці.

ngIf та ngFor називаються “структурними директивами”, оскільки вони можуть змінювати структуру частин DOM. Іншими словами, вони формують структуру ще до того, як Angular показує вміст у DOM.

Дізнайтесь більше про ngIf, ngFor та інші структурні директиви у розділах Структурні директиви та Синтаксис шаблона.

Браузер оновиться й ми побачимо список героїв, але без детальної інформації про вибраного героя. ngIf утримує цей блок за межами DOM до того часу, поки selectedHero буде undefined. Коли ми клацнимо на певному герою зі списка, вибраний герой покажеться у блоці з детальною інформацією. Все працює як ми очікуємо.

Стилі для вибраних героїв

Ми бачимо вибраних героїв в частині для детальної інформації внизу, але ми не можемо швидко знайти цих героїв у списку вгорі. Ми можемо виправити це, застосувавши CSS-клас selected до відповідного <li> у головному списку. Наприклад, коли ми вибираємо Magneta із списку героїв, його можна виділити візуально через зміну кольору фону, як показано нижче.

Selected hero

Ми додамо у шаблоні прив'язку до властивості class для класу selected. Встановимо туди вираз, який порівнює поточний selectedHero із hero.

Ключом є назва CSS-класа (selected). Значення буде true, якщо обидва героя однакові, і false - в противному разі. Таким чином ми говоримо “застосувати CSS-клас selected, якщо герої будуть однаковими, і видалити цей клас, якщо вони неоднакові”.

app.component.ts (налаштування CSS-класа)

[class.selected]="hero === selectedHero"

Зверніть увагу, що у шаблоні цей class.selected взято у квадратні дужки ([]). Таким є синтаксис для прив'язки властивостей, прив'язки в якій дані йдуть в одному напрямку: із джерела даних (вираз hero === selectedHero) до властивості class.

app.component.ts (стилі для кожного героя)

<li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li>

Дізнайтесь більше про прив'язку властивостей в розділі Синтаксис Шаблона.

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

Output of heroes list app

Ми виберемо іншого героя, і тепер вже він підсвітиться.

Ось увесь app.component.ts:

app.component.ts

import { Component } from '@angular/core'; export class Hero { id: number; name: string; } 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> <div *ngIf="selectedHero"> <h2>{{selectedHero.name}} details!</h2> <div><label>id: </label>{{selectedHero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="selectedHero.name" placeholder="name"/> </div> </div> `, 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; } }

Що ми пройшли

Ось що ми пройшли в цьому розділі:

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

Йдемо далі

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

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

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