Multiple Components
我们的app正变得更丰富,接下来我们要做的是:可重用组件,传递数据到组件以及创建更多的可重用资源。让我们把英雄列表和英雄详情分离,使其可重用。
运行在线示例演示这一部分。
从哪里开始
在我们继续英雄之旅之前,让我们先验证以下的文件结构。如果不对,我们需要返回上一章重新学习。
angular2-tour-of-heroes
├── app
│ ├── app.component.ts
│ └── main.ts
├── node_modules ...
├── typings ...
├── index.html
├── package.json
├── styles.css
├── systemjs.config.js
├── tsconfig.json
└── typings.json
保持app运行
我们想要运行TypeScript编译器,让它监控文件变更,并且运行服务器。为此我们将执行命令:
npm start
这个命令将运行监控模式的编译器,启动服务器,在浏览器中打开app,并保证app在我们不断创建英雄之旅时运行。
创建一个英雄详情组件
英雄列表与英雄详情现在处于同一个组件、同一个文件中,虽然它们内容很少但以后可能会变得更复杂,我们总是会收到新的需求。所有的变更都让组件面临风险,承担双倍的测试压力。如果我们准备在app其他地方重用英雄详情,英雄列表部分也会一同被重用。
我们目前的组件违背了单一职责原则。虽然这仅仅是一个教程,但我们仍然要做正确的事 — 特别是如果把事做对相当简单且我们可以同时学习如何创建Angular应用。
让我们开始分离英雄详情到一个单独的组件。
分离英雄详情组件
在app
目录下添加一个名为hero-detail.component.ts
的新文件,并如下创建HeroDetailComponent
。
import { Component, Input } from '@angular/core';
@Component({
selector: 'my-hero-detail',
})
export class HeroDetailComponent {
}
命名约定
我们希望迅速识别出哪些类是组件,哪些文件包含组件。
注意到我们在文件
app.component.ts
中有个AppComponent
组件,并且我们的新组件HeroDetilComponent
包含于文件hero-detail.component.ts
。我们所有的组件名称都以“Component”结尾。所有的组件文件都以“.component”结尾。
我们的文件名使用小写的dash case(又称 kebab-case)命名,因此我们不需要担心服务器或版本控制工具的字母大小写问题。
我们以引入Angular的Component
和Input
装饰器作为开始,我们马上就会需要用到它们。
我们用@Component
装饰器创建元数据,在其中指定标识了这个组件元素的选择器名称。接着我们export这个类使其对其他组件可访问。
当我们做完这一步,我们将会把它引入到AppComponent
并且创建一个想对应的<my-hero-detail>
元素。
英雄详情Template
此时,英雄列表和英雄详情视图正合并在AppComponent
中的一个template中。让我们将英雄详情内容从AppComponent
剪切,并且粘贴到HeroDetailComponent
中的template属性里。
之前template绑定了AppComponent
的selectedHero.name
属性。HeroDetailComponent
将会有个hero
属性,而不是selectedHero
属性。因此我们在template中把所有的selectedHero
替换为hero
。这是我们做的唯一的变动,结果看起来是这样的:
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
文件中来解决这个问题。
export class Hero {
id: number;
name: string;
}
我们在hero.ts
文件中exportHero
类,因为我们在所有组件中都需要引用这它。在app.component.js
和hero-detail.component.js
顶部都添加以下引入语句。
import { Hero } from './hero';
hero属性是一个input。
HeroDetailComponent
必须被告知需要展示哪一个英雄。那么谁来告诉它?AppComponent
!
AppComponent
知道需要展示哪个英雄:就是用户从列表中选择的那个。用户的选择保存在selectedHero
属性中。
我们将会更新AppComponent
的template,使它的selectedHero
属性绑定到HeroDetailComponent
的hero
属性。代码看起来是这样的:
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
注意到hero
属性是属性绑定的target —— 它在等号左边的中括号里。
Angular坚持认为我们要s声明一个target属性作为一个input属性。假如不这样做,Angular将拒绝绑定并且抛出一个错误。
我们在这里描述更多关于input属性的知识,同时描述了为什么target属性需要这种特殊对待,同样对待的还有source属性。
我们有好几种方法来声明hero
成为一个input, 我们会用我们更推崇的方式来做,通过用我们之前引入的@Input装饰器标注hero
属性。
@Input()
hero: Hero;
想要学习更多关于
@Input()
装饰器的知识,请访问Attribute Directives章节。
更新AppComponent
我们回到AppComponent
并且教它如何利用HeroDetailComponent
。
首先引入HeroDetailComponent
,是我们可以引用它。
import { HeroDetailComponent } from './hero-detail.component';
在template找到我们移除英雄详情的位置,接着添加一个代表HeroDetailComponent
的元素
<my-hero-detail></my-hero-detail>
my-hero-detail是我们在
HeroDetailComponent
元数据中定义的selector
名称。
然后绑定AppComponent
的selectedHero
属性到HeroDetailComponent
元素的hero
属性,像这样:
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
现在AppComponent
的template看起来应该是这样的:
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
接收英雄并在列表下方展示英雄详情。每次用户选择一个新的英雄,详情信息都会跟着更新。
但上述行为还没有实现。
我们点击不同英雄后,没有详情显示。我们在浏览器console中查找错误信息,然而并没任何错误。
就好像Angular忽略了这个新的标签。实际上正是因为它忽略了这个新标签。
directives 数组
浏览器将会忽略它不能识别的HTML标签和属性,Angular也是这样。
我们已经引入了HeroDetailComponent
,我们已经在template使用了它,但是我们还没告知Angular。
我们需要把它列在元数据diectives
数组中来通知Angular。在@Component
配置对象的底部,紧接着template
和styles
属性,添加这个数组属性。
directives: [HeroDetailComponent]
成功了!
当我们在浏览器中查看app,我们将看到一个英雄列表。当我们选择一个英雄时,将看到选中英雄的详情。
最重要的是,我们可以用HeroDetailComponent
在app的任何地方展示英雄详情。
我们已经创建了第一个可重用组件!
回顾App结构
现在我们来验证下目前的文件结构:
angular2-tour-of-heroes
├── app
│ ├── app.component.ts
│ ├── hero.ts
│ ├── hero-detial.component.ts
│ └── main.ts
├── node_modules ...
├── typings ...
├── index.html
├── package.json
├── styles.css
├── systemjs.config.js
├── tsconfig.json
└── typings.json
以下是此章最终的代码文件:
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;
}
app/app.component.ts
import { Component } from '@angular/core';
import { Hero } from './hero';
import { HeroDetailComponent } from './hero-detail.component';
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;
}
`],
directives: [HeroDetailComponent]
})
export class AppComponent {
title = 'Tour of Heroes';
heroes = HEROES;
selectedHero: Hero;
onSelect(hero: Hero) { this.selectedHero = hero; }
}
app/hero.js
export class Hero {
id: number;
name: string;
}
回顾历程
让我们盘点下已经完成的部分。
- 创建了一个可重用组件
- 学习如何使组件接收input
- 学习绑定父子组件
- 学习在
directives
数组中声明所需的应用指令
运行在线示例来演示这部分内容。