第三章 多组件
点此处可以下载本章完成后的 示例代码 。运行方法为:
- 解压缩文件,进入
 heroes项目根目录。- 使用
 npm install补上依赖包。也可以把之前备份的node_modules目录复制一份在项目根目录下。- 使用
 npm start运行项目,打开浏览器访问http://localhost:4200/进入项目。
我们希望在刚才的英雄列表中点击一个英雄,可以展示这个我们点击的英雄的详情。随着英雄指南系统功能越来越多,我们不能把所有的功能都放在一个组件中,否则代码将变得越来越难以维护。我们应该使用多组件,每个组件只集中做好一件事情。
1. 增加英雄详情组件
在命令行中进入工程目录,在src/app/目录中,使用
Angular/cli工具新建一个Component。ng g c heroes/hero-detail installing component create src/app/heroes/hero-detail/hero-detail.component.css create src/app/heroes/hero-detail/hero-detail.component.html create src/app/heroes/hero-detail/hero-detail.component.spec.ts create src/app/heroes/hero-detail/hero-detail.component.ts update src/app/app.module.ts打开
hero-detail.component.ts,添加一个属性hero: Hero,这就是我们要显示详情的英雄。我们知道这个hero属性应该由父组件来传递过来,所以hero属性是一个输入属性,将它改写为@Input hero: Hero。
了解属性的更多知识,查看属性型指令.
现在,hero属性是heroDetailComponent中的唯一的东西,它所做的就是通过它的输入属性hero接受一个对象,然后把这个属性绑定到自己的模板中。
下面是完整的hero-detai.component.ts文件:
import { Component, OnInit } from '@angular/core';
import { Input } from '@angular/core';
import { Hero } from '../../domain/hero';
@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
  @Input() hero: Hero;
  constructor() { }
  ngOnInit() {
  }
}
- 打开
hero-detail.component.html文件,添加: 
<div *ngIf="hero">
    <h2>{{hero.name}}'s details!</h2>
    <div><label>id: </label>{{hero.id}}</div>
    <div>
      <label>name: </label>
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </div>
    <div>
      <label>desc:</label>
      <input [(ngModel)]="hero.desc" placeholder="description">
    </div>
</div>
我们使用ngIf隐藏空的详情,当应用加载时,我们会看到一个英雄列表,但还没有任何英雄被选中。这时。如果不使用ngIf,浏览器会报这样的错误:
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
所以,我们要在选中英雄之前,把这些英雄的详情留在DOM之外,所以把模板中的英雄详情内容区放在一个
同时,需要增加英雄详情的样式。
hero-detail.component.css
label {
  display: inline-block;
  width: 3em;
  margin: .5em 0;
  color: #607D8B;
  font-weight: bold;
}
input {
  height: 2em;
  font-size: 1em;
  padding-left: .4em;
}
2. 修改英雄列表
为实现功能,我们需要为英雄列表添加一个属性,用来标示哪个hero是被选中的。然后,增加一个展示英雄详情的组件、把HeroDetailComponent组件添加到HeroListComponent组件中去。
添加属性
打开hero-list.component.ts文件,在heroes = HEROES下行添加
  selectedHero : Hero;
我们添加了一个用来标识哪个英雄是选中的属性。
添加点击事件
- 在
HeroListComponent类中添加onSelect方法。 
还是在hero-list.component.ts文件,添加:
  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }
把HeroDetailComponent添加到HeroListComponent中
- 把
元素添加到 hero-list.component.html的底部,那里就是英雄详情视图所在的位置。 
<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>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>是将selectedHero属性传给组件HeroDetailComponent。在等号的左边,是方括号围绕的hero属性,这表示它是属性绑定表达式的目标。我们要绑定到的目标属性必须是一个输入属性,否则Angular会拒绝绑定,并抛出一个错误。而HeroDetailComponent中的hero属性在上边我们声明时就是输入属性。- 圆括号标识
 <li>元素上的click事件是绑定的目标。 等号右边的onSelect(hero)表达式调用HeroComponent的onSelect()方法,并把模板输入变量hero作为参数传进去。 它是我们前面在ngFor指令中定义的那个hero变量。
为英雄列表添加样式
打开hero-list.component.css文件,在文件尾部添加样式:
.selected {
  background-color: #CFD8DC !important;
  color: white;
}
.heroes li.selected:hover {
  background-color: #BBD8DC !important;
  color: white;
}
点击列表中的一个英雄,可以看到:

小结
在本章结束时,你的英雄列表已经可以点击一个英雄后显示他的详细信息了,这是通过两个组件通信完成的。
3. 扩展阅读:组件的输入输出属性
Angular提供了输入(@Input)和输出(@Output)语法来处理组件数据的流入流出。下面是一个例子:
// children.component.ts
export class ChildrenComponent implements OnInit {
    @Input() array: any = {}; // 绑定数值
    @Output() routerNavigate = new EventEmitter<number>(); //绑定事件
    // ...
}
<!-- father.component.html -->
<li *ngFor="let arr of arrays">
    <item [array]="arr" (routerNavigate)="routerNavagate($event)">
    </item>
