主从结构

Our story needs more heroes. We’ll expand our Tour of Heroes app to display a list of heroes, allow the user to select a hero, and display the hero’s details.

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

Run the for this part.

运行这部分的在线例子

Let’s take stock of what we’ll need to display a list of heroes. First, we need a list of heroes. We want to display those heroes in the view’s template, so we’ll need a way to do that.

我们来盘点一下显示英雄列表都需要些什么。 首先,需要一份英雄列表数据。还要把这些英雄显示到一个视图的模板中,所以, 我们需要用某种途径来做到这一点。

Where We Left Off

延续上一步教程

Before we continue with Part 2 of the Tour of Heroes, let’s verify we have the following structure after Part 1. If not, we’ll need to go back to Part 1 and figure out what we missed.

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

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

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.

这个命令会在我们构建《英雄指南》的时候让应用得以持续运行。

Displaying Our Heroes

显示我们的英雄

Creating heroes

创建英雄

Let’s create an array of ten heroes.

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

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

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' } ];

The HEROES array is of type Hero, the class defined in part one, to create an array of heroes. We aspire to fetch this list of heroes from a web service, but let’s take small steps first and display mock heroes.

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

Exposing heroes

暴露英雄

Let’s create a public property in AppComponent that exposes the heroes for binding.

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

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

heroes = HEROES;

We did not have to define the heroes type. TypeScript can infer it from the HEROES array.

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

We could have defined the heroes list here in this component class. But we know that ultimately we’ll get the heroes from a data service. Because we know where we are heading, it makes sense to separate the hero data from the class implementation from the start.

我们固然可以在这个组件类中定义英雄列表,但显然,最终我们还是得从一个数据服务中获取这些英雄。 因此,不如索性一开始就把英雄数据隔离到一个类中来实现。

Displaying heroes in a template

在模板中显示英雄

Our component has heroes. Let’s create an unordered list in our template to display them. We’ll insert the following chunk of HTML below the title and above the hero details.

我们的组件有了heroes属性,我们再到模板中创建一个无序列表来显示它们。 我们将在标题和英雄详情之间,插入下面这段 HTML 代码。

src/app/app.component.ts (heroes template)

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

Now we have a template that we can fill with our heroes.

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

Listing heroes with ngFor

通过 ngFor 来显示英雄列表

We want to bind the array of heroes in our component to our template, iterate over them, and display them individually. We’ll need some help from Angular to do this. Let’s do this step by step.

我们想要把组件中的heroes数组绑定到模板中,迭代并逐个显示它们。 这下,我们就得借助 Angular 了。我们来一步步实现它!

First modify the <li> tag by adding the built-in directive *ngFor.

首先,修改<li>标签,往上添加内置指令*ngFor

src/app/app.component.ts (ngFor)

<li *ngFor="let hero of heroes">

The leading asterisk (*) in front of ngFor is a critical part of this syntax.

ngFor的前导星号(*)是此语法的重要组成部分。

The (*) prefix to ngFor indicates that the <li> element and its children constitute a master template.

ngFor*前缀表示<li>及其子元素组成了一个主控模板。

The ngFor directive iterates over the heroes array returned by the AppComponent.heroes property and stamps out instances of this template.

ngFor指令在AppComponent.heroes属性返回的heroes数组上迭代,并输出此模板的实例。

The quoted text assigned to ngFor means “take each hero in the heroes array, store it in the local hero variable, and make it available to the corresponding template instance”.

引号中赋值给ngFor的那段文本表示“heroes数组中取出每个英雄,存入一个局部的hero变量,并让它在相应的模板实例中可用”。

The let keyword before "hero" identifies hero as a template input variable. We can reference this variable within the template to access a hero’s properties.

hero前的let关键字表示hero是一个模板输入变量。 在模板中,我们可以引用这个变量来访问一位英雄的属性。

Learn more about ngFor and template input variables in the Displaying Data and Template Syntax chapters.

要学习更多关于ngFor和模板输入变量的知识,见显示数据模板语法

Now we insert some content between the <li> tags that uses the hero template variable to display the hero’s properties.

现在,我们在<li>标签中插入一些内容,以便使用模板变量hero来显示英雄的属性。

src/app/app.component.ts (ngFor template)

<li *ngFor="let hero of heroes"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li>

When the browser refreshes, we see a list of heroes!

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

Styling our heroes

给我们的英雄们“美容”

Our list of heroes looks pretty bland. We want to make it visually obvious to a user which hero we are hovering over and which hero is selected.

我们的英雄列表看起来实在是稀松平常。 但当用户的鼠标划过英雄或选中了一个英雄时,我们得让它/她看起来醒目一点。

Let’s add some styles to our component by setting the styles property on the @Component decorator to the following CSS classes:

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

Notice that we again use the back-tick notation for multi-line strings.

注意,我们又使用了反引号语法来书写多行字符串。

That's a lot of styles! We can put them inline as shown here, or we can move them out to their own file which will make it easier to code our component. We'll do this in a later chapter. For now let's keep rolling.

这里有很多种样式!我们可以像上面那样把它们内联在组件中,或者把样式移到单独的文件中, 这样能让编写组件变得更容易。我们会后面的章节中使用独立样式文件,现在我们先不管它。

When we assign styles to a component they are scoped to that specific component. Our styles will only apply to our AppComponent and won't "leak" to the outer HTML.

当我们为一个组件指定样式时,它们的作用域将仅限于该组件。 上面的例子中,这些样式只会作用于AppComponent组件,而不会“泄露”到外部 HTML 中。

Our template for displaying the heroes should now look like this:

用于显示英雄们的模板看起来像这样:

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>

Selecting a Hero

选择英雄

We have a list of heroes and we have a single hero displayed in our app. The list and the single hero are not connected in any way. We want the user to select a hero from our list, and have the selected hero appear in the details view. This UI pattern is widely known as "master-detail". In our case, the master is the heroes list and the detail is the selected hero.

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

Let’s connect the master to the detail through a selectedHero component property bound to a click event.

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

Click event

点击事件

We modify the <li> by inserting an Angular event binding to its click event.

我们往<li>元素上插入一句 Angular 事件绑定代码,绑定到它的点击事件。

src/app/app.component.ts (template excerpt)

<li *ngFor="let hero of heroes" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li>

Focus on the event binding

事件绑定详解

(click)="onSelect(hero)"

The parenthesis identify the <li> element’s click event as the target. The expression to the right of the equal sign calls the AppComponent method, onSelect(), passing the template input variable hero as an argument. That’s the same hero variable we defined previously in the ngFor.

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

Learn more about Event Binding in the User Input and Templating Syntax chapters.

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

Add the click handler

添加点击处理器

Our event binding refers to an onSelect method that doesn’t exist yet. We’ll add that method to our component now.

我们的事件绑定引用了onSelect方法,但它还不存在。 我们现在就把它添加到组件上。

What should that method do? It should set the component’s selected hero to the hero that the user clicked.

这个方法该做什么?它应该把组件中被选中的英雄设置为用户刚刚点击的那个。

Our component doesn’t have a “selected hero” yet either. We’ll start there.

我们的组件还没有用来表示“当前选中的英雄”的变量,我们就从这一步开始。

Expose the selected hero

暴露选中的英雄

We no longer need the static hero property of the AppComponent. Replace it with this simple selectedHero property:

我们不再需要AppComponenthero属性。 把它替换selectedHero属性。

src/app/app.component.ts (selectedHero)

selectedHero: Hero;

We’ve decided that none of the heroes should be selected before the user picks a hero so we won’t initialize the selectedHero as we were doing with hero.

我们决定在用户选取之前,不会默认选择任何英雄,所以,不用像hero一样初始化selectedHero变量。

Now add an onSelect method that sets the selectedHero property to the hero the user clicked.

现在,添加一个onSelect方法,用于将用户点击的英雄赋给selectedHero属性。

src/app/app.component.ts (onSelect)

onSelect(hero: Hero): void { this.selectedHero = hero; }

We will be showing the selected hero's details in our template. At the moment, it is still referring to the old hero property. Let’s fix the template to bind to the new selectedHero property.

我们将把所选英雄的详细信息显示在模板中。目前,它仍然引用之前的hero属性。 我们这就修改模板,让它绑定到新的selectedHero属性。

src/app/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>

Hide the empty detail with ngIf

使用 ngIf 隐藏空的详情

When our app loads we see a list of heroes, but a hero is not selected. The selectedHero is undefined. That’s why we'll see the following error in the browser’s console:

当应用加载时,我们会看到一个英雄列表,但还没有任何英雄被选中。 selectedHero属性是undefined。 因此,我们会看到浏览器控制台中出现下列错误:

EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]

Remember that we are displaying selectedHero.name in the template. This name property does not exist because selectedHero itself is undefined.

别忘了我们要在模板中显示的是selectedHero.name。 显然,这个 name 属性是不存在的,因为selectedHero本身还是undefined呢。

We'll address this problem by keeping the hero detail out of the DOM until there is a selected hero.

要处理这个问题,我们可以先让英雄详情不要出现在 DOM 中,直到有英雄被选中。

We wrap the HTML hero detail content of our template with a <div>. Then we add the ngIf built-in directive and set it to the selectedHero property of our component.

我们把模板中的英雄详情内容区用放在一个<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>

Remember that the leading asterisk (*) in front of ngIf is a critical part of this syntax.

记住,ngIf前导星号 (*) 是语法中的重要组成部分。

When there is no selectedHero, the ngIf directive removes the hero detail HTML from the DOM. There will be no hero detail elements and no bindings to worry about.

当没有selectedHero时,ngIf指令从 DOM 中移除表示英雄详情的这段 HTML 。 没有了表示英雄详情的元素,也就不用担心绑定问题。

When the user picks a hero, selectedHero becomes "truthy" and ngIf puts the hero detail content into the DOM and evaluates the nested bindings.

当用户选取了一个英雄,selectedHero变成了“真”值,于是ngIf把英雄详情加回 DOM 中,并计算它所嵌套的各种绑定。

ngIf and ngFor are called “structural directives” because they can change the structure of portions of the DOM. In other words, they give structure to the way Angular displays content in the DOM.

ngIfngFor被称为“结构型指令”,因为它们可以修改部分 DOM 的结构。 换句话说,它们让 Angular 在 DOM 中显示内容的方式结构化了。

Learn more about ngIf, ngFor and other structural directives in the Structural Directives and Template Syntax chapters.

更多ngIfngFor和其它结构型指令的信息,见 结构型指令模板语法

The browser refreshes and we see the list of heroes but not the selected hero detail. The ngIf keeps it out of the DOM as long as the selectedHero is undefined. When we click on a hero in the list, the selected hero displays in the hero details. Everything is working as we expect.

浏览器刷新了,我们看到了一个英雄列表,但没有显示选中的英雄详情。 当selectedHeroundefined时,ngIf会保证英雄详情不出现在 DOM 中。 当我们从列表中点击一个英雄时,选中的英雄被显示在英雄详情里。 正如我们所预期的那样。

Styling the selection

给所选英雄添加样式

We see the selected hero in the details area below but we can’t quickly locate that hero in the list above. We can fix that by applying the selected CSS class to the appropriate <li> in the master list. For example, when we select Magneta from the heroes list, we can make it pop out visually by giving it a subtle background color as shown here.

我们在下面的详情区看到了选中的英雄,但是我们还是没法在上面的列表区快速地定位这位英雄。 可以通过在主列表的相应<li>元素上添加 CSS 类selected来解决这个问题。 例如,当我们在列表区选中了 Magneta 时,我们可以通过设置一个轻微的背景色来让它略显突出。

选中的英雄

We’ll add a property binding on class for the selected class to the template. We'll set this to an expression that compares the current selectedHero to the hero.

在模块中,我们在class上为selected类添加一个属性绑定。我们把绑定表达式设置为selectedHerohero的比较结果。

The key is the name of the CSS class (selected). The value is true if the two heroes match and false otherwise. We’re saying “apply the selected class if the heroes match, remove it if they don’t”.

键是 CSS 类的名字 (selected)。当两位英雄一致时,值为true,否则为false。 也就是说:“当两位英雄匹配时,应用上selected类,否则不应用”。

src/app/app.component.ts (setting the CSS class)

[class.selected]="hero === selectedHero"

Notice in the template that the class.selected is surrounded in square brackets ([]). This is the syntax for a property binding, a binding in which data flows one way from the data source (the expression hero === selectedHero) to a property of class.

注意,模板中的class.selected包裹在方括号中。 这就是属性绑定的语法,实现从数据源(hero === selectedHero表达式)到class属性的单向数据流动。

src/app/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>

Learn more about property bindings in the Template Syntax chapter.

关于属性绑定的更多信息,见模板语法

The browser reloads our app. We select the hero Magneta and the selection is clearly identified by the background color.

浏览器重新加载了我们的应用。 我们选中英雄 Magneta,通过背景色的变化,它被清晰的标记出来。

英雄列表应用的输出

We select a different hero and the tell-tale color switches to that hero.

我们选择另一个英雄,色标也随着切换。

Here's the complete app.component.ts as it stands now:

完整的app.component.ts文件如下:

src/app/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; } }

The Road We’ve Travelled

已走的路

Here’s what we achieved in this chapter:

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

Run the for this part.

运行这部分的在线例子

The Road Ahead

前方的路

Our Tour of Heroes has grown, but it’s far from complete. We can't put the entire app into a single component. We need to break it up into sub-components and teach them to work together as we learn in the next chapter.

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

下一步

多个组件