主从组件

Master/Detail Components

此刻,HeroesComponent 同时显示了英雄列表和所选英雄的详情。

At the moment, the HeroesComponent displays both the list of heroes and the selected hero's details.

把所有特性都放在同一个组件中,将会使应用“长大”后变得不可维护。 你要把大型组件拆分成小一点的子组件,每个子组件都要集中精力处理某个特定的任务或工作流。

Keeping all features in one component as the application grows will not be maintainable. You'll want to split up large components into smaller sub-components, each focused on a specific task or workflow.

本页面中,你将迈出第一步 —— 把英雄详情移入一个独立的、可复用的 HeroDetailComponent

In this page, you'll take the first step in that direction by moving the hero details into a separate, reusable HeroDetailComponent.

HeroesComponent 将仅仅用来表示英雄列表。 HeroDetailComponent 将用来表示所选英雄的详情。

The HeroesComponent will only present the list of heroes. The HeroDetailComponent will present details of a selected hero.

制作 HeroDetailComponent

Make the HeroDetailComponent

使用 Angular CLI 生成一个名叫 hero-detail 的新组件。

Use the Angular CLI to generate a new component named hero-detail.

ng generate component hero-detail
      
      ng generate component hero-detail
    

这个命令会做这些事:

The command scaffolds the following:

  • 创建目录 src/app/hero-detail

    Creates a directory src/app/hero-detail.

在这个目录中会生成四个文件:

Inside that directory four files are generated:

  • 作为组件样式的 CSS 文件。

    A CSS file for the component styles.

  • 作为组件模板的 HTML 文件。

    An HTML file for the component template.

  • 存放组件类 HeroDetailComponent 的 TypeScript 文件。

    A TypeScript file with a component class named HeroDetailComponent.

  • HeroDetailComponent 类的测试文件。

    A test file for the HeroDetailComponent class.

该命令还会把 HeroDetailComponent 添加到 src/app/app.module.ts 文件中 @NgModuledeclarations 列表中。

The command also adds the HeroDetailComponent as a declaration in the @NgModule decorator of the src/app/app.module.ts file.

编写模板

Write the template

HeroesComponent 模板的底部把表示英雄详情的 HTML 代码剪切粘贴到所生成的 HeroDetailComponent 模板中。

Cut the HTML for the hero detail from the bottom of the HeroesComponent template and paste it over the generated boilerplate in the HeroDetailComponent template.

所粘贴的 HTML 引用了 selectedHero。 新的 HeroDetailComponent 可以展示任意英雄,而不仅仅所选的。因此还要把模板中的所有 selectedHero 替换为 hero

The pasted HTML refers to a selectedHero. The new HeroDetailComponent can present any hero, not just a selected hero. So replace "selectedHero" with "hero" everywhere in the template.

完工之后,HeroDetailComponent 的模板应该是这样的:

When you're done, the HeroDetailComponent template should look like this:

<div *ngIf="hero"> <h2>{{hero.name | uppercase}} Details</h2> <div><span>id: </span>{{hero.id}}</div> <div> <label>name: <input [(ngModel)]="hero.name" placeholder="name"/> </label> </div> </div>
src/app/hero-detail/hero-detail.component.html
      
      <div *ngIf="hero">

  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label>name:
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </label>
  </div>

</div>
    

添加 @Input() hero 属性

Add the @Input() hero property

HeroDetailComponent 模板中绑定了组件中的 hero 属性,它的类型是 Hero

The HeroDetailComponent template binds to the component's hero property which is of type Hero.

打开 HeroDetailComponent 类文件,并导入 Hero 符号。

Open the HeroDetailComponent class file and import the Hero symbol.

import { Hero } from '../hero';
src/app/hero-detail/hero-detail.component.ts (import Hero)
      
      import { Hero } from '../hero';
    

hero 属性必须是一个带有 @Input() 装饰器的输入属性,因为外部的 HeroesComponent 组件将会绑定到它。就像这样:

The hero property must be an Input property, annotated with the @Input() decorator, because the external HeroesComponent will bind to it like this.

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

修改 @angular/core 的导入语句,导入 Input 符号。

Amend the @angular/core import statement to include the Input symbol.

import { Component, OnInit, Input } from '@angular/core';
src/app/hero-detail/hero-detail.component.ts (import Input)
      
      import { Component, OnInit, Input } from '@angular/core';
    

添加一个带有 @Input() 装饰器的 hero 属性。

Add a hero property, preceded by the @Input() decorator.

@Input() hero: Hero;
      
      @Input() hero: Hero;
    

这就是你要对 HeroDetailComponent 类做的唯一一项修改。 没有其它属性,也没有展示逻辑。这个组件所做的只是通过 hero 属性接收一个英雄对象,并显示它。

That's the only change you should make to the HeroDetailComponent class. There are no more properties. There's no presentation logic. This component simply receives a hero object through its hero property and displays it.

显示 HeroDetailComponent

Show the HeroDetailComponent

HeroesComponent 仍然是主从视图。

The HeroesComponent is still a master/detail view.

在你从模板中剪切走代码之前,它自己负责显示英雄的详情。现在它要把这个职责委托给 HeroDetailComponent 了。

It used to display the hero details on its own, before you cut that portion of the template. Now it will delegate to the HeroDetailComponent.

