我们构建一个主从结构的页面,用于展现英雄列表
延续上一步教程
让应用代码保持转译和运行
我们要启动 TypeScript 编译器,它会监视文件变更,并启动开发服务器。我们只要敲:
npm start
这个命令会在我们构建《英雄指南》的时候让应用得以持续运行。
显示我们的英雄
创建英雄
我们先创建一个由十位英雄组成的数组。
/app/app.component.ts
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 服务中获取这个英雄列表,但别急,我们得把步子迈得小一点,先用一组模拟出来的英雄。
暴露英雄
我们在AppComponent
上创建一个公共属性,用来暴露这些英雄,以供绑定。
暴露英雄
我们在AppComponent
上创建一个公共属性,用来暴露这些英雄,以供绑定。
/app/app.component.ts
export class AppComponent{ heros=HEROS; }
我们并不需要明确定义heros属性的数据类型,TypeScript能从HEROS数组中推断出来。
我们已经把英雄列表定义在了这个组件类中。 但显然,我们最终还是得从一个数据服务中获取这些英雄。 正因如此,一开始就应该把英雄数据隔离到一个类中来实现,以便于后期我们可以随时从一个服务中获取数据来替换。
在模板中显示英雄
我们的组件有了heroes
属性,我们再到模板中创建一个无序列表来显示它们。 我们将在标题和英雄详情之间,插入下面这段 HTML 代码。
/app/app.component.ts
<h2>My Heroes</h2> <ul class="heroes"> <li> <!-- each hero goes here --> </li> </ul>
现在,我们有了一个模板。接下来,就用英雄们的数据来填充它。
通过 ngFor 来显示英雄列表
我们想要把组件中的heroes
数组绑定到模板中,迭代并逐个显示它们。 这下,我们就得借助 Angular 了。我们来一步步实现它!
首先,修改<li>
标签,往上添加内置指令*ngFor
。
<li *ngFor="let hero of heros">
ngFor
的前导星号(*
)是此语法的重要组成部分
ngFor
的*
前缀表示<li>
及其子元素组成了一个主控模板。
ngFor
指令在AppComponent.heroes
属性返回的heroes
数组上迭代,并输出此模板的实例。
引号中赋值给ngFor
的那段文本表示“从heroes
数组中取出每个英雄,存入一个局部的hero
变量,并让它在相应的模板实例中可用”。
hero
前的let
关键字表示hero
是一个模板输入变量。 在模板中,我们可以引用这个变量来访问一位英雄的属性。
要学习更多关于ngFor
和模板输入变量的知识,见显示数据和 模板语法。
现在,我们在<li>
标签中插入一些内容,以便使用模板变量hero
来显示英雄的属性。
/app/app.component.ts
<li *ngFor="let hero of heroes"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li>
当浏览器刷新时,我们就看到了英雄列表。
给我们的英雄们“美容”
我们的英雄列表看起来实在是稀松平常。 但当用户的鼠标划过英雄或选中了一个英雄时,我们得让它/她看起来醒目一点。
要想给我们的组件添加一些样式,请把@Component
装饰器的styles
属性设置为下列 CSS 类:
/app/app.component.ts
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 中。
用于显示英雄们的模板看起来像这样:
<h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul>
选择英雄
在我们的应用中,已经有了英雄列表以及一个单独的英雄。 但列表和单独的英雄之间还没有任何关联。 我们希望用户在列表中选中一个英雄,然后让这个被选中的英雄出现在详情视图中。 这种 UI 布局模式,通常被称为“主从结构”。 在这个例子中,主视图是英雄列表,从视图则是被选中的英雄。
我们通过组件中的一个selectedHero
属性来连接主从视图,它被绑定到了点击事件上。
我们往<li>
元素上插入一句 Angular 事件绑定代码,绑定到它的点击事件。
看起来是这样的。<li *ngFor=‘let hero of heroes‘ (click)="onSelect(hero)">
事件绑定详解
(click)="onSelect(hero)"
圆括号表示<li>
元素上的click
事件是绑定的目标。 等号右边的表达式调用AppComponent
的onSelect()
方法,并把模板输入变量hero
作为参数传进去。 它是我们前面在ngFor
中定义的那个hero
变量。
添加点击处理器
我们的事件绑定引用了onSelect
方法,但它还不存在。 我们现在就把它添加到组件上。
这个方法该做什么?它应该把组件中被选中的英雄设置为用户刚刚点击的那个。
我们的组件还没有用来表示“当前选中的英雄”的变量,我们就从这一步开始。
暴露选中的英雄
我们不再需要AppComponent
的hero
属性。 把它替换成selectedHero
属性。
selectedHero: Hero;
我们决定在用户选取之前,不会默认选择任何英雄,所以,不用像hero
一样初始化selectedHero
变量。
现在,添加一个onSelect
方法,用于将用户点击的英雄赋给selectedHero
属性。
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
我们将把所选英雄的详细信息显示在模板中。目前,它仍然引用之前的hero
属性。 我们这就修改模板,让它绑定到新的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 隐藏空的详情
当应用加载时,我们会看到一个英雄列表,但还没有任何英雄被选中。 selectedHero
属性是undefined
。 因此,我们会看到浏览器控制台中出现下列错误:
EXCEPTION: TypeError: Cannot read property ‘name‘ of undefined in [null]
别忘了我们要在模板中显示的是selectedHero.name
。 显然,这个 name 属性是不存在的,因为selectedHero
本身还是undefined
呢。
要处理这个问题,我们可以先让英雄详情不要出现在 DOM 中,直到有英雄被选中。
我们把模板中的英雄详情内容区用放在一个<div>
中。 然后,添加一个ngIf
内置指令,把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>
当没有selectedHero
时,ngIf
指令从 DOM 中移除表示英雄详情的这段 HTML 。 没有了表示英雄详情的元素,也就不用担心绑定问题。
当用户选取了一个英雄,selectedHero
变成了“真”值,于是ngIf
把英雄详情加回 DOM 中,并计算它所嵌套的各种绑定。
注:ngIf
和ngFor
被称为“结构型指令”,因为它们可以修改部分 DOM 的结构。 换句话说,它们让 Angular 在 DOM 中显示内容的方式结构化了。
浏览器刷新了,我们看到了一个英雄列表,但没有显示选中的英雄详情。 当selectedHero
是undefined
时,ngIf
会保证英雄详情不出现在 DOM 中。 当我们从列表中点击一个英雄时,选中的英雄被显示在英雄详情里。 正如我们所预期的那样。
给所选英雄添加样式
我们在下面的详情区看到了选中的英雄,但是我们还是没法在上面的列表区快速地定位这位英雄。 可以通过在主列表的相应<li>
元素上添加 CSS 类selected
来解决这个问题。 例如,当我们在列表区选中了 Magneta 时,我们可以通过设置一个轻微的背景色来让它略显突出。
在模块中,我们在class
上为selected
类添加一个属性绑定。我们把绑定表达式设置为selectedHero
和hero
的比较结果。
键是 CSS 类的名字 (selected
)。当两位英雄一致时,值为true
,否则为false
。 也就是说:“当两位英雄匹配时,应用上selected
类,否则不应用”。
[class.selected]="hero === selectedHero"
注意,模板中的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>
浏览器重新加载了我们的应用。 我们选中英雄 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): void { this.selectedHero = hero; } }
已走的路
在本章中,我们完成了以下内容:
-
我们的《英雄指南》现在显示一个可选英雄的列表
-
我们可以选择英雄,并显示这个英雄的详情
-
我们学会了如何在组件模板中使用内置的
ngIf
和ngFor
指令,以及属性指令的写法[class.selected]="hero===selectedHero".