英雄编辑器

The Hero Editor

应用程序现在有了基本的标题。 接下来你要创建一个新的组件来显示英雄信息并且把这个组件放到应用程序的外壳里去。

The application now has a basic title. Next you will create a new component to display hero information and place that component in the application shell.

创建英雄列表组件

Create the heroes component

使用 Angular CLI 创建一个名为 heroes 的新组件。

Using the Angular CLI, generate a new component named heroes.

ng generate component heroes
      
      ng generate component heroes
    

CLI 创建了一个新的文件夹 src/app/heroes/,并生成了 HeroesComponent 的三个文件。

The CLI creates a new folder, src/app/heroes/, and generates the three files of the HeroesComponent.

HeroesComponent 的类文件如下:

The HeroesComponent class file is as follows:

import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { constructor() { } ngOnInit() { } }
app/heroes/heroes.component.ts (initial version)
      
      import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}
    

你要从 Angular 核心库中导入 Component 符号,并为组件类加上 @Component 装饰器。

You always import the Component symbol from the Angular core library and annotate the component class with @Component.

@Component 是个装饰器函数,用于为该组件指定 Angular 所需的元数据。

@Component is a decorator function that specifies the Angular metadata for the component.

CLI 自动生成了三个元数据属性:

The CLI generated three metadata properties:

  1. selector— 组件的选择器(CSS 元素选择器)

    selector— the component's CSS element selector

  2. templateUrl— 组件模板文件的位置。

    templateUrl— the location of the component's template file.

  3. styleUrls— 组件私有 CSS 样式表文件的位置。

    styleUrls— the location of the component's private CSS styles.

CSS 元素选择器 app-heroes 用来在父组件的模板中匹配 HTML 元素的名称,以识别出该组件。

The CSS element selector, 'app-heroes', matches the name of the HTML element that identifies this component within a parent component's template.

ngOnInit 是一个生命周期钩子,Angular 在创建完组件后很快就会调用 ngOnInit。这里是放置初始化逻辑的好地方。

The ngOnInit is a lifecycle hook. Angular calls ngOnInit shortly after creating a component. It's a good place to put initialization logic.

始终要 export 这个组件类,以便在其它地方(比如 AppModule)导入它。

Always export the component class so you can import it elsewhere ... like in the AppModule.

添加 hero 属性

Add a hero property

HeroesComponent 中添加一个 hero 属性,用来表示一个名叫 “Windstorm” 的英雄。

Add a hero property to the HeroesComponent for a hero named "Windstorm."

hero = 'Windstorm';
heroes.component.ts (hero property)
      
      hero = 'Windstorm';
    

显示英雄

Show the hero

打开模板文件 heroes.component.html。删除 Angular CLI 自动生成的默认内容,改为到 hero 属性的数据绑定。

Open the heroes.component.html template file. Delete the default text generated by the Angular CLI and replace it with a data binding to the new hero property.

{{hero}}
heroes.component.html
      
      {{hero}}
    

显示 HeroesComponent 视图

Show the HeroesComponent view

要显示 HeroesComponent 你必须把它加到壳组件 AppComponent 的模板中。

To display the HeroesComponent, you must add it to the template of the shell AppComponent.

别忘了,app-heroes 就是 HeroesComponent元素选择器。 所以,只要把 <app-heroes> 元素添加到 AppComponent 的模板文件中就可以了,就放在标题下方。

Remember that app-heroes is the element selector for the HeroesComponent. So add an <app-heroes> element to the AppComponent template file, just below the title.

<h1>{{title}}</h1> <app-heroes></app-heroes>
src/app/app.component.html
      
      <h1>{{title}}</h1>
<app-heroes></app-heroes>
    

如果 CLI 的 ng serve 命令仍在运行,浏览器就会自动刷新,并且同时显示出应用的标题和英雄的名字。

Assuming that the CLI ng serve command is still running, the browser should refresh and display both the application title and the hero name.

创建 Hero

Create a Hero class

真实的英雄当然不止一个名字。

A real hero is more than a name.

src/app 文件夹中为 Hero 类创建一个文件,并添加 idname 属性。

Create a Hero class in its own file in the src/app folder. Give it id and name properties.

export class Hero { id: number; name: string; }
src/app/hero.ts
      
      export class Hero {
  id: number;
  name: string;
}
    

回到 HeroesComponent 类,并且导入这个 Hero 类。

Return to the HeroesComponent class and import the Hero class.

把组件的 hero 属性的类型重构为 Hero。 然后以 1id、以 “Windstorm” 为名字初始化它。

Refactor the component's hero property to be of type Hero. Initialize it with an id of 1 and the name Windstorm.

修改后的 HeroesComponent 类应该是这样的:

The revised HeroesComponent class file should look like this:

import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { hero: Hero = { id: 1, name: 'Windstorm' }; constructor() { } ngOnInit() { } }
src/app/heroes/heroes.component.ts
      
      import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  hero: Hero = {
    id: 1,
    name: 'Windstorm'
  };

  constructor() { }

  ngOnInit() {
  }

}
    

页面显示变得不正常了,因为你刚刚把 hero 从字符串改成了对象。

The page no longer displays properly because you changed the hero from a string to an object.

显示 hero 对象

Show the hero object

修改模板中的绑定,以显示英雄的名字,并在详情中显示 idname,就像这样:

Update the binding in the template to announce the hero's name and show both id and name in a details layout like this:

<h2>{{hero.name}} Details</h2> <div><span>id: </span>{{hero.id}}</div> <div><span>name: </span>{{hero.name}}</div>
heroes.component.html (HeroesComponent's template)
      
      <h2>{{hero.name}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><span>name: </span>{{hero.name}}</div>
    

浏览器自动刷新,并显示这位英雄的信息。

The browser refreshes and displays the hero's information.

使用 UppercasePipe 进行格式化

Format with the UppercasePipe

hero.name 的绑定修改成这样:

Modify the hero.name binding like this.

<h2>{{hero.name | uppercase}} Details</h2>
      
      <h2>{{hero.name | uppercase}} Details</h2>
    

浏览器刷新了。现在,英雄的名字显示成了大写字母。

The browser refreshes and now the hero's name is displayed in capital letters.

绑定表达式中的 uppercase 位于管道操作符( | )的右边,用来调用内置管道 UppercasePipe

The word uppercase in the interpolation binding, right after the pipe operator ( | ), activates the built-in UppercasePipe.

管道 是格式化字符串、金额、日期和其它显示数据的好办法。 Angular 发布了一些内置管道,而且你还可以创建自己的管道。

Pipes are a good way to format strings, currency amounts, dates and other display data. Angular ships with several built-in pipes and you can create your own.

编辑英雄名字

Edit the hero

用户应该能在一个 <input> 输入框中编辑英雄的名字。

Users should be able to edit the hero name in an <input> textbox.

当用户输入时,这个输入框应该能同时显示修改英雄的 name 属性。 也就是说,数据流从组件类流出到屏幕,并且从屏幕流回到组件类

The textbox should both display the hero's name property and update that property as the user types. That means data flow from the component class out to the screen and from the screen back to the class.

要想让这种数据流动自动化,就要在表单元素 <input> 和组件的 hero.name 属性之间建立双向数据绑定。

To automate that data flow, setup a two-way data binding between the <input> form element and the hero.name property.

双向绑定

Two-way binding

把模板中的英雄详情区重构成这样:

Refactor the details area in the HeroesComponent template so it looks like this:

<div> <label>name: <input [(ngModel)]="hero.name" placeholder="name"> </label> </div>
src/app/heroes/heroes.component.html (HeroesComponent's template)
      
      <div>
  <label>name:
    <input [(ngModel)]="hero.name" placeholder="name">
  </label>
</div>
    

[(ngModel)] 是 Angular 的双向数据绑定语法。

[(ngModel)] is Angular's two-way data binding syntax.

这里把 hero.name 属性绑定到了 HTML 的 textbox 元素上,以便数据流可以双向流动:从 hero.name 属性流动到 textbox,并且从 textbox 流回到 hero.name

Here it binds the hero.name property to the HTML textbox so that data can flow in both directions: from the hero.name property to the textbox, and from the textbox back to the hero.name.

缺少 FormsModule

The missing FormsModule

注意,当你加上 [(ngModel)] 之后这个应用无法工作了。

Notice that the app stopped working when you added [(ngModel)].

打开浏览器的开发工具,就会在控制台中看到如下信息:

To see the error, open the browser development tools and look in the console for a message like

Template parse errors: Can't bind to 'ngModel' since it isn't a known property of 'input'.
      
      Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'.
    

虽然 ngModel 是一个有效的 Angular 指令,不过它在默认情况下是不可用的。

Although ngModel is a valid Angular directive, it isn't available by default.

它属于一个可选模块 FormsModule,你必须自行添加此模块才能使用该指令。

It belongs to the optional FormsModule and you must opt-in to using it.

AppModule

Angular 需要知道如何把应用程序的各个部分组合到一起,以及该应用需要哪些其它文件和库。 这些信息被称为元数据(metadata)

Angular needs to know how the pieces of your application fit together and what other files and libraries the app requires. This information is called metadata

有些元数据位于 @Component 装饰器中,你会把它加到组件类上。 另一些关键性的元数据位于 @NgModule装饰器中。

Some of the metadata is in the @Component decorators that you added to your component classes. Other critical metadata is in @NgModuledecorators.

最重要的 @NgModule 装饰器位于顶级类 AppModule 上。

The most important @NgModule decorator annotates the top-level AppModule class.

Angular CLI 在创建项目的时候就在 src/app/app.module.ts 中生成了一个 AppModule 类。 这里也就是你要添加 FormsModule 的地方。

The Angular CLI generated an AppModule class in src/app/app.module.ts when it created the project. This is where you opt-in to the FormsModule.

导入 FormsModule

Import FormsModule

打开 AppModule (app.module.ts) 并从 @angular/forms 库中导入 FormsModule 符号。

Open AppModule (app.module.ts) and import the FormsModule symbol from the @angular/forms library.

import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
app.module.ts (FormsModule symbol import)
      
      import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
    

然后把 FormsModule 添加到 @NgModule 元数据的 imports 数组中,这里是该应用所需外部模块的列表。

Then add FormsModule to the @NgModule metadata's imports array, which contains a list of external modules that the app needs.

app.module.ts ( @NgModule imports)
      
      imports: [
  BrowserModule,
  FormsModule
],
    

刷新浏览器,应用又能正常工作了。你可以编辑英雄的名字,并且会看到这个改动立刻体现在这个输入框上方的 <h2> 中。

When the browser refreshes, the app should work again. You can edit the hero's name and see the changes reflected immediately in the <h2> above the textbox.

声明 HeroesComponent

Declare HeroesComponent

每个组件都必须声明在(且只能声明在)一个 NgModule 中。

Every component must be declared in exactly one NgModule.

没有声明过 HeroesComponent,可为什么本应用却正常呢?

You didn't declare the HeroesComponent. So why did the application work?

这是因为 Angular CLI 在生成 HeroesComponent 组件的时候就自动把它加到了 AppModule 中。

It worked because the Angular CLI declared HeroesComponent in the AppModule when it generated that component.

打开 src/app/app.module.ts 你就会发现 HeroesComponent 已经在顶部导入过了。

Open src/app/app.module.ts and find HeroesComponent imported near the top.

import { HeroesComponent } from './heroes/heroes.component';
      
      import { HeroesComponent } from './heroes/heroes.component';
    

HeroesComponent 也已经声明在了 @NgModule.declarations 数组中。

The HeroesComponent is declared in the @NgModule.declarations array.

declarations: [ AppComponent, HeroesComponent ],
      
      declarations: [
  AppComponent,
  HeroesComponent
],
    

注意 AppModule 声明了应用中的所有组件,AppComponentHeroesComponent

Note that AppModule declares both application components, AppComponent and HeroesComponent.

查看最终代码

Final code review

应用跑起来应该是这样的:在线例子 / 下载范例。本页中所提及的代码如下:

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

import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { hero: Hero = { id: 1, name: 'Windstorm' }; constructor() { } ngOnInit() { } }<h2>{{hero.name | uppercase}} Details</h2> <div><span>id: </span>{{hero.id}}</div> <div> <label>name: <input [(ngModel)]="hero.name" placeholder="name"> </label> </div>import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; // <-- NgModel lives here import { AppComponent } from './app.component'; import { HeroesComponent } from './heroes/heroes.component'; @NgModule({ declarations: [ AppComponent, HeroesComponent ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Tour of Heroes'; }<h1>{{title}}</h1> <app-heroes></app-heroes>export class Hero { id: number; name: string; }
      
      
  1. import { Component, OnInit } from '@angular/core';
  2. import { Hero } from '../hero';
  3.  
  4. @Component({
  5. selector: 'app-heroes',
  6. templateUrl: './heroes.component.html',
  7. styleUrls: ['./heroes.component.css']
  8. })
  9. export class HeroesComponent implements OnInit {
  10. hero: Hero = {
  11. id: 1,
  12. name: 'Windstorm'
  13. };
  14.  
  15. constructor() { }
  16.  
  17. ngOnInit() {
  18. }
  19.  
  20. }

小结

Summary

  • 你使用 CLI 创建了第二个组件 HeroesComponent

    You used the CLI to create a second HeroesComponent.

  • 你把 HeroesComponent 添加到了壳组件 AppComponent 中,以便显示它。

    You displayed the HeroesComponent by adding it to the AppComponent shell.

  • 你使用 UppercasePipe 来格式化英雄的名字。

    You applied the UppercasePipe to format the name.

  • 你用 ngModel 指令实现了双向数据绑定。

    You used two-way data binding with the ngModel directive.

  • 你知道了 AppModule

    You learned about the AppModule.

  • 你把 FormsModule 导入了 AppModule,以便 Angular 能识别并应用 ngModel 指令。

    You imported the FormsModule in the AppModule so that Angular would recognize and apply the ngModel directive.

  • 你知道了把组件声明到 AppModule 是很重要的,并认识到 CLI 会自动帮你声明它。

    You learned the importance of declaring components in the AppModule and appreciated that the CLI declared it for you.