Multiple Components

我们的app正变得更丰富,接下来我们要做的是:可重用组件,传递数据到组件以及创建更多的可重用资源。让我们把英雄列表和英雄详情分离,使其可重用。

运行在线示例演示这一部分。

从哪里开始

在我们继续英雄之旅之前,让我们先验证以下的文件结构。如果不对,我们需要返回上一章重新学习。

angular2-tour-of-heroes
├── app
│   ├── app.component.ts
│   └── main.ts
├── node_modules ...
├── typings ...
├── index.html
├── package.json
├── styles.css
├── systemjs.config.js
├── tsconfig.json
└── typings.json

保持app运行

我们想要运行TypeScript编译器,让它监控文件变更,并且运行服务器。为此我们将执行命令:

npm start

这个命令将运行监控模式的编译器,启动服务器,在浏览器中打开app,并保证app在我们不断创建英雄之旅时运行。

创建一个英雄详情组件

英雄列表与英雄详情现在处于同一个组件、同一个文件中,虽然它们内容很少但以后可能会变得更复杂,我们总是会收到新的需求。所有的变更都让组件面临风险,承担双倍的测试压力。如果我们准备在app其他地方重用英雄详情,英雄列表部分也会一同被重用。

我们目前的组件违背了单一职责原则。虽然这仅仅是一个教程,但我们仍然要做正确的事 — 特别是如果把事做对相当简单且我们可以同时学习如何创建Angular应用。

让我们开始分离英雄详情到一个单独的组件。

分离英雄详情组件

app目录下添加一个名为hero-detail.component.ts的新文件,并如下创建HeroDetailComponent

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

@Component({
  selector: 'my-hero-detail',
})
export class HeroDetailComponent {
}

命名约定

我们希望迅速识别出哪些类是组件,哪些文件包含组件。

注意到我们在文件app.component.ts中有个AppComponent组件,并且我们的新组件HeroDetilComponent包含于文件hero-detail.component.ts

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

我们的文件名使用小写的dash case(又称 kebab-case)命名,因此我们不需要担心服务器或版本控制工具的字母大小写问题。

我们以引入Angular的ComponentInput装饰器作为开始,我们马上就会需要用到它们。

我们用@Component装饰器创建元数据,在其中指定标识了这个组件元素的选择器名称。接着我们export这个类使其对其他组件可访问。

当我们做完这一步,我们将会把它引入到AppComponent并且创建一个想对应的<my-hero-detail>元素。

英雄详情Template

此时,英雄列表和英雄详情视图正合并在AppComponent中的一个template中。让我们将英雄详情内容从AppComponent剪切,并且粘贴HeroDetailComponent中的template属性里。

之前template绑定了AppComponentselectedHero.name属性。HeroDetailComponent将会有个hero属性,而不是selectedHero属性。因此我们在template中把所有的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>
`

现在英雄详情布局就仅仅存在于HeroDetailComponent了。

添加hero属性

让我们来添加hero属性到组件类。

hero: Hero;

啊哦,我们把hero属性定义成Hero类型,但是Hero类远在app.component.ts文件里。我们有两个定义在不同文件的组件都需要引用Hero类。

我们需要把Hero类从app.component.ts中挪到一个单独的hero.ts文件中来解决这个问题。

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

我们在hero.ts文件中exportHero类,因为我们在所有组件中都需要引用这它。在app.component.jshero-detail.component.js顶部都添加以下引入语句。

import { Hero } from './hero';

hero属性是一个input

HeroDetailComponent必须被告知需要展示哪一个英雄。那么谁来告诉它?AppComponent

AppComponent知道需要展示哪个英雄:就是用户从列表中选择的那个。用户的选择保存在selectedHero属性中。

我们将会更新AppComponent的template,使它的selectedHero属性绑定到HeroDetailComponenthero属性。代码看起来是这样的:

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

注意到hero属性是属性绑定的target —— 它在等号左边的中括号里。

Angular坚持认为我们要s声明一个target属性作为一个input属性。假如不这样做,Angular将拒绝绑定并且抛出一个错误。

我们在这里描述更多关于input属性的知识,同时描述了为什么target属性需要这种特殊对待,同样对待的还有source属性。

我们有好几种方法来声明hero成为一个input, 我们会用我们更推崇的方式来做,通过用我们之前引入的@Input装饰器标注hero属性。

  @Input()
  hero: Hero;

想要学习更多关于@Input()装饰器的知识,请访问Attribute Directives章节。

更新AppComponent

我们回到AppComponent并且教它如何利用HeroDetailComponent

首先引入HeroDetailComponent,是我们可以引用它。

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

在template找到我们移除英雄详情的位置,接着添加一个代表HeroDetailComponent的元素

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

my-hero-detail是我们在HeroDetailComponent元数据中定义的selector名称。

然后绑定AppComponentselectedHero属性到HeroDetailComponent元素的hero属性,像这样:

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

现在AppComponent的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>
`,

多亏了绑定操作,现在HeroDetailComponent应该能从AppComponent接收英雄并在列表下方展示英雄详情。每次用户选择一个新的英雄,详情信息都会跟着更新。

但上述行为还没有实现。

我们点击不同英雄后,没有详情显示。我们在浏览器console中查找错误信息,然而并没任何错误。

就好像Angular忽略了这个新的标签。实际上正是因为它忽略了这个新标签

directives 数组

浏览器将会忽略它不能识别的HTML标签和属性,Angular也是这样。

我们已经引入了HeroDetailComponent,我们已经在template使用了它,但是我们还没告知Angular。

我们需要把它列在元数据diectives数组中来通知Angular。在@Component配置对象的底部,紧接着templatestyles属性,添加这个数组属性。

directives: [HeroDetailComponent]

成功了!

当我们在浏览器中查看app,我们将看到一个英雄列表。当我们选择一个英雄时,将看到选中英雄的详情。

最重要的是,我们可以用HeroDetailComponent在app的任何地方展示英雄详情。

我们已经创建了第一个可重用组件!

回顾App结构

现在我们来验证下目前的文件结构:

angular2-tour-of-heroes
├── app
│   ├── app.component.ts
│   ├── hero.ts
│   ├── hero-detial.component.ts
│   └── main.ts
├── node_modules ...
├── typings ...
├── index.html
├── package.json
├── styles.css
├── systemjs.config.js
├── tsconfig.json
└── typings.json

以下是此章最终的代码文件:

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

app/app.component.ts

import { Component } from '@angular/core';
import { Hero } from './hero';
import { HeroDetailComponent } from './hero-detail.component';
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;
    }
  `],
  directives: [HeroDetailComponent]
})
export class AppComponent {
  title = 'Tour of Heroes';
  heroes = HEROES;
  selectedHero: Hero;
  onSelect(hero: Hero) { this.selectedHero = hero; }
}

app/hero.js

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

回顾历程

让我们盘点下已经完成的部分。

  • 创建了一个可重用组件
  • 学习如何使组件接收input
  • 学习绑定父子组件
  • 学习在directives数组中声明所需的应用指令

运行在线示例来演示这部分内容。

results matching ""

    No results matching ""