Our app is growing. Use cases are flowing in for reusing components, passing data to components, and creating more reusable assets. Let's separate the heroes list from the hero details and make the details component reusable.

我们的应用正在成长中。现在又有新的用例:重复使用组件,传递数据给组件并创建更多可复用的资产。 我们来把英雄详情从英雄列表中分离出来,让这个英雄详情组件可以被复用。

Run the for this part.


Where We Left Off


Before we continue with our Tour of Heroes, let’s verify we have the following structure. If not, we’ll need to go back and follow the previous chapters.


node_modules ...

Keep the app transpiling and running


We want to start the TypeScript compiler, have it watch for changes, and start our server. We'll do this by typing

我们要启动 TypeScript 编译器,它会监视文件变更,并启动开发服务器。只要敲:

npm start

This will keep the application running while we continue to build the Tour of Heroes.


Making a Hero Detail Component


Our heroes list and our hero details are in the same component in the same file. They're small now but each could grow. We are sure to receive new requirements for one and not the other. Yet every change puts both components at risk and doubles the testing burden without benefit. If we had to reuse the hero details elsewhere in our app, the heroes list would tag along for the ride.

目前,英雄列表和英雄详情位于同一个文件的同一个组件中。 它们现在还很小,但很快它们都会长大。 我们将来肯定会收到新需求:针对这一个,却不能影响另一个。 然而,每一次更改都会给这两个组件带来风险和双倍的测试负担,却没有任何好处。 如果我们需要在应用的其它地方复用英雄详情组件,英雄列表组件也会跟着混进去。

Our current component violates the Single Responsibility Principle. It's only a tutorial but we can still do things right — especially if doing them right is easy and we learn how to build Angular apps in the process.

我们当前的组件违反了单一职责原则。 虽然这只是一个教程,但我们还是得坚持做正确的事 — 况且,做正确的事这么容易,在此过程中,我们又能学习如何构建 Angular 应用。

Let’s break the hero details out into its own component.


Separating the Hero Detail Component


Add a new file named hero-detail.component.ts to the app folder and create HeroDetailComponent as follows.


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

import { Component, Input } from '@angular/core'; @Component({ selector: 'my-hero-detail', }) export class HeroDetailComponent { }

Naming conventions


We like to identify at a glance which classes are components and which files contain components.


Notice that we have an AppComponent in a file named app.component.ts and our new HeroDetailComponent is in a file named hero-detail.component.ts.


All of our component names end in "Component". All of our component file names end in ".component".


We spell our file names in lower dash case (AKA kebab-case) so we don't worry about case sensitivity on the server or in source control.

这里我们使用小写中线命名法 (也叫烤串命名法)拼写文件名, 所以不用担心它在服务器或者版本控制系统中出现大小写问题。

We begin by importing the Component and Input decorators from Angular because we're going to need them soon.

我们先从 Angular 中导入ComponentInput装饰器,因为马上就会用到它们。

We create metadata with the @Component decorator where we specify the selector name that identifies this component's element. Then we export the class to make it available to other components.

我们使用@Component装饰器创建元数据。在元数据中,我们指定选择器的名字,用以标识此组件的元素。 然后,我们导出这个类,以便其它组件可以使用它。

When we finish here, we'll import it into AppComponent and create a corresponding <my-hero-detail> element.


Hero Detail Template


At the moment, the Heroes and Hero Detail views are combined in one template in AppComponent. Let’s cut the Hero Detail content from AppComponent and paste it into the new template property of HeroDetailComponent.

此时,AppComponent英雄列表英雄详情视图被组合进同一个模板中。 让我们从AppComponent剪切英雄详情的内容,并且粘贴HeroDetailComponent组件的template属性中。

We previously bound to the selectedHero.name property of the AppComponent. Our HeroDetailComponent will have a hero property, not a selectedHero property. So we replace selectedHero with hero everywhere in our new template. That's our only change. The result looks like this:

