主从结构

我们需要管理多个英雄。我们将扩展《英雄指南》应用,让它显示一个英雄列表, 允许用户选择一个英雄,查看该英雄的详细信息。

当我们完成本章时,应用应该是这样的:在线例子 / 可下载的例子

延续上一步教程

在继续《英雄指南》的第二部分之前,先来检查一下,完成第一部分之后,你是否已经有了如下目录结构。如果没有,你得先回到第一部分,看看错过了哪里。

angular-tour-of-heroes
src
app
app.component.ts
app.module.ts
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json

让应用代码保持转译和运行

在控制台中敲下列命令:

npm start

这个命令会在“监听”模式下运行TypeScript编译器,当代码变化时,它会自动重新编译。 同时,该命令还会在浏览器中启动该应用,并且当代码变化时刷新浏览器。

在后续构建《英雄指南》过程中,应用能持续运行,而不用中断服务来编译或刷新浏览器。

显示我们的英雄

要显示英雄列表,我们就要先往视图模板中添加一些英雄。

创建英雄

我们先创建一个由十位英雄组成的数组。

src/app/app.component.ts (hero array)

  1. const HEROES: Hero[] = [
  2. { id: 11, name: 'Mr. Nice' },
  3. { id: 12, name: 'Narco' },
  4. { id: 13, name: 'Bombasto' },
  5. { id: 14, name: 'Celeritas' },
  6. { id: 15, name: 'Magneta' },
  7. { id: 16, name: 'RubberMan' },
  8. { id: 17, name: 'Dynama' },
  9. { id: 18, name: 'Dr IQ' },
  10. { id: 19, name: 'Magma' },
  11. { id: 20, name: 'Tornado' }
  12. ];

HEROES是一个由Hero类的实例构成的数组,我们在第一部分定义过它。 我们当然希望从一个 Web 服务中获取这个英雄列表,但别急,我们得把步子迈得小一点,先用一组模拟出来的英雄。

暴露英雄

我们在AppComponent上创建一个公共属性,用来暴露这些英雄,以供绑定。

app.component.ts (hero array property)

heroes = HEROES;

我们并不需要明确定义heroes属性的数据类型,TypeScript 能从HEROES数组中推断出来。

英雄的数据从实现类中分离了出来,因为最终,英雄的名字会来自一个数据服务。

在模板中显示英雄

我们还要在模板中创建一个无序列表来显示这些英雄的名字。 那就在标题和英雄详情之间,插入下面这段 HTML 代码。

app.component.ts (heroes template)

<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <!-- each hero goes here -->
  </li>
</ul>

现在,我们有了一个模板。接下来,就用英雄们的数据来填充它。

通过 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>

当浏览器刷新时,我们就看到了英雄列表。

给我们的英雄们“美容”

当用户的鼠标划过英雄或选中一个英雄时,我们得让他/她看起来醒目一点。

要想给我们的组件添加一些样式,请把@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>

选择英雄

我们的应用已经有了英雄列表和单个英雄的详情视图。 但列表和单独的英雄之间还没有任何关联。 我们希望用户在列表中选中一个英雄,然后让这个被选中的英雄出现在详情视图中。 这种 UI 布局模式,通常被称为“主从结构”。 在这个例子中,主视图是英雄列表,从视图则是被选中的英雄。

接下来,我们要通过组件中的一个selectedHero属性来连接主从视图,它被绑定到了点击事件上。

处理点击事件

我们再往<li>元素上插入一句点击事件的绑定代码:

app.component.ts (template excerpt)

<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
  ...
</li>

圆括号标识<li>元素上的click事件是绑定的目标。 等号右边的onSelect(hero)表达式调用AppComponentonSelect()方法,并把模板输入变量hero作为参数传进去。 它是我们前面在ngFor指令中定义的那个hero变量。

关于事件绑定的更多内容,参见: 用户输入页 和 模板语法页的事件绑定节。

添加点击处理器以暴露选中的英雄

