我们需要管理多个英雄。我们将扩展《英雄指南》应用,让它显示一个英雄列表, 允许用户选择一个英雄,查看该英雄的详细信息。
当我们完成本章时,应用应该是这样的:
延续上一步教程
Where you left off
在继续《英雄指南》的第二部分之前,先来检查一下,完成第一部分之后,你是否已经有了如下目录结构。如果没有,你得先回到第一部分,看看错过了哪里。
让应用代码保持转译和运行
Keep the app transpiling and running
在控制台中敲下列命令:
npm start
这个命令会在“监听”模式下运行TypeScript编译器,当代码变化时,它会自动重新编译。 同时,该命令还会在浏览器中启动该应用,并且当代码变化时刷新浏览器。
在后续构建《英雄指南》过程中,应用能持续运行,而不用中断服务来编译或刷新浏览器。
显示我们的英雄
Displaying heroes
要显示英雄列表,我们就要先往视图模板中添加一些英雄。
创建英雄
Create heroes
我们先创建一个由十位英雄组成的数组。
src/app/app.component.ts (hero array)
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
类的实例构成的数组,我们在第一部分定义过它。
我们当然希望从一个 Web 服务中获取这个英雄列表,但别急,我们得把步子迈得小一点,先用一组模拟出来的英雄。
暴露英雄
Expose heroes
我们在AppComponent
上创建一个公共属性,用来暴露这些英雄,以供绑定。
app.component.ts (hero array property)
heroes = HEROES;
我们并不需要明确定义heroes
属性的数据类型,TypeScript 能从HEROES
数组中推断出来。
英雄的数据从实现类中分离了出来,因为最终,英雄的名字会来自一个数据服务。
在模板中显示英雄
Display hero names in a template
我们还要在模板中创建一个无序列表来显示这些英雄的名字。 那就在标题和英雄详情之间,插入下面这段 HTML 代码。
app.component.ts (heroes template)
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<!-- each hero goes here -->
</li>
</ul>
现在,我们有了一个模板。接下来,就用英雄们的数据来填充它。
通过 ngFor 来显示英雄列表
List heroes with ngFor
我们想要把组件中的heroes
数组绑定到模板中,迭代并逐个显示它们。
首先,修改<li>
标签,往上添加内置指令*ngFor
。
app.component.ts (ngFor)
<li *ngFor="let hero of heroes">
ngFor
的*
前缀表示<li>
及其子元素组成了一个主控模板。
ngFor
指令在AppComponent.heroes
属性返回的heroes
数组上迭代,并输出此模板的实例。
引号中赋值给ngFor
的那段文本表示“从heroes
数组中取出每个英雄,存入一个局部的hero
变量,并让它在相应的模板实例中可用”。
要学习更多关于ngFor
和模板输入变量的知识,参见显示数据一章的用*ngFor显示数组属性和
模板语法章的ngFor。
接着,我们在<li>
标签中插入一些内容,以便使用模板变量hero
来显示英雄的属性。
app.component.ts (ngFor template)
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
当浏览器刷新时,我们就看到了英雄列表。
给我们的英雄们“美容”
Style the heroes
当用户的鼠标划过英雄或选中一个英雄时,我们得让他/她看起来醒目一点。
要想给我们的组件添加一些样式,请把@Component
装饰器的styles
属性设置为下列 CSS 类:
src/app/app.component.ts (styles)
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;
}
`]
注意,我们又使用了反引号语法来书写多行字符串。
添加这些样式会让此文件变得更长。在后面的章节中,我们将会把这些样式移到单独的文件中去。
当我们为一个组件指定样式时,它们的作用域将仅限于该组件。
上面的例子中,这些样式只会作用于AppComponent
组件,而不会“泄露”到外部 HTML 中。
用于显示英雄们的模板应该是这样的:
src/app/app.component.ts (styled heroes)
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
选择英雄
Selecting a hero
我们的应用已经有了英雄列表和单个英雄的详情视图。 但列表和单独的英雄之间还没有任何关联。 我们希望用户在列表中选中一个英雄,然后让这个被选中的英雄出现在详情视图中。 这种 UI 布局模式,通常被称为“主从结构”。 在这个例子中,主视图是英雄列表,从视图则是被选中的英雄。
接下来,我们要通过组件中的一个selectedHero
属性来连接主从视图,它被绑定到了点击事件上。
处理点击事件
Handle click events
我们再往<li>
元素上插入一句点击事件的绑定代码:
app.component.ts (template excerpt)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
...
</li>
圆括号标识<li>
元素上的click
事件是绑定的目标。
等号右边的onSelect(hero)
表达式调用AppComponent
的onSelect()
方法,并把模板输入变量hero
作为参数传进去。
它是我们前面在ngFor
指令中定义的那个hero
变量。
添加点击处理器以暴露选中的英雄
Add a click handler to expose the selected hero
我们不再需要AppComponent
的hero
属性,因为不需要再显示单个的英雄,我们只需要显示英雄列表。但是用户可以点选一个英雄。
所以我们要把hero
属性替换成selectedHero
属性。
src/app/app.component.ts (selectedHero)
selectedHero: Hero;
在用户选取一个英雄之前,所有的英雄名字都应该是未选中的。所以我们不希望像hero
一样初始化selectedHero
变量。
现在,添加一个onSelect
方法,用于将用户点击的英雄赋给selectedHero
属性。
src/app/app.component.ts (onSelect)
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
我们将把所选英雄的详细信息显示在模板中。目前,它仍然引用之前的hero
属性。
我们这就修改模板,让它绑定到新的selectedHero
属性。
app.component.ts (template excerpt)
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
使用 ngIf 隐藏空的详情
Hide the empty detail with ngIf
当应用加载时,我们会看到一个英雄列表,但还没有任何英雄被选中。
selectedHero
属性是undefined
。
因此,我们会看到浏览器控制台中出现下列错误:
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
虽然我们要在模板中显示的是selectedHero.name
,但在选中了一个英雄之前,我们必须让这些英雄详情留在DOM之外。
我们可以把模板中的英雄详情内容区放在一个<div>
中。
然后,添加一个ngIf
内置指令,把ngIf
的值设置为组件的selectedHero
属性。
src/app/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
前的星号 (*
)。
应用不再出错,而名字列表也再次显示在浏览器中。
当没有选中英雄时,ngIf
指令会从 DOM 中移除表示英雄详情的这段 HTML 。
没有了表示英雄详情的元素,也就不用担心绑定问题。
当用户选取了一个英雄,selectedHero
变成了“已定义的”值,于是ngIf
把英雄详情加回 DOM 中,并计算它所嵌套的各种绑定。
给所选英雄添加样式
Style the selected hero
我们在下面的详情区看到了选中的英雄,但是我们还是没法在上面的列表区快速定位这位英雄。
在我们前面添加的styles
元数据中,有一个名叫selected
的自定义CSS类。
要想让选中的英雄更加醒目,当用户点击一个英雄名字时,我们要为<li>
添加selected
类。
例如,当用户点击“Magneta”时,它应该使用不一样的醒目的背景色。

在这个模板中,往<li>
上添加一个[class.selected]
绑定:
app.component.ts (setting the CSS class)
[class.selected]="hero === selectedHero"
当表达式(hero === selectedHero
)为true
时,Angular会添加一个CSS类selected
。为false
时则会移除selected
类。
关于[class]
绑定的更多信息,参见模板语法。
The final version of the <li>
looks like this:
app.component.ts (styling each hero)
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
浏览器重新加载了我们的应用。 我们选中英雄 Magneta,通过背景色的变化,它被清晰的标记出来。

完整的app.component.ts
文件如下:
src/app/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;
}
}
已走的路
The road you've travelled
在本章中,我们完成了以下内容:
-
我们的《英雄指南》现在显示一个可选英雄的列表
-
我们可以选择英雄,并显示这个英雄的详情
-
我们学会了如何在组件模板中使用内置的
ngIf
和ngFor
指令
前方的路
The Road Ahead
我们的《英雄指南》长大了,但还远远不够完善。 我们显然不能把整个应用都放进一个组件中。 我们需要把它拆分成一系列子组件,然后教它们协同工作, 就像我们将在下一章学到的那样。