之前我们绑定了AppComponentselectedHero.name属性。 HeroDetailComponent组件将会有一个hero属性,而不是selectedHero属性。 所以,我们要把模板中的所有selectedHero替换为hero。只改这些就够了。 最终结果如下所示:

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

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> `

Now our hero detail layout exists only in the HeroDetailComponent.


Add the hero property

添加 hero 属性

Let’s add that hero property we were talking about to the component class.


hero: Hero;

Uh oh. We declared the hero property as type Hero but our Hero class is over in the app.component.ts file. We have two components, each in their own file, that need to reference the Hero class.

啊哦!我们声明hero属性是Hero类型,但是我们的Hero类还在app.component.ts文件中。 我们有了两个组件,它们位于各自的文件,并且都需要引用Hero类。

We solve the problem by relocating the Hero class from app.component.ts to its own hero.ts file.



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

We export the Hero class from hero.ts because we'll need to reference it in both component files. Add the following import statement near the top of both app.component.ts and hero-detail.component.ts.

我们从hero.ts中导出Hero类,因为我们要从两个组件文件中引用它。 在app.component.tshero-detail.component.ts的顶部添加下列 import 语句:

import { Hero } from './hero';

The hero property is an input


The HeroDetailComponent must be told what hero to display. Who will tell it? The parent AppComponent!


The AppComponent knows which hero to show: the hero that the user selected from the list. The user's selection is in its selectedHero property.

AppComponent确实知道该显示哪个英雄:用户从列表中选中的那个。 用户选择的英雄在它的selectedHero属性中。

We will soon update the AppComponent template so that it binds its selectedHero property to the hero property of our HeroDetailComponent. The binding might look like this:

我们马上升级AppComponent的模板,把该组件的selectedHero属性绑定到HeroDetailComponent组件的hero属性上。 绑定看起来可能是这样的:

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

Notice that the hero property is the target of a property binding — it's in square brackets to the left of the (=).

注意,hero是属性绑定的目标 — 它位于等号 (=) 左边方括号中。

Angular insists that we declare a target property to be an input property. If we don't, Angular rejects the binding and throws an error.

Angular 希望我们把目标属性声明为组件的输入属性,否则,Angular 会拒绝绑定,并抛出错误。

We explain input properties in more detail here where we also explain why target properties require this special treatment and source properties do not.


There are a couple of ways we can declare that hero is an input. We'll do it the way we prefer, by annotating the hero property with the @Input decorator that we imported earlier.

我们有几种方式把hero声明成输入属性。 这里我们采用首选的方式:使用我们前面导入的@Input装饰器向hero属性添加注解。

@Input() hero: Hero;

Learn more about the @Input() decorator in the Attribute Directives chapter.


Refresh the AppModule

更新 AppModule

We return to the AppModule, the application's root module, and teach it to use the HeroDetailComponent.


We begin by importing the HeroDetailComponent so we can refer to it.


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

Then we add HeroDetailComponent to the NgModule decorator's declarations array. This array contains the list of all components, pipes, and directives that we created and that belong in our application's module.

接下来,添加HeroDetailComponentNgModule装饰器中的declarations数组。 这个数组包含了所有由我们创建的并属于应用模块的组件、管道和指令。

@NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

Refresh the AppComponent

更新 AppComponent

Now that the application knows about our HeroDetailComponent, find the location in the AppComponent template where we removed the Hero Detail content and add an element tag that represents the HeroDetailComponent.

现在,应用知道了我们的HeroDetailComponent, 找到我们刚刚从模板中移除英雄详情的地方, 放上用来表示HeroDetailComponent组件的元素标签。


my-hero-detail is the name we set as the selector in the HeroDetailComponent metadata.

my-hero-detail 是我们在HeroDetailComponent元数据中的selector属性所指定的名字。

The two components won't coordinate until we bind the selectedHero property of the AppComponent to the HeroDetailComponent element's hero property like this:

这两个组件目前还不能协同工作,直到我们把AppComponent组件的selectedHero 属性和HeroDetailComponent组件的hero属性绑定在一起,就像这样:

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

The AppComponent’s template should now look like this


app.component.ts (template)

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

Thanks to the binding, the HeroDetailComponent should receive the hero from the AppComponent and display that hero's detail beneath the list. The detail should update every time the user picks a new hero.

感谢数据绑定机制,HeroDetailComponent应该能接收来自AppComponent的英雄数据,并在列表下方显示英雄的详情。 每当用户选中一个新的英雄时,详情信息应该随之更新。

It works!


When we view our app in the browser we see the list of heroes. When we select a hero we can see the selected hero’s details.

当在浏览器中查看应用时,可以看到英雄列表。 当选中一个英雄时,可以看到所选英雄的详情。

What's fundamentally new is that we can use this HeroDetailComponent to show hero details anywhere in the app.


We’ve created our first reusable component!


Reviewing the App Structure


Let’s verify that we have the following structure after all of our good refactoring in this chapter:


node_modules ...

Here are the code files we discussed in this chapter.


import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: 'my-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> ` }) export class HeroDetailComponent { @Input() hero: Hero; } import { Component } from '@angular/core'; import { Hero } from './hero'; 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> <my-hero-detail [hero]="selectedHero"></my-hero-detail> `, 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; } } export class Hero { id: number; name: string; } import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

The Road We’ve Travelled


Let’s take stock of what we’ve built.


Run the for this part.


The Road Ahead


Our Tour of Heroes has become more reusable with shared components.


We're still getting our (mock) data within the AppComponent. That's not sustainable. We should refactor data access to a separate service and share it among the components that need data.

AppComponent中,我们仍然使用着模拟数据。 显然,这种方式不能“可持续发展”。 我们要把数据访问逻辑抽取到一个独立的服务中,并在需要数据的组件之间共享。

We’ll learn to create services in the next tutorial chapter.