Angularjs2 组件(指令)交互

在组件化开发中,会将相同的功能或者业务封装为独立的组件,以达到组件复用,在各个组件的组合使用中,避免不了在独立组件间进行数据事件的传递。

在Angular1中,我们常常会使用$scope来进行交互绑定,这里我们就按照官方文档的顺序简单聊下Angular2中的交互,主要的交互方式大致有一下途径:

接下来将通过一个示例来完成各种方法的学习。

** 准备 **
首先让我们准备两个组件OuterPanelInnerPanel,两者为父子级嵌套关系。
创建InnerPanel组件,代码如下:

innerComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from '@angular/core';
/**
* <innerPanel></innerPanel>
*/
@Component({
selector: 'innerPanel',
template: `
<div class="inner-panel">inner Panel</div>
`,
styles: [`
.inner-panel{
border: 1px #000 solid;
padding: 20px;
}
`]
})
export class InnerComponent {}

创建OuterPanel组件,代码如下:

outerComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component } from '@angular/core';
/**
* <outerPanel></outerPanel>
*/
@Component({
selector: 'outerPanel',
template: `
<div class="outer-panel">
<span>outer Panel</span>
<innerPanel></innerPanel>
</div>
`,
styles: [`
.outer-panel{
border: 1px red solid;
width: 500px;
padding: 20px;
}
`]
})
export class OuterComponent {}

然后我们在模板中引用组件

app.component.html
1
<outerPanel></outerPanel>

运行示例,将会在界面上渲染出如下结果:
渲染结果

接着我们将扩展上面的两个组件,来完成各种方式的数据交互。


** 通过数据绑定 **

数据绑定在前面的也有过介绍,这里简单的再提一下。我们需要做个扩展,要求是:在InnerPanel组件中,声明一个变量innerName,然后在InnerPanel的模板中进行展示。

innerComponent.ts
1
2
3
export class InnerComponent {
innerName = 'Daniel';
}

在组件中添加变量的声明代码,并修改组件模板进行输出展示。

innerComponent.ts
1
2
3
4
5
6
template: `
<div class="inner-panel">
<span>inner Panel</span>
<div>innerName: {{innerName}}</div>
</div>
`,

保存运行代码,将会看到变量在inner Panel区域输出了。
渲染结果

当前我们只是简单的输出组件自身的变量值,那么如果是在outerPanel中进行赋值呢?默认组件中的属性都是私有的,需要设置公共后才能被外界访问,修改InnerPanel组件代码如下:

innerComponent.ts
1
2
3
4
5
6
7
//引入Input
import { Component, Input } from '@angular/core';
...
//属性添加@Input注解
export class InnerComponent {
@Input() innerName = 'Daniel';
}

然后在调用组件的时候,进行赋值:

outerComponent.ts
1
2
3
4
5
6
7
template: `
<div class="outer-panel">
<span>outer Panel</span>
<!-- 常量赋值 -->
<innerPanel innerName="outerName"></innerPanel>
</div>
`,

渲染结果
可以看到innerName="outerName"已经生效,传值到了内部组件,这里,如果InnerPanel组件不添加@Input注解,运行时则会报错:

异常信息
1
2
Uncaught Error: Template parse errors:
Can't bind to 'innerName' since it isn't a known property of 'innerPanel'. (" <span>outer Panel</span>

所以,在开发的时候一定要注意属性是否需要设置为公共的。

前面我们传的值是个常量值,实际开发中多数都是动态的数据,那么下面我们来学习下动态值的传递,其实非常简单,在开始之前,先理解下这几条语句的区别

示例
1
2
3
<innerPanel innerName="outerName"></innerPanel>
<innerPanel innerName="{{outerName}}"></innerPanel>
<innerPanel [innerName]="outerName"></innerPanel>

语句1:这个已经见到过了,就是将innerPanelinnerName属性赋值为outerName
语句2、3:这两条语句的效果是一样的,都可以实现动态数据的绑定。语句2通过插值表达式来绑定输出属性值,然后会绑定更新到innerPanel组件中。语句3直接使用属性绑定来绑定组件属性。

接着我们继续扩展下代码,添加一个按钮事件,来实现属性值的变化,来测试一下动态数据的绑定。

outerComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
//在模板中添加按钮,并绑定事件,同时修改绑定属性名
template: `
<div class="outer-panel">
<span>outer Panel</span>
<button (click)="change()">Change</button>
<!-- 这里同时使用两种方式 -->
<innerPanel [innerName]="outerName2Inner"></innerPanel>
<innerPanel innerName="{{outerName2Inner}}"></innerPanel>
</div>
`,
...
//修改属性名,通过计数器来修改属性值
export class OuterComponent {
outerName2Inner = "OuterDaniel";
//通过计时器变动,测试修改效果
i = 0;
change(){
this.outerName2Inner = `Outer Changed:${this.i++}`;
}
}

通过点击按钮,我们可以发现外部组件中值的变化会通过绑定关系,传递到内部组件中进行展示。
渲染结果

接下来我们来试下,函数的绑定,实现将


** 通过组件引用 **

组件引用的通讯方式,主要有可以细分为两种 本地变量(local variable)、模板引用变量(Template reference variables)@ViewChld(),这两种方式的核心思想都是在当前的上下文中创建一个对子组件的引用,然后通过这个创建的引用来访问子组件的属性和方法。

最要的区别是:本地变量仅能在组件的模板中使用,也就是说只能在HTML中使用。接下来应该是什么,大家应该已经想到了,对的,**@ViewChild()**仅能在组件的JavaScript逻辑中使用的了。接下来,我们继续扩展前面的例子,然后进行学习测试。

先对模板引用变量进行下介绍,分别在组件中加入相应的测试代码

innerComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
import { Component, Input } from '@angular/core';
...
export class InnerComponent {
...
//组件中添加一个测试变量,用来在父组件中访问
innerVarName = 'innerVarName';
//组件中添加一个测试函数,用来在父组件中调用
innerVarNameChange(){
this.innerVarName = "innerChanged";
}
...
}
outerComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
//模板变量引用,可以直接在模板中对组件进行引用
template: `
<div class="outer-panel">
<span>outer Panel</span>
<button (click)="change()">Change</button>
<!-- 添加两个按钮,分别通过子组件引用来进行测试:输出子组件的属性和函数调用 -->
<button (click)="innerPanel1.innerVarChange()">{{innerPanel1.innerVarName}}</button>
<button (click)="innerPanel2.innerVarChange()">{{innerPanel2.innerVarName}}</button>
<!-- 使用#符号,对子组件设置引用 -->
<innerPanel #innerPanel1 [innerName]="outerName2Inner"></innerPanel>
<!-- 使用ref-前缀,对子组件设置引用 -->
<innerPanel ref-innerPanel2 innerName="{{outerName2Inner}}"></innerPanel>
</div>
`,
...

点击新增的两个按钮,我们可以看到,对应的Button值已经发生了变化。
点击测试按钮结果

接着我们来介绍下**@ViewChild()**的方式,主要在OuterComponent中修改下引用方式,代码如下:

outerComponent.ts
1
2
3
4
5
//导入ViewChild、ViewChildren、QueryList和AfterViewInit
import { AfterViewInit, Component, ViewChild, ViewChildren, QueryList } from '@angular/core';
//导入组件对象
import { InnerComponent } from './innerComponent';
...

其中ViewChildViewChildren都是可以实现子组件的注入,主要区别在于ViewChildren的注入结果是一个QueryList的集合。

ViewChildViewChildren都可以使用通过名称类型来进行注入,我们在outerComponent.ts中加入如下代码:

outerComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
...
//通过类型进行注入,只使用当前上下文中的第一个子组件实例
@ViewChild(InnerComponent) innerPanel0: InnerComponent;
//通过名称进行注入,可以直接对当前模板中的子组件进行指定性的引用
@ViewChild('innerPanel1') innerPanel1: InnerComponent;
@ViewChild('innerPanel2') innerPanel2: InnerComponent;
...
//通过类型进行注入,结果为QueryList类型,可从_results中获取子组件信息
@ViewChildren(InnerComponent) innerPanels: QueryList<InnerComponent>;
//通过名称进行注入,结果为QueryList类型,可从_results中获取子组件信息
//(意义不大,这里只是为了介绍,如果只是取指定名称的,直接使用@ViewChild就行)
@ViewChildren('innerPanel1') innerPanel3: QueryList<InnerComponent>;

接下来我们要在父组件中来使用下子组件的引用,我们前面的代码导入了AfterViewInit,然后让组件OuterComponent实现接口。

outerComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
//修改模板代码,用以区分两个子组件
<innerPanel #innerPanel1 [innerName]="outerName2Inner"></innerPanel>
<innerPanel ref-innerPanel2 innerName="{{outerName2Inner + '-diff'}}"></innerPanel>
...
export class OuterComponent implements AfterViewInit {
...
ngAfterViewInit() {
//在View初始化完成后使用组件
console.log("@ViewChild(InnerComponent):", this.innerPanel0);
console.log("@ViewChild('innerPanel1'):", this.innerPanel1);
console.log("@ViewChild('innerPanel2'):", this.innerPanel2);
console.log("@ViewChildren(InnerComponent):", this.innerPanels);
console.log("@ViewChildren('innerPanel1'):", this.innerPanel3);
}
}

我们看到输出结果:
1:蓝框中所示,通过类型引用,默认会使用第一个自组建势力,如果换一下模板中两个子组件的顺序,则这里也会改变
2:红框中所示,QueryList是个Array,只有一个长度,表示只拿到了指定的子组件
既然已经拿到了组件的引用,那么就可以访问组件的属性和函数了。
子组件输出结果


** 通过服务组件 **

父子组件通过服务组件来进行通讯,主要利用的就是服务组件的单例模式,然后通过共享服务实例的数据来完成交互。这里我们将会在学习依赖注入事件时进行介绍,后续会更新链接至此。

** 其他 **
当然,父子组件间的通讯还有一些其他的方法扩展,比如设置属性的setter访问器或使用组件的ngOnChanges()来监听输入值改变,两种方式的使用原型如下,大家可以自行测试使用一下,具体的方法选用,还得主要看具体的业务场景,来选择合适的方法。

setter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//通过设置setter来获取输入绑定,官方示例
export class NameChildComponent {
private _name = '';
@Input()
set name(name: string) {
this._name = (name && name.trim()) || '<no name set>';
}
get name(): string { return this._name; }
}
//通过实现ngOnChanges方法来获取组件属性变更
import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
export class VersionChildComponent implements OnChanges {
@Input() major: number;
@Input() minor: number;
changeLog: string[] = [];
//实现方法
ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
let log: string[] = [];
for (let propName in changes) {
let changedProp = changes[propName];
let to = JSON.stringify(changedProp.currentValue);
if (changedProp.isFirstChange()) {
log.push(`Initial value of ${propName} set to ${to}`);
} else {
let from = JSON.stringify(changedProp.previousValue);
log.push(`${propName} changed from ${from} to ${to}`);
}
}
this.changeLog.push(log.join(', '));
}
}