我们不再需要AppComponenthero属性,因为不需要再显示单个的英雄,我们只需要显示英雄列表。但是用户可以点选一个英雄。 所以我们要把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 隐藏空的详情

当应用加载时,我们会看到一个英雄列表,但还没有任何英雄被选中。 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 中,并计算它所嵌套的各种绑定。

要了解更多ngIfngFor和其它结构型指令的信息,参见 结构型指令模板语法章的内置指令部分。

给所选英雄添加样式

我们在下面的详情区看到了选中的英雄,但是我们还是没法在上面的列表区快速定位这位英雄。

在我们前面添加的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

  1. import { Component } from '@angular/core';
  2. export class Hero {
  3. id: number;
  4. name: string;
  5. }
  6. const HEROES: Hero[] = [
  7. { id: 11, name: 'Mr. Nice' },
  8. { id: 12, name: 'Narco' },
  9. { id: 13, name: 'Bombasto' },
  10. { id: 14, name: 'Celeritas' },
  11. { id: 15, name: 'Magneta' },
  12. { id: 16, name: 'RubberMan' },
  13. { id: 17, name: 'Dynama' },
  14. { id: 18, name: 'Dr IQ' },
  15. { id: 19, name: 'Magma' },
  16. { id: 20, name: 'Tornado' }
  17. ];
  18. @Component({
  19. selector: 'my-app',
  20. template: `
  21. <h1>{{title}}</h1>
  22. <h2>My Heroes</h2>
  23. <ul class="heroes">
  24. <li *ngFor="let hero of heroes"
  25. [class.selected]="hero === selectedHero"
  26. (click)="onSelect(hero)">
  27. <span class="badge">{{hero.id}}</span> {{hero.name}}
  28. </li>
  29. </ul>
  30. <div *ngIf="selectedHero">
  31. <h2>{{selectedHero.name}} details!</h2>
  32. <div><label>id: </label>{{selectedHero.id}}</div>
  33. <div>
  34. <label>name: </label>
  35. <input [(ngModel)]="selectedHero.name" placeholder="name"/>
  36. </div>
  37. </div>
  38. `,
  39. styles: [`
  40. .selected {
  41. background-color: #CFD8DC !important;
  42. color: white;
  43. }
  44. .heroes {
  45. margin: 0 0 2em 0;
  46. list-style-type: none;
  47. padding: 0;
  48. width: 15em;
  49. }
  50. .heroes li {
  51. cursor: pointer;
  52. position: relative;
  53. left: 0;
  54. background-color: #EEE;
  55. margin: .5em;
  56. padding: .3em 0;
  57. height: 1.6em;
  58. border-radius: 4px;
  59. }
  60. .heroes li.selected:hover {
  61. background-color: #BBD8DC !important;
  62. color: white;
  63. }
  64. .heroes li:hover {
  65. color: #607D8B;
  66. background-color: #DDD;
  67. left: .1em;
  68. }
  69. .heroes .text {
  70. position: relative;
  71. top: -3px;
  72. }
  73. .heroes .badge {
  74. display: inline-block;
  75. font-size: small;
  76. color: white;
  77. padding: 0.8em 0.7em 0 0.7em;
  78. background-color: #607D8B;
  79. line-height: 1em;
  80. position: relative;
  81. left: -1px;
  82. top: -4px;
  83. height: 1.8em;
  84. margin-right: .8em;
  85. border-radius: 4px 0 0 4px;
  86. }
  87. `]
  88. })
  89. export class AppComponent {
  90. title = 'Tour of Heroes';
  91. heroes = HEROES;
  92. selectedHero: Hero;
  93. onSelect(hero: Hero): void {
  94. this.selectedHero = hero;
  95. }
  96. }

已走的路

在本章中,我们完成了以下内容:

运行这部分的在线例子 / 可下载的例子

前方的路

我们的《英雄指南》长大了,但还远远不够完善。 我们显然不能把整个应用都放进一个组件中。 我们需要把它拆分成一系列子组件,然后教它们协同工作, 就像我们将在下一章学到的那样。

下一步

多个组件