这两个组件将会具有父子关系。 当用户从列表中选择了某个英雄时,父组件 HeroesComponent 将通过把要显示的新英雄发送给子组件 HeroDetailComponent,来控制子组件。

The two components will have a parent/child relationship. The parent HeroesComponent will control the child HeroDetailComponent by sending it a new hero to display whenever the user selects a hero from the list.

你不用修改 HeroesComponent ,但是要修改它的模板

You won't change the HeroesComponent class but you will change its template.

修改 HeroesComponent 的模板

Update the HeroesComponent template

HeroDetailComponent 的选择器是 'app-hero-detail'。 把 <app-hero-detail> 添加到 HeroesComponent 模板的底部,以便把英雄详情的视图显示到那里。

The HeroDetailComponent selector is 'app-hero-detail'. Add an <app-hero-detail> element near the bottom of the HeroesComponent template, where the hero detail view used to be.

HeroesComponent.selectedHero 绑定到该元素的 hero 属性,就像这样:

Bind the HeroesComponent.selectedHero to the element's hero property like this.

<app-hero-detail [hero]="selectedHero"></app-hero-detail>
heroes.component.html (HeroDetail binding)
      
      <app-hero-detail [hero]="selectedHero"></app-hero-detail>
    

[hero]="selectedHero" 是 Angular 的属性绑定语法。

[hero]="selectedHero" is an Angular property binding.

这是一种单向数据绑定。从 HeroesComponentselectedHero 属性绑定到目标元素的 hero 属性,并映射到了 HeroDetailComponenthero 属性。

It's a one way data binding from the selectedHero property of the HeroesComponent to the hero property of the target element, which maps to the hero property of the HeroDetailComponent.

现在,当用户在列表中点击某个英雄时,selectedHero 就改变了。 当 selectedHero 改变时,属性绑定会修改 HeroDetailComponenthero 属性,HeroDetailComponent 就会显示这个新的英雄。

Now when the user clicks a hero in the list, the selectedHero changes. When the selectedHero changes, the property binding updates hero and the HeroDetailComponent displays the new hero.

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

The revised HeroesComponent template should look like this:

<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> <app-hero-detail [hero]="selectedHero"></app-hero-detail>
heroes.component.html
      
      <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>

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

浏览器刷新,应用又像以前一样开始工作了。

The browser refreshes and the app starts working again as it did before.

有哪些变化?

What changed?

以前一样,一旦用户点击了一个英雄的名字,该英雄的详情就显示在了英雄列表下方。 现在,HeroDetailComponent 负责显示那些详情,而不再是 HeroesComponent

As before, whenever a user clicks on a hero name, the hero detail appears below the hero list. Now the HeroDetailComponent is presenting those details instead of the HeroesComponent.

把原来的 HeroesComponent 重构成两个组件带来了一些优点,无论是现在还是未来:

Refactoring the original HeroesComponent into two components yields benefits, both now and in the future:

  1. 你通过缩减 HeroesComponent 的职责简化了该组件。

    You simplified the HeroesComponent by reducing its responsibilities.

  2. 你可以把 HeroDetailComponent 改进成一个功能丰富的英雄编辑器,而不用改动父组件 HeroesComponent

    You can evolve the HeroDetailComponent into a rich hero editor without touching the parent HeroesComponent.

  3. 你可以改进 HeroesComponent,而不用改动英雄详情视图。

    You can evolve the HeroesComponent without touching the hero detail view.

  4. 将来你可以在其它组件的模板中重复使用 HeroDetailComponent

    You can re-use the HeroDetailComponent in the template of some future component.

查看最终代码

Final code review

你的应用应该变成了这样在线例子 / 下载范例。本页所提及的代码文件如下:

Here are the code files discussed on this page and your app should look like this在线例子 / 下载范例.

import { Component, OnInit, Input } from '@angular/core'; import { Hero } from '../hero'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { @Input() hero: Hero; constructor() { } ngOnInit() { } }<div *ngIf="hero"> <h2>{{hero.name | uppercase}} Details</h2> <div><span>id: </span>{{hero.id}}</div> <div> <label>name: <input [(ngModel)]="hero.name" placeholder="name"/> </label> </div> </div><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> <app-hero-detail [hero]="selectedHero"></app-hero-detail>import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HeroesComponent } from './heroes/heroes.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; @NgModule({ declarations: [ AppComponent, HeroesComponent, HeroDetailComponent ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
      
      
  1. import { Component, OnInit, Input } from '@angular/core';
  2. import { Hero } from '../hero';
  3.  
  4. @Component({
  5. selector: 'app-hero-detail',
  6. templateUrl: './hero-detail.component.html',
  7. styleUrls: ['./hero-detail.component.css']
  8. })
  9. export class HeroDetailComponent implements OnInit {
  10. @Input() hero: Hero;
  11.  
  12. constructor() { }
  13.  
  14. ngOnInit() {
  15. }
  16.  
  17. }

小结

Summary

  • 你创建了一个独立的、可复用的 HeroDetailComponent 组件。

    You created a separate, reusable HeroDetailComponent.

  • 你用属性绑定语法来让父组件 HeroesComponent 可以控制子组件 HeroDetailComponent

    You used a property binding to give the parent HeroesComponent control over the child HeroDetailComponent.

  • 你用 @Input 装饰器来让 hero 属性可以在外部的 HeroesComponent 中绑定。

    You used the @Input decorator to make the hero property available for binding by the external HeroesComponent.