组件间交互简单来说就是让两个或多个组件之间共享信息。接下来我们就对Angular2组件间的交互做一个简单的解释。当然做好的文档还是官方文档:https://www.angular.cn/guide/component-interaction
一、通过@Input把父组件的属性绑定到子组件
@Input注解是属性绑定,通常在父组件需要向子组件传递数据的时候使用。关于@Input你可以简单的理解为子组件创建的时候需要传递参数(当然子组件的创建指的是在父组件对应的html里面申明)。
有@Input那肯定就会对应的有一个@Output,@Output是用于子组件向父组件传递数据的时候触发事件。关于@Output我们会在下面讲到。
@Input的使用简单的很,首先在子组件定义的时候我们先明确哪些属性是需要父组件传递过来的,给加上@Input注解就完事了。然后父组件通过模板语法把属性绑定到子组件上去就完事了。
我们用一个非常简单的实例看下@Input的使用,父组件需要把Hero对象传递到子组件里面去。
子组件hero属性加上@Input()注解。
import {Component, Input} from '@angular/core';
import {Hero} from '../hero';
@Component({
selector: 'app-data-child',
template: `
我是子组件,父组件传递的值是:"{{hero.name}}"
`
})
export class DataChildComponent {
// 该属性需要从父组件传递过来
@Input() hero: Hero;
constructor() { }
}
父组件通过[hero]="parentHero"把parentHero属性绑定到子组件上去
import {Component} from '@angular/core';
import {Hero} from '../hero';
@Component({
selector: 'app-data-parent',
styleUrls: ['./data-parent.component.css'],
template: `
`
})
export class DataParentComponent {
parentHero: Hero = new Hero();
constructor() {
this.parentHero.name = '我是父组件定义的';
}
}
@Input的使用就是这么的简单的,除了[hero]="parentHero"单向绑定,我们也可以使用[(hero)]="parentHero"双向绑定。
1.1、通过setter截听输入属性值的变化
使用输入属性的 setter、getter方法来拦截父组件中值的变化,一边是在setter函数里面做一些相应的处理,然后getter函数里面返回。 我们还是继续在上面的基础上做一个简单的修改。对子组件做一个简单的修改把父组件传递过来的Hero对象里面的名字都改成大写。
import {Component, Input} from '@angular/core';
import {Hero} from '../hero';
@Component({
selector: 'app-data-child',
template: `
我是子组件,父组件传递的值是:"{{hero.name}}"
`
})
export class DataChildComponent {
private _hero: Hero;
// 该属性需要从父组件传递过来,我们把Hero对象里面的name改成大写
@Input()
set hero(hero: Hero) {
// 把父组件传递过来的数据装换成大写
const name = (hero.name && hero.name.toUpperCase()) || '';
this._hero = new Hero();
this._hero.name = name;
}
get hero(): Hero {
return this._hero;
}
constructor() {
}
}
1.2、通过ngOnChanges()钩子来拦截输入属性值的变化
使用OnChanges生命周期钩子接口的ngOnChanges() 方法来监测输入属性值的变化并做出回应。当输入属性值变化的时候会回调ngOnChanges()方法。
ngOnChanges()钩子:当Angular(重新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的SimpleChanges对象
当被绑定的输入属性的值发生变化时调用,首次调用一定会发生在ngOnInit()之前。
关于通过ngOnChanges()钩子来拦截属性值的变化。想强调的一点就是。数据变化指的是属性指向的地址发生改变才会回调ngOnChanges()函数。所以对于一些自定义的数据类型(class)要特别小心。
我们还是用一个简单的例子来说明怎么通过通过ngOnChanges()钩子来拦截输入属性值的变化。子组件需要父组件传递一个string属性过来,通过ngOnChanges()钩子拦截到属性的变化,把数据都改为大写的。
子组件,把父组件传递过来的数据改为大写
import {Component, Input, OnChanges, SimpleChange} from '@angular/core';
@Component({
selector: 'app-data-child',
template: `
我是子组件,父组件传递的值是:"{{inputString}}"
`
})
export class DataChildComponent implements OnChanges {
// 该属性需要从父组件传递过来
@Input()
inputString: string;
// 拦截inputString的变化,并且把他变成大写
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (const propName in changes) {
if (changes.hasOwnProperty(propName)) {
const changedProp = changes[propName];
const to = JSON.stringify(changedProp.currentValue);
// 我们这里只想要inputString属性的变化
if (propName === 'inputString') {
if (changedProp.isFirstChange()) {
// 第一次数据设置
} else {
// 不是第一次
}
this.inputString = to.toUpperCase();
}
}
}
}
}
父组件相关代码
import {Component} from '@angular/core';
@Component({
selector: 'app-data-parent',
styleUrls: ['./data-parent.component.css'],
template: `
`
})
export class DataParentComponent {
inputString: string;
constructor() {
this.inputString = 'nihao';
}
onValueChangeClick() {
this.inputString = 'change';
}
}
二、通过@Output让父组件监听子组件的事件
通过@Output注解指明一个输出属性(EventEmitter类型)。通过在子组件里面暴露一个EventEmitter属性,当事件发生时,子组件利用该属性 emits(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。子组件的EventEmitter属性是一个输出属性,所以需要带有@Output装饰器。这一部分可以类比JAVA里面的接口的使用。相当于在父组件里面实现接口,在子组件里面调用接口。
通过@Output让父组件监听子组件的事件的使用也非常简单。我们分为三个步骤:
- 子组件里面明确我们要把什么事件抛出去给父组件(定义一个@Output()注解修饰的EventEmitter属性),
- 抛出事件。子组件做了什么动作之后抛出事件。调用EventEmitter属性的emit()方法抛出事件。
- 父组件里面绑定事件处理器,当子组件有事件抛出来的时候会调用父组件的处理函数。
我还是以一个非常简单的例子来说明,我们在子组件里面定义一个button,当button点击的时候。把事件抛给父组件。统计点击的次数。
子组件相关代码
import {Component, EventEmitter, OnChanges, Output} from '@angular/core';
@Component({
selector: 'app-data-child',
template: `
`
})
export class DataChildComponent {
// @Output定义一个准备回调父组件的事件EventEmitter也是可以传递参数的
@Output() voted = new EventEmitter();
vote(agreed: boolean) {
// 把事件往上抛出去,可以带参数
this.voted.emit(agreed);
}
}
父组件相关代码
import {Component} from '@angular/core';
@Component({
selector: 'app-data-parent',
styleUrls: ['./data-parent.component.css'],
template: `
点击 {{clickCount}} 次
`
})
export class DataParentComponent {
clickCount = 0;
/**
* 子组件抛上来的事件
*/
onVoted(agreed: boolean) {
this.clickCount++;
}
}
三、父子组件通过本地变量互动
在父组件模板里,新建一个本地变量来代表子组件(指向子组件)。然后利用这个变量来读取子组件的属性和调用子组件的方法。
一个本地变量互动的简单实例,在父组件里面有两个按钮:一个开始按钮、一个结束按钮。调用子组件里面的开始和结束方法。在下面代码中chiild指向的就是子组件,然后通过chiild来调用子组件里面的方法。
子组件相关代码
import {Component} from '@angular/core';
@Component({
selector: 'app-data-child',
template: `{{message}}
`,
styleUrls: ['./data-child.component.css']
})
export class DataChildComponent {
message = '初始值';
onStart(): void {
this.message = '父组件告诉开始了';
}
onEnd(): void {
this.message = '父组件告诉结束了';
}
}
父组件相关代码
import {Component} from '@angular/core';
@Component({
selector: 'app-data-parent',
template: `
`,
styleUrls: ['./data-parent.component.css']
})
export class DataParentComponent {
}
父子组件通过本地变量互动缺点是,本地变量的作用范围只是html(模板)文件里面。在ts文件里面没办法使用。并且只能是单向的,只能在父组件的模板里面调用子组件的属性或者方法。
四、父组件通过@ViewChild()调用子组件里面的属性方法
父子组件通过本地变量互动的缺点是变量只能在模板里面使用,没办法在ts文件代码里面使用。@ViewChild()就是来解决这个办法的。
当父组件类需要访问子组件属性或者方法的时候,可以把子组件作为 ViewChild,注入到父组件里面。
我们还是对上面的例子做一个简单的修改,父组件告诉子组件开始和结束。
子组件代码
import {Component} from '@angular/core';
@Component({
selector: 'app-data-child',
template: `{{message}}
`,
styleUrls: ['./data-child.component.css']
})
export class DataChildComponent {
message = '初始值';
onStart(): void {
this.message = '父组件告诉开始了';
}
onEnd(): void {
this.message = '父组件告诉结束了';
}
}
父组件代码
import {Component, ViewChild} from '@angular/core';
import {DataChildComponent} from './data-child.component';
@Component({
selector: 'app-data-parent',
template: `
`,
styleUrls: ['./data-parent.component.css']
})
export class DataParentComponent {
@ViewChild(DataChildComponent)
private childComponent: DataChildComponent;
start(): void {
this.childComponent.onStart();
}
end(): void {
this.childComponent.onEnd();
}
}
当在一个父组件里面有同一个子组件多个的时候,又应该怎么处理呢。
import {Component, ViewChild} from '@angular/core';
import {DataChildComponent} from './data-child.component';
@Component({
selector: 'app-data-parent',
template: `
`,
styleUrls: ['./data-parent.component.css']
})
export class DataParentComponent {
@ViewChild('chiild1')
private childComponent1: DataChildComponent;
@ViewChild('chiild2')
private childComponent2: DataChildComponent;
start(): void {
this.childComponent1.onStart();
this.childComponent2.onStart();
}
end(): void {
this.childComponent1.onEnd();
this.childComponent2.onEnd();
}
}
五、父子组件通过服务通讯
父组件和它的子组件共享同一个服务(父组件和子组件使用的是同一个服务实例,说白了就是同一个对象)。父子组件间通过发布订阅的消息机制来实现通讯,一个发布消息,一个订阅消息。说白了就是观察值模式。
我们用一个父子组件的相互通信来做一个简单的说明。MissionService就是中间服务,MissionService里面有两个bservable属性。用来发布订阅消息的。在一个组件里面发布另一个组件里面订阅。
service->MissionService
import {Injectable} from '@angular/core';
import {Subject} from 'rxjs';
@Injectable()
export class MissionService {
// Subject可以看着是一个桥梁或者代理
private childToParentSubject = new Subject();
private parentToChildSubject = new Subject();
// 定义观察者(Observable变量在定义的时候都会在后面加上$)
childToParentObservable$ = this.childToParentSubject.asObservable();
parentToChildObservable$ = this.parentToChildSubject.asObservable();
// 父组件给子组件发送消息,这样parentToChildObservable$就能收到消息
parentSendMessageToChild(mission: string) {
this.parentToChildSubject.next(mission);
}
// 子组件给父组件发送消息,这样childToParentObservable$就能收到消息
childSendMessageToParent(astronaut: string) {
this.childToParentSubject.next(astronaut);
}
}
子组件对应代码
import {Component, OnDestroy} from '@angular/core';
import {MissionService} from './mission.service';
import {Subscription} from 'rxjs';
@Component({
selector: 'app-service-child',
template: `
收到父组件的消息: {{message}}
`,
styleUrls: ['./service-child.component.css']
})
export class ServiceChildComponent implements OnDestroy {
message = '';
subscription: Subscription;
constructor(private missionService: MissionService) {
// 订阅消息
this.subscription = missionService.parentToChildObservable$.subscribe(
mission => {
this.message = mission;
});
}
// 发送消息
sendMessage() {
this.missionService.childSendMessageToParent('我是子组件给你发消息了哈');
}
ngOnDestroy() {
// 组件销毁的时候,subscription需要取消订阅
this.subscription.unsubscribe();
}
}
父组件对应代码
import {Component, OnDestroy} from '@angular/core';
import {MissionService} from './mission.service';
import {Subscription} from 'rxjs';
@Component({
selector: 'app-service-parent',
template: `
收到子组件的消息: {{message}}
`,
styleUrls: ['./service-parent.component.css'],
providers: [MissionService]
})
export class ServiceParentComponent implements OnDestroy{
message = '';
subscription: Subscription;
constructor(private missionService: MissionService) {
// 订阅消息,当数据改变的时候,会调用到改函数
this.subscription = missionService.childToParentObservable$.subscribe(
astronaut => {
this.message = astronaut;
});
}
sendMessage() {
this.missionService.parentSendMessageToChild('我是父组件给你发消息了哈');
}
ngOnDestroy(): void {
// 取消订阅
this.subscription.unsubscribe();
}
}
本文涉及到的所有例子下载地址:DEMO下载地址。demo里面的例子可能会稍微复杂一点。
关于组件间的交互,我们就讲这么多,貌似看起来也不是很复杂,咱也是个初学者(我是做android,以前也没有接触过前段方面的知识)。这里我想在说一句最好的文档还是官方文档,强烈推荐大家看官方文档 https://www.angular.cn/guide/component-interaction#parent-interacts-with-child-via-emlocal-variableem 。