版本:v4.0.0+2
在这一章,你将扩展英雄指南应用来显示一列英雄,并允许用户来选择一个英雄,同时显示这个英雄的详情。
当你按照本章完成时,应用应该看起来这样——在线示例 (查看源码)。
我们离开的地方
在继续本章的英雄指南之前,先来检查一下是否有下面的结构。如果不是,你得先回到前一章英雄编辑器,看看错过了什么。
如果应用不运行了,启动应用。当你做出修改时,通过刷新浏览器保持继续运行。
应用重构
在添加新的应用之前,你将从重构应用标题受益。
应用模板文件
你将对 app 组件的模板做几个更新。首先,移动模板到自己的文件:
// lib/app_component.html
<h1>{{title}}</h1>
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name">
</div>
使用templateUrl
引用新的模板文件替换@Component template
:
// lib/app_component.dart (metadata)
@Component(
selector: 'my-app',
templateUrl: 'app_component.html',
directives: const [formDirectives],
)
刷新浏览器。应用仍然运行。
英雄类
从app_component.dart
分离Hero
类到它自己的文件。
创建lib/src
目录包含Hero
资源:
// lib/src/hero.dart
class Hero {
final int id;
String name;
Hero(this.id, this.name);
}
回到 app 组件,使用相对路径添加新创建文件的导入:
// lib/app_component.dart (hero import)
import 'src/hero.dart';
刷新浏览器。应用仍然运行,现在开始准备添加新的特性。
显示英雄
要显示一列英雄,添加 heroes 到视图模板。
模拟英雄
在lib/src
目录下创建如下一个由十位英雄组成的列表的文件。
// lib/src/mock_heroes.dart
import 'hero.dart';
final mockHeroes = <Hero>[
new Hero(11, 'Mr. Nice'),
new Hero(12, 'Narco'),
new Hero(13, 'Bombasto'),
new Hero(14, 'Celeritas'),
new Hero(15, 'Magneta'),
new Hero(16, 'RubberMan'),
new Hero(17, 'Dynama'),
new Hero(18, 'Dr IQ'),
new Hero(19, 'Magma'),
new Hero(20, 'Tornado')
];
在最终,应用会从一个 web 服务器获取英雄列表,但现在你可以显示模拟应用。
应用的 heroes 字段
在AppComponent
中使用公共的heroes
字段替换hero
字段,并使用模拟英雄(不要忘记导入)初始化它:
// lib/app_component.dart (heroes)
import 'src/mock_heroes.dart';
// ···
class AppComponent {
final title = 'Tour of Heroes';
List<Hero> heroes = mockHeroes;
// ···
}
英雄数据是由分开的不同类实现的,因为最终,英雄名将会来自服务器数据。
在模板中显示英雄名
在一个无序列表中显示英雄名,使用下面的 HTML 代替 所有当前模板:
// lib/app_component.html (heroes template)
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<!-- each hero goes here -->
</li>
</ul>
下一步,你将添加英雄名。
使用 ngFor 来列出英雄
目标是绑定组件的 heroes 列表到模板,迭代它们,并单独显示它们。
修改<li>
标签,添加核心指令 *ngFor
。
<li *ngFor="let hero of heroes">
ngFor
的前缀星号(*
)是这个语法的关键部分。它表明<li>
元素及其子元素组成了一个主控模板。
ngFor
指令遍历组件的heroes
列表,并按照这个模板渲染列表中每个英雄。表达式的
let hero
部分识别hero
为模板输入变量,为每一个迭代保存当前英雄条目。你可以在模板中引用这个变量来访问当前英雄的属性。
在<li>
元素内,使用模板变量hero
来添加内容,显示英雄的属性。
// lib/app_component.html (ngFor)
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
要在模板中使用 Angular 指令,需要在组件的@Component
注解的directives
参数中列出它们。类似于在前一章做的,添加 CORE_DIRECTIVES:
// lib/app_component.dart (directives)
@Component(
selector: 'my-app',
// ···
directives: const [CORE_DIRECTIVES, formDirectives],
)
刷新浏览,一列英雄出现了。
给英雄们添加样式
用户应该获取视觉提示,他们悬停在哪个英雄上,哪个英雄被选中。
通过设置@Component
注解的styles
参数,来给组件添加样式:
// lib/app_component.dart (styles)
// 当添加许多 CSS 类时不推荐
styles: const [
'''
.selected { ... }
.heroes { ... }
...
'''
],
但是当添加许多样式时,会使 Dart 文件变长且难以阅读。相反,把样式放到一个.css
文件中,然后在@Component
的styleUrls
参数中引用该文件。按照惯例,组件的 CSS 文件名和 Dart 文件有相同的基础(app_component
)。
// lib/app_component.css
.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 {
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #EEE;
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。
显示英雄的模板看起来像这样:
// lib/app_component.html (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>
选择英雄
应用现在显示一列英雄,同时在详情视图显示一个单独的英雄。但列表和详情视图之间没有关联。当用户从列表选中一个英雄时,选中的英雄应该出现在详情视图中。这种 UI 模式被称为“master/detail.”。在这个例子中,master 是英雄列表,detail 是被选中的英雄。
接下来,通过组件的selectedHero
属性来连接主从视图,它被绑定到一个点击事件。
处理点击事件
如下添加一个点击事件绑定到<li>
元素:
// lib/app_component.html (click)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
圆括号指定了<li>
元素的click
事件为目标。onSelect(hero)
表达式调用AppComponent
的onSelect()
方法,它传递模板输入变量hero
作为参数。和之前在ngFor
指令中定义的是同一个hero
变量。
添加一个点击处理器来暴露选中的英雄
你不再需要hero
属性,因为你不再显示一个单独的英雄;你显示一列英雄。但用户能够通过点击它来选择其中一个英雄。所以使用这个简单的selectedHero
属性代替hero
属性。
// lib/app_component.dart (selectedHero)
Hero selectedHero;
在用户选择一个英雄之前,所有英雄都是未被选中的,所以,你不需要像hero
一样初始化selectedHero
。
添加一个onSelect()
方法,将用户点击的hero
赋值给electedHero
属性。
// lib/app_component.dart (onSelect)
void onSelect(Hero hero) => selectedHero = hero;
模板仍然引用旧的hero
属性。如下所示,绑定到新的selectedHero
属性代替它:
// lib/app_component.html (selectedHero details)
<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
是 null。当用户点击英雄名字的时候,selectedHero
才会被初始化。Angular 不能显示空的selectedHero
的属性,并且抛出如下的错误,可以在浏览器控制台中查看:
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
尽管selectedHero.name
显示在了模板中,但在有一个选中的英雄之前,英雄详情不会出现在 DOM 中。
模板中的英雄详情 HTML 内容使用一个<div>
包裹起来。然后,添加ngIf
核心指令,并把它设置为selectedHero != null
。
// lib/app_component.html (ngIf)
<div *ngIf="selectedHero != null">
<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
不再是null
,ngIf
把英雄详情添加到 DOM 中,并且对嵌套的绑定进行求值计算。
给选中英雄添加样式
虽然选中英雄的详情出现在了列表下面,但很难在列表中识别出选中的英雄。
在之前添加的styles
元数据中,有一个自定义的 CSS 类selected
。要使选中英雄更明显,当用户点击一个英雄名时,把selected
类应用到<li>
上。例如,当用户点击 “Magneta” 时,它应该用一个略有不同的背景色显示出来,就像这样:
在模板中,如下所示,添加绑定到<li>
标签:
[class.selected]="hero === selectedHero"
当表达式(hero === selectedHero
)是true
时,Angular 添加selected
CSS 类。当表达式是false
,Angular 移除selected
类。
===
操作符测试给出的对象是否完全相等。更多关于
[class]
绑定的内容请看模板语法指南。
最终版的<li>
看起来如下:
// lib/app_component.html (ngFor with class.selected)
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
点击 “Magneta” 后,列表看起来如下:
回顾应用结构
你的应用应该有如下文件:
教程测试组件
本教程没有包含测试,如果你查看示例代码,它有对本教程添加的每个新特性的组件测试。查看 Component Testing 了解更多信息。
你已走过的路
在本页你完成了以下内容:
- 英雄指南应用显示一列可选英雄。
- 移动应用的模板到它自己的文件。
- 移动
Hero
类到lib/src
下它自己的文件。 - 添加选择英雄并且显示英雄详情的功能。
- 学习在组件的模板中,如何使用核心指令
ngIf
和ngFor
。 - 在 CSS 文件中定义样式,并使用它们给应用添加样式。
下一步