多个组件

此刻,AppComponent负责所有事。 起初,它只显示单个英雄的详情。然后,它变成了主从结构,同时显示英雄列表和一个英雄详情。 现在,我们很快又会有新需求了。 我们不能把这些需求全都放在一个组件中,否则将不可维护。

我们要把它拆分成一些子组件,每个子组件只聚焦在一个特定的任务或工作流上。 最后,AppComponent将会变成一个简单的壳,用来作为那些子组件的宿主。

本章中,我们要做的第一步就是把英雄详情拆分到一个独立的、可复用的组件中。 做完这些,应用是这样的:在线例子 / 可下载的例子

延续上一步教程

在继续《英雄指南》之前,先检查一下,是否已经有了如下目录结构。如果没有,回上一章,看看错过了哪里。

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命令,以便在构建《英雄指南》时保持持续转译和运行。

制作英雄详情组件

app/文件夹下添加一个名叫hero-detail.component.ts的文件。这个文件中会存放这个新的HeroDetailComponent

文件名和组件名遵循风格指南中的标准方式。

HeroDetailComponent的代码如下:

app/hero-detail.component.ts (initial version)

import { Component } from '@angular/core';

@Component({
  selector: 'hero-detail',
})
export class HeroDetailComponent {
}

要定义一个组件,我们总是要先导入符号Component

@Component装饰器为组件提供了Angular元数据。 CSS选择器的名字hero-detail会匹配元素的标签名,用于在父组件的模板中标记出当前组件的位置。 本章的最后,我们会把<hero-detail>添加到AppComponent的模板中。

总是export这个组件类,因为你必然会在别处import它。

英雄详情的模板

要把英雄详情的视图移入HeroDetailComponent,只要把英雄详情的 内容AppComponent模板的底部剪切出来, 粘贴到@Component元数据的template属性中就可以了。

HeroDetailComponent有一个 hero属性,而不再是selectedHero。 所以我们也要在模板中把所有的selectedHero替换为hero。 这些完成之后,新的模板是这样的:

src/app/hero-detail.component.ts (template)

@Component({
  selector: 'hero-detail',
  template: `
    <div *ngIf="hero">
      <h2>{{hero.name}} details!</h2>
      <div><label>id: </label>{{hero.id}}</div>
      <div>
        <label>name: </label>
        <input [(ngModel)]="hero.name" placeholder="name"/>
      </div>
    </div>
  `
})

添加hero属性

HeroDetailComponent模板绑定到了该组件的hero属性上。 把这个属性添加到HeroDetailComponent类上,就像这样:

src/app/hero-detail.component.ts (hero property)

hero: Hero;

hero属性的类型是HeroHero类仍然在app.component.ts文件中。 现在,有两个组件需要Hero类的引用。 而Angular风格指南建议每个文件中只有一个类。

因此我们要把Hero类从app.component.ts移到它自己的hero.ts文件中:

src/app/hero.ts

export class Hero {
  id: number;
  name: string;
}

现在,Hero类有了自己的文件,AppComponentHeroDetailComponent 就要import它了。 把下列import语句添加到app.component.tshero-detail.component.ts文件的顶部。

import { Hero } from './hero';

hero属性是一个输入属性

在本章稍后的部分, 父组件AppComponent会告诉子组件HeroDetailComponent要显示哪个英雄, 告诉的方法是把它的selectedHero属性绑定到HeroDetailComponenthero属性上。 这种绑定是这样的:

<hero-detail [hero]="selectedHero"></hero-detail>

在等号的左边,是方括号围绕的hero属性,这表示它是属性绑定表达式的目标。 我们要绑定到的目标属性必须是一个输入属性,否则Angular会拒绝绑定,并抛出一个错误。

首先,修改@angular/core导入语句,使其包含符号Input

src/app/hero-detail.component.ts (excerpt)

import { Component, Input } from '@angular/core';

然后,通过在hero属性前面加上@Input装饰器,来表明它是一个输入属性。

src/app/hero-detail.component.ts (excerpt)

@Input() hero: Hero;

要了解输入属性的更多知识,参见属性型指令页。

现在,hero属性是HeroDetailComponent类中唯一的东西。

export class HeroDetailComponent {
  @Input() hero: Hero;
}

它所做的一切就是通过它的输入属性hero接收一个英雄对象,然后把这个属性绑定到自己的模板中。

下面是完整的HeroDetailComponent

src/app/hero-detail.component.ts

  1. import { Component, Input } from '@angular/core';
  2. import { Hero } from './hero';
  3. @Component({
  4. selector: 'hero-detail',
  5. template: `
  6. <div *ngIf="hero">
  7. <h2>{{hero.name}} details!</h2>
  8. <div><label>id: </label>{{hero.id}}</div>
  9. <div>
  10. <label>name: </label>
  11. <input [(ngModel)]="hero.name" placeholder="name"/>
  12. </div>
  13. </div>
  14. `
  15. })
  16. export class HeroDetailComponent {
  17. @Input() hero: Hero;
  18. }

AppModule中声明HeroDetailComponent

每个组件都必须在一个(且只有一个)Angular模块中声明。

打开app.module.ts并且导入HeroDetailComponent,以便我们可以引用它。

src/app/app.module.ts

import { HeroDetailComponent } from './hero-detail.component';

HeroDetailComponent添加到该模块的declarations数组中。

src/app/app.module.ts

declarations: [
  AppComponent,
  HeroDetailComponent
],

通常,declarations数组包含应用中属于该模块的组件、管道和指令的列表。 组件在被其它组件引用之前必须先在一个模块中声明过。 这个模块只声明了两个组件:AppComponentHeroDetailComponent

要了解关于Angular模块的更多知识,参见Angular模块页。

HeroDetailComponent添加到AppComponent

AppComponent仍然是主从视图。 在我们剪切模板之前,它自己显示英雄的详情。 现在,它委托给了HeroDetailComponent

回想一下,hero-detail正是HeroDetailComponent元数据中使用的 CSS selector 它是一个HTML元素的标签名,用于表示HeroDetailComponent

<hero-detail>元素添加到AppComponent模板的底部,那里就是英雄详情视图所在的位置。

协调主视图AppComponentHeroDetailComponent的方式是把AppComponentselectedHero属性绑定到HeroDetailComponenthero属性上。

app.component.ts (excerpt)

<hero-detail [hero]="selectedHero"></hero-detail>

每当selectedHero变化时,HeroDetailComponent就会显示一个新的英雄。

修改后的AppComponent模板是这样的:

app.component.ts (excerpt)

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>
  <hero-detail [hero]="selectedHero"></hero-detail>
`,

有哪些变化?

仍然像以前一样,一旦用户点击了英雄的名字,英雄详情就会显示在英雄列表的下方。 不过现在改用HeroDetailView来表示英雄详情了。

我们把原来的AppComponent重构成了两个组件具有一些显著优点,无论是现在还是未来:

  1. 通过缩减AppComponent的职责,我们简化了它。

  2. 我们将来可以把HeroDetailComponent改进为功能更丰富的英雄编辑器,而不用动AppComponent

  3. 同样,我们也可以改进AppComponent而不用动英雄详情视图。

  4. 我们可以在未来的其它父组件的模板中复用HeroDetailComponent

审视本应用的代码结构

验证它是否已经有了如下结构:

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

下面是我们在本章讨论的代码文件:

  1. import { Component, Input } from '@angular/core';
  2. import { Hero } from './hero';
  3. @Component({
  4. selector: 'hero-detail',
  5. template: `
  6. <div *ngIf="hero">
  7. <h2>{{hero.name}} details!</h2>
  8. <div><label>id: </label>{{hero.id}}</div>
  9. <div>
  10. <label>name: </label>
  11. <input [(ngModel)]="hero.name" placeholder="name"/>
  12. </div>
  13. </div>
  14. `
  15. })
  16. export class HeroDetailComponent {
  17. @Input() hero: Hero;
  18. }

走过的路

来盘点一下我们已经构建了什么。

现在,应用应该变成了这样:在线例子 / 可下载的例子

前方的路

通过抽取共享组件,我们的《英雄指南》变得更有复用性了,但在AppComponent中,我们仍然使用着硬编码的模拟数据。显然,这种方式不能“可持续发展”。 我们要把数据访问逻辑重构到一个独立的服务中,并在需要数据的组件之间共享。

下一步,我们将学习如何创建服务。

下一步

服务