Наше приложение растет. В этой части сосредоточимся на компонентах, пригодных для повторного использования, а также на передаче данных компонентам. Давайте отделим список героев в отдельный компонент и сделаем этот компонент пригодным для повторного использования.
Где мы остановились
Прежде чем продолжить наш Тур героев, давайте проверим, что наш проект имеет следующую структуру. Если это не так, нужно будет вернуться к предыдущим главам.
angular2-tour-of-heroes
app
app.component.ts
main.ts
node_modules ...
typings ...
index.html
package.json
tsconfig.json
typings.json
Поддержка преобразования кода и выполнения приложения
Нам нужно запустить компилятор TypeScript, чтобы при этом он отслеживал изменения в файлах и сразу выполнял компиляцию, а также запустить наш web-сервер. Мы сделаем это, набрав
npm start
Это позволит держать приложение запущенным, пока мы продолжаем создавать Тур героев.
Создание компонента детальной информации о герое
Список героев и детальная информация о герое находятся в одном и том же компоненте, в одном файле. Пока что они невелики, но каждый из них может вырасти. Мы можем получить новые требования к одному из них, что потребует изменения только одного, но не другого. Тем не менее, каждое изменение таит опасность ошибок для двух компонентов и удваивает тестирование. Если бы возникла необходимость повторно использовать детальную информацию о герое в другом месте нашего приложения, нам пришлось бы прихватить и список героев.
Наш текущий компонент нарушает единый принцип ответственности. Этот материал всего лишь урок, но мы можем сделать все правильно — тем более, что это не так сложно. Кроме того, в процессе мы узнаем больше о том, как строить приложения в Angular.
Давайте извлечем детальную информацию о герое в свой собственный компонент.
Отделяем детальную информацию о герое
Добавим новый файл с именем hero-detail.component.ts
в папку app
и создадим HeroDetailComponent
, как показано ниже.
hero-detail.component.ts (первоначальная версия)
import {Component, Input} from 'angular2/core';
@Component({
selector: 'my-hero-detail',
})
export class HeroDetailComponent {
}
Cоглашения об именовании
Мы хотели бы понять с первого взгляда, какие классы являются компонентами (по названию класса) и какие файлы содержат компоненты (по названию файла).
Обратите внимание на то, что у нас есть AppComponent
в файле с именем app.component.ts
и наш новый HeroDetailComponent
находится в файле с именем hero-detail.component.ts
.
Все наши составные имена классов заканчиваются на "Component". Все наши составные имена файлов заканчиваются на ".component".
Мы переводим имена файлов в "нижний регистр с тире" (kebab-case), поэтому мы не беспокоимся о чувствительности к регистру на сервере или в системе управления версиями.
Рассмотрим вышеприведенный код.
Мы начали с импорта декораторов Angular — Component
и Input
, потому что они скоро нам понадобятся.
Затем мы создаем метаданные с декоратором @Component
, где мы указываем имя селектора, который идентифицирует элемент компонента. Затем мы экспортируем класс, чтобы сделать его доступным для других компонентов.
Закончив здесь, мы импортируем его в AppComponent
и создадим соответствующий элемент
<my-hero-detail>
.
Шаблон детальной информации о герое
В данный момент, представления Heroes и Hero Detail объединены в один шаблон в AppComponent
. Давайте вырежем содержимое Hero Detail из AppComponent
и вставим его в новое свойство шаблона HeroDetailComponent
.
Ранее мы привязывали свойство selectedHero.name
в AppComponent
. Наш HeroDetailComponent
будет иметь свойство hero
, а не свойство selectedHero
. Таким образом, мы заменим selectedHero
на hero
повсюду в нашем новом шаблоне. Это наше единственное изменение. Результат будет выглядеть так:
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
, но наш класс героя находится в файле app.component.ts
. У нас есть два компонента, каждый из которых в своем собственном файле, которые должны ссылаться на класс Hero
.
Мы решим эту проблему, переместив класс Hero
из app.component.ts
в свой собственный файл hero.ts
.
hero.ts (Экспортированный класс Hero)
export class Hero {
id: number;
name: string;
}
Мы экспортируем класс Hero
из hero.ts
, потому что нам нужно ссылаться на него в обоих файлах компонентов. Добавьте следующий оператор импорта в верхней части app.component.ts
и hero-detail.component.ts
.
hero-detail.component.ts и app.component.ts (Импорт класса Hero)
import {Hero} from './hero';
Свойство hero является входящим.
Нужно сказать компоненту HeroDetailComponent
, какого героя ему отобразить. Кто ему скажет это? Родитель AppComponent
!
AppComponent
знает, какого героя показать: героя, который пользователь выбрал из списка. Выбор пользователя находится в свойстве selectedHero
.
Мы обновим шаблон AppComponent
так, что он свяжет свое свойство selectedHero
со свойством hero
нашего HeroDetailComponent
. Связывание может выглядеть следующим образом:
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
Обратите внимание на то, что свойство hero
является целевым свойством — оно в квадратных скобках слева от (=).
Angular требует, чтобы объявленное целевое свойство было входящим свойством. Если мы это не сделаем, Angular откажет в связывании и выдаст сообщение об ошибке.
Мы объясним входные свойства более подробно здесь, мы также поясним, почему целевые свойства требуют этот специальный подход, а свойства источника нет.
Есть несколько способов, как указать, что hero
является входящим. Мы сделаем это предпочтительным способом, аннотировав свойство hero
декоратором @Input
, которое мы импортировали ранее.
@Input()
hero: Hero;
Узнать больше о декораторе @Input()
можно в главе Директивы атрибутов.
Обновление AppComponent
Вернемся к AppComponent
и научим его использовать HeroDetailComponent
.
Начнем с импорта HeroDetailComponent
, чтобы можно было сослаться на него.
import {HeroDetailComponent} from './hero-detail.component';
Найдем место в шаблоне, где мы удалили содержимое Hero Detail и добавим тег элемента, который представляет 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="#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
и отобразить детальную информацию этого героя под списком. Эта информация должна обновляться каждый раз, когда пользователь выбирает нового героя.
Это пока что не происходит!
Щелкаем в списке героев. Никакой информации. Мы ищем ошибки в консоли "Инструменты разработчика браузера". Ошибок нет.
Выглядит так, будто Angular игнорирует новый тег. Все это потому, что он действительно игнорирует новый тег.
Массив директив
Браузер игнорирует неизвестные ему HTML теги и атрибуты. Так же поступает и Angular.
Мы импортировали HeroDetailComponent
и использовали его в шаблоне, но мы не сказали Angular об этом.
Мы говорим об этом Angular путем перечисления этого компонента в метаданных, в массиве directives
. Добавим этот массив свойств в нижней части конфигурации @Component
, сразу после template
и styles
.
directives: [HeroDetailComponent]
Заработало!
Когда мы просматриваем наше приложение в браузере, мы видим список героев. Когда мы выбираем героя, мы видим детальную информацию о нем.
Принципиальное изменение в том, что мы можем использовать этот компонент HeroDetailComponent
, чтобы отобразить детальную информацию о герое где-нибудь в другом месте приложения.
Мы создали наш первый повторно используемый компонент!
Обзор структуры приложения
Давайте проверим, что после проведенного в этой главе рефакторинга, у нас следующая структура проекта:
angular2-tour-of-heroes
app
app.component.ts
hero.ts
hero-detail.component.ts
main.ts
node_modules ...
typings ...
index.html
package.json
tsconfig.json
typings.json
Файлы кода, которые мы обсуждали в этой главе.
import {Component, Input} from 'angular2/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 'angular2/core';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
@Component({
selector: 'my-app',
template:`
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#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;
}
`],
directives: [HeroDetailComponent]
})
export class AppComponent {
title = 'Tour of Heroes';
heroes = HEROES;
selectedHero: Hero;
onSelect(hero: Hero) { this.selectedHero = hero; }
}
var 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" }
];
export class Hero {
id: number;
name: string;
}
Путь, который мы прошли
Давайте подведем итоги того, что мы создали.
- Мы создали компонент, который можно использовать повторно.
- Мы узнали, как сделать, чтобы компонент принимал входные данные.
- Мы научились связывать родительский компонент с дочерним компонентом.
- Мы научились объявлять нужные нам директивы приложения в массиве
directives
.
Предстоящий путь
Наш тур героев стал более подходящим для многократного использования с разделяемыми компонентами.
Мы все еще получаем наши данные о героях (используя заглушку для их получения) в AppComponent
. Это не лучший вариант. Мы должны сделать рефакторинг доступа к данным, вынеся получение данных в отдельный сервис, и расшарить этот сервис компонентам, которым необходимы эти данные.
Мы будем учиться создавать сервисы в следующей главе.
Комментарии (12)
Focushift
28.04.2016 14:16-1Открываю хабр и вижу интересную статью, но почему-то сразу часть 3, может в самой статье есть ссылка на начало? А нет, и в заголовке текст вида «шоу продолжается», откуда продолжение? Статью закрываем без чтения…
Sterhel
28.04.2016 14:17+1Делаем 1 осознанный клик по никнейму автора над заголовком – и получаем все 3 публикации серии, https://habrahabr.ru/users/illian/topics/
EJIqpEP
29.04.2016 10:05Для тех, кому проще и понятнее смотреть видео чем читать текст, я снимаю тур героев на русском. На данный момент снял 4 урока. Сегодня досниму пятый. Вот ссылка на первый урок.
EJIqpEP
29.04.2016 10:29Почему то ссылка на прикрепилась. http://monsterlessons.com/project/lessons/tur-geroev-vstuplenie
bromzh
29.04.2016 14:41В beta.17 вышли breaking change для синтаксиса ngFor.
Суть такова: теперь #foo всегда будет локальной ссылкой на элемент. var-биндинг исчез, вместо него теперь let-биндинг.
Теперь, вместо<div *ngFor="#item of items">
надо писать<div *ngFor="let item of items">
.
Такой код:
<ul> <li *ngFor="let item in items">{{item}}</li> </ul>
Раскрывается в такой:
<ul> <template ngFor let-item [ngForOf]='items'><li>{{item}}</li></template> </ul>
senyaarseniy
30.04.2016 20:30Хороший перевод спс)
Не подскажите где можно былобы достать список ивентов которые можно использовать в html файла? Обыскал весь инет ничего не могу найти(illian
30.04.2016 20:33Не совсем понял вопрос. Список директив в html-шаблоне?
senyaarseniy
30.04.2016 21:30Уже нашёл ответ, cпасибо, беруться стандартные ивенты без префикса «on» (onClick => click)
Andris_habrahabr
Очень доступно изложено, спасибо!
Ждём продолжения :)