Master/Detail
展示更多英雄
我们的故事需要更多英雄。我们将会扩展英雄之旅app来展示一个英雄列表,允许用户选择一个英雄并且展示英雄的详细信息。
运行这个在线示例。
让我们整理一下我们所需要展示的英雄列表。首先,我们需要一个英雄列表。我们希望在视图模板中展示那些英雄,因此我们将需要一个可行的方法实现它。
从哪里开始
在我们进行英雄之旅第2部分之前,让我们先验证一下第1部分的文件结构。如果不对,我们需要返回上一部分查找缺失了什么。
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.component.ts
底部创建一个包含10个英雄的数组。
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
, 它定义在第1部分。我们本希望从一个web service中获取英雄列表,但让我们先简单用模拟的英雄数据进行展示。
暴露英雄
让我们在AppComponent
中创建一个公共属性用来绑定英雄列表。
public heroes = HEROES;
我们不需要定义heroes
类型,TypeScript可以从HEROES数组中继承类型。
我们可以在组件类中定义英雄列表,但是我们知道最终我们将会从一个数据service获取英雄数据。由于我们知道将来会如何改动,所以在一开始将英雄数据定义与类实现分离是有意义的。
在template中展示英雄数据
现在我们的组件有heroes
了,让我们在template中创建一个无序列表展示它们。我们将会在title与hero详情之间插入以下html代码片段。
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<!-- each hero goes here -->
</li>
</ul>
现在我们可以填充我们的英雄了。
使用ngFor列举英雄
我们希望在组件中把heroes
数据与template绑定,迭代它们,并且分别展示。我们需要Angular来帮助完成这些。让我们一步一步进行。
首先修改<li>
标签,添加内置指令*ngFor
<li *ngFor="let hero of heroes">
ngFor
前面的的星号(*)是语法中极其重要的部分。
ngFor
之前的*
暗示<li>
元素和它的子元素构成一个主模板。
ngFor
指令迭代AppComponent.heroes
属性返回的heroes
数组。赋值给
ngFor
的引号中的文内容意思是“获取heroes
数组中的每一个hero,保存在localhero
变量中,并且使之让相对应的模板实例可访问。”“hero”之前的
let
关键字标识hero
作为一个模板输入变量。我们可以在模板引用这个变量来访问hero的属性。想要学习更多关于
ngFor
和模板输入变量只是,请访问Displaying Data和Template Syntax章节。
现在我们来在<li>
标签之间插入一些内容,它将使用hero
模板变量来展示hero的属性。
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
刷新浏览器后,我们就能看到英雄列表了!
为英雄添加样式
我们的英雄列表看起来相当单调。当我们将鼠标悬停在上面以及选择英雄的时候,我们想要使之在视觉上对用户更加显眼。
让我们在@Component
装饰器中通过设置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。
现在我们展示英雄的template看起来是这样的:
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
选择一个英雄
现在我们有一个英雄列表以及单个英雄展示在app上。这个列表和单个的英雄并没有任何的联系。我们希望用户从列表中选择一个英雄,并且让这个选中的英雄展示在详情视图中。这个UI模式就是众所周知的“主从模式”。在我们的示例中,主即英雄列表,从即选中的英雄详情。
现在让我们通过将selectedHero
组件属性绑定到一个鼠标点击事件来将主从关联起来。
点击事件
我们来修改<li>
元素,插入一个绑定到点击事件的Angular事件
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
关注下事件绑定
(click)="onSelect(hero)"
小括号标识出<li>
元素的click
事件作为绑定目标。表达式等号右边调用了AppComponent
的方法onSelect()
,同时传递了模板输入变量hero
作为参数。它与我们之前定义在ngFor
中的hero
变量是一致的。
想要学习更多关于时间绑定的只是,请访问User Input和Templating Syntax章节。
添加鼠标点击处理程序
事件所绑定的onSelect
方法尚未添加,我们现在要把这个方法添加到组件中。
那么这个方法应该做些什么呢?它会设置组件选中的hero为用户所点击的那一个。
我们的组件尚未拥有一个“选中的英雄”,我们将从这里入手。
暴露选中的英雄
我们现在不需要AppComponent
的hero
静态属性了,把它替换为下面这个简单的selectedHero
属性:
selectedHero: Hero;
我们已经明确在用户选择一个英雄之前不默认设置选中的英雄,因此我们就不再像hero
那样初始化selectedHero
了。
现在添加一个onSelect
方法,在其中设置selectedHero
属性为用户点击的hero
。
onSelect(hero: Hero) { this.selectedHero = hero; }
接着我们将在template中展示选中英雄的详情。此时,template仍然引用着老的hero
属性,让我们修改template使之绑定到新的selectedHero
属性。
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
使用ngIf隐藏空详情
当app加载后,我们看到一个英雄列表,但并没有选中任何英雄,此时selectedHero
为undefined
。这就是为什么我们会在浏览器console中看到如下错误:
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
记得之前我们在template展示了selectedHere.name
,然而由于selectedHero
自身是undefined,name属性并不存在。
我们将通过保持英雄详情在DOM之外直到显式选择一个英雄来解决这个问题。
让我们来用一个<div>
包裹英雄详情模板的html。接着添加内置指令ngIf
,并且赋值为组件的selectedHero
属性。
<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
指令从DOM中移除了英雄详情html。这样就不会有英雄详情页面元素,也不用担心没有绑定项了。
当用户选中一个英雄,selectedHero
变为"true",ngIf
把英雄详情内容放入DOM中,并且为赋值给嵌套的绑定项。
ngIf
和ngFor
都叫做“结构性指令”, 因为他们可以改变DOM的部分结构。换句话说,它们把结构给了Angular,使之在DOM展示内容。想要学习更多关于
ngIf
,ngFor
和其他结构性指令的知识,请访问Structural Directives和Template Syntax章节。
刷新浏览器我们会看到英雄列表单而不是英雄详情。只要selectedHero
为undefined,ngIf
就会确保它在DOM之外。当我们点击列表中的一个英雄时,英雄详情视图就会显示选中的英雄信息。一切都如我们期望的那样运行。
给选中项添加样式
我们可以看到下方详情区域里选中的英雄,但是我们并不能在上方列表中快速定位相对应的英雄。我们可以通过给对应的<li>
附加selected
css class来修复这个问题。比如,当我们选中列表中的Magneta,我们可以通过设置背景色使之视觉上凸显,如下图所示。
我们将在template中为selected
class添加一个关联到class
的属性绑定。我们把这个属性赋值为一个表达式,将当前的selectedHero
与hero
比较。
key是CSS class的名称(selected
)。如果这两个hero相匹配,那么值为true
,反之为false
。也就是说“如果hero相匹配则添加selected
class,反之则移除。”
[class.selected]="hero === selectedHero"
注意到template中的class.selected
包裹在中括号([]
)里。这是一个属性绑定的语法,这个绑定使得数据从数据源(表达式hero===selectedHero
)单向流动到class
的一个属性中。
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
想要学习更多关于Property Bingdings的知识,请访问Template Syntax章节
刷新浏览器,我们选择英雄Magneta后,选中项就会清楚地被背景色标识出来。
现在完整的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) { this.selectedHero = hero; }
}
回顾历程
本章我们达成了如下目标:
- 英雄之旅现在能显示一个可选择的英雄俩表
- 我们添加了选中英雄并展示英雄详情的功能
- 我们学习了如何在template中使用内置指令
ngIf
和ngFor
运行在线实例来演示这一部分。