我们把主从结构的页面重构成多个组件

多个组件

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

首先老规矩,我们得让我们的代码运行起来:

 

让应用代码保持转译和运行

 

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

npm start

 

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

制作英雄详情组件

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

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

我们来把英雄详情拆分成一个独立的组件。

拆分英雄详情组件

app目录下添加一个名叫hero-detail.component.ts的文件,并且创建HeroDetailComponent。代码如下:

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

 selector:"my-hero-detail",

})
export class HeroDetailComponent{

}
View Code

命名约定

我们希望一眼就能看出哪些类是组件,哪些文件包含组件。

你会注意到,在名叫app.component.ts的文件中有一个AppComponent组件,在名叫hero-detail.component.ts的文件中有一个HeroDetailComponent组件。

我们的所有组件名都以Component结尾。所有组件的文件名都以.component结尾。

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

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

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

做完这些,我们把它导入AppComponent组件,并创建相应的<my-hero-detail>元素。

英雄详情模板

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

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

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

现在,我们的英雄详情布局只存在于HeroDetailComponent组件中。

添加 HERO 属性

把刚刚所说的hero属性添加到组件类。

hero: Hero;

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

要解决这个问题,我们从app.component.ts文件中把Hero类移到属于它自己的hero.ts文件中。

app/hero.ts

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

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

import {Hero} from  './hero';

HERO属性是一个输入属性

还得告诉HeroDetailComponent显示哪个英雄。谁告诉它呢?自然是父组件AppComponent了!

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

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

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

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

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

我们在这里详细解释了输入属性,以及为什么目标属性需要这样的特殊待遇,而源属性却不需要。

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

 @Input()
  hero: Hero;

更新 AppModule

回到应用的根模块AppModule,让它使用HeroDetailComponent组件。

我们先导入HeroDetailComponent组件,后面好引用它。

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

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

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

更新 AppComponent

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

<my-hero-detail></my-hero-detail>

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

<my-hero-detail [hero]="selectedHero"></my-hero-detail>
AppComponent的模板是这样的:
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>
`,
View Code

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

搞定!

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

值得关注的进步是:我们可以在应用中的任何地方使用这个HeroDetailComponent组件来显示英雄详情。

我们创建了第一个可复用组件!

完整的代码文件如下

app/hero-detail.component.ts

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;
}
View Code

app/app.component.ts

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;
  }
}
View Code

app/hero.ts

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

app/app.module.ts

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 { }
View Code

 

 

走过的路

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

  • 我们创建了一个可复用组件

  • 我们学会了如何让一个组件接收输入

  • 我们学会了在 Angular 模块中声明该应用所需的指令。 只要把这些指令列在NgModule装饰器的declarations数组中就可以了。

  • 我们学会了把父组件绑定到子组件。

 

前方的路

通过抽取共享组件,我们的《英雄指南》变得更有复用性了。

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

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

posted @ 2016-12-09 11:12  不是玩的  阅读(382)  评论(0编辑  收藏  举报