</li>
上述代码分别自定义了[array]和(routerNavigate)的输入输出变量,用于满足传入单个数据以及相应的跳转函数。被@Input修饰的arr变量属于输入属性,被@Output修饰的routerNavigate则是输出属性,当然,输入输出是对于当前的组件角度去说的。
接下来详细介绍这两种用法。
父组件向子组件传递数据(@Input)
父组件的数据通过子组件的输入属性流入子组件,在子组件完成接受或拦截,以此实现了数据由上而下的传递。那么还是上更为详细的代码来加深理解:
// father.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
    selector: 'list', // 这个是选择它上一级的标签渲染,可以忽略
    template: ` 
    // 注意template后使用那个符号作为标记
        <ul class="list">
            <li *ngFor="let arr of arrays">
                <item [array]="arr"></item> // item标签被它下一级选择,需要额外注意
            </li>
        </ul>
        `
    })
export class FatherComponent implements OnInit {
    // ...
    this.arrays = data; // data是获取到的源数据,可以从各种地方获得
}
将每一个对象通过arr绑定到array属性来供子组件引用,数据由上而下流入子组件,在子组件中通过@Input装饰器进行数据的接收,子组件的示例代码如下:
// children.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
    selector: 'item', // 注意这里装饰器选择父组件的item作为填充
    template: `
        <div class="arr-info">
            <lable class="arr-name">{{ array.name }}</lable>
            <span class="arr-telephone">{{ array.telephone }}</span>
        </div>
        `
})
export class ChildrenComponent implements OnInit {
    @Input() array: any = {}; // array是从上一级传递下来的数组元数据
    // ...
}
但是这样编译器会报一个错。解决的方法还是很简单的,只需要再import一个angular的模块就行了,原来angular会将一个个模块封装好,等你需要调用的时候再进行引入即可。
把children.component.ts的第一行代码修改为
import { Component, OnInit, Input } from '@angular/core';
然后编译器顺利地完成了编译。
子组件向父组件传递数据
使用事件传递是子组件向父组件传递数据最常用的方式。子组件需要实例化一个用来订阅和触发自定义事件的EventEmitter类,这个实例对象是一个由装饰器@Output修饰的输出属性,当有用户操作行为发生时该事件会被触发,父组件则通过事件绑定的方式来订阅来自子组件触发的事件,即子组件触发的具体实践会被其父组件订阅到。
下面将通过事件传递的方式来实现联系人详情页中收藏联系人的例子,创建两个组件,父组件:CollectionComponent,子组件:ContactCollectComponent。单击“收藏”按钮后将完成联系人的收藏操作,在子组件中通过数据绑定的方式实现了单击收藏的功能,具体的收藏操作统一在父组件中实现。
// CollectionComponent.ts(父组件)
import { Component } from '@angular/core';
@Component({
    selector: 'collection';
    template: `
        // 注意是将父组件的detail传到子组件的contact去
        <contact-collect [contact]="detail" (onCollect)="collectTheContact($event)">
        </contact-collect>
    `
})
export class CollectionComponent implements OnInit {
    detail: any = {};
    // 这个方法是在父组件中处理,将值进行0和1之间的转换,用以记录有没有被收藏
    collectTheContact() {
        this.detail.collection == 0 ? this.detail.collection = 1 : this.detail.collection = 0;
    }
}
父组件通过绑定自定义事件onCollect订阅来自子组件触发的事件。当有子组件对应的事件被触发在父组件中能够监听到该事件以此来完成收藏功能,具体的操作是在父组件的collectTheContact()方法中实现。
下面介绍子组件ContactCollectComponent的具体实现。子组件绑定一个点击事件,用户执行收藏操作来完成联系人收藏。当收藏按钮单击后,将会触发父组件的自定义事件onCollect,从而在父组件中完成联系人的收藏。示例代码如下:
// ContactCollectComponent.ts(子组件)
import { Component, EventEmitter, Input, Output } from '@augular/core'; // 注意引入其它模块
@Component({
    selector: 'contact-collect',
    template: `
        <i [ngClass]="{collected: contact.collection}" (click)="collectTheContact()">收藏
        </i>
    `
})
export class ContactCollectComponent {
    @Input() contact: any = {};
    @Output() onCollect = new EventEmitter<boolean>();
    collectTheContact() {
        this.onCollect.emit();
    }
}
单击“收藏”按钮后将触发自定义的事件onCollect = new EventEmitter<boolean>(),通过输出属性@Output将数据流向父组件,在父组件完成事件的监听,声明事件绑定的输出特性,当输出属性发出一个事件,在模板中绑定的对应事件处理句柄将会被调用。
当然,除了自定义事件,框架本身就含有大量的内置事件:onclick,mouseout,mouseover等等。
其它组件的交互方式
除了@Input和@Output这两种最基本的用法,还有很多种组件之间的通讯方式
- 通过局部变量实现数据交互
 - 使用ViewChild实现数据交互
 - 利用service进行交互