qrcode

马上订阅,开启修仙之旅

Angular ViewChild和ViewChildren

ViewChild

Angular 为我们提供 ViewChild 和 ViewChildren 装饰器来获取模板视图中匹配的元素。ViewChild 是属性装饰器,用来从模板视图中获取匹配的元素。视图查询在 ngAfterViewInit 钩子函数调用前完成,因此在 ngAfterViewInit 钩子函数中,就能正常获取查询的元素。

现在我们先来更新一下 AuthFormComponent 组件(关于它的出身,可以浏览 “Angular 内容投影” 这篇文章),即把下面的消息提示封装为组件。

1
2
3
<div *ngIf="showMessage">
保持登录30天
</div>

基于上面的模板,我们可以简单的创建一个 AuthMessageComponent 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component } from "@angular/core";

@Component({
selector: "auth-message",
template: `
<div>
保持登录 {{ days }} 天
</div>
`
})
export class AuthMessageComponent {
days: number = 7;
}

创建完 AuthMessageComponent 组件,我们需要同步更新一下 AuthFormComponent 组件,具体如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { Component, Output, EventEmitter, ContentChildren, ViewChild, QueryList, AfterContentInit, AfterViewInit } from '@angular/core';

import { AuthRememberComponent } from './auth-remember.component';
import { AuthMessageComponent } from './auth-message.component';

import { User } from './auth-form.interface';

@Component({
selector: 'auth-form',
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<ng-content select="h3"></ng-content>
<label>
邮箱
<input type="email" name="email" ngModel>
</label>
<label>
密码
<input type="password" name="password" ngModel>
</label>
<ng-content select="auth-remember"></ng-content>
<auth-message
[style.display]="(showMessage ? 'inherit' : 'none')">
</auth-message>
<ng-content select="button"></ng-content>
</form>
</div>
`
})
export class AuthFormComponent implements AfterContentInit, AfterViewInit {

showMessage: boolean;

@ViewChild(AuthMessageComponent) message: AuthMessageComponent;

@ContentChildren(AuthRememberComponent) remember: QueryList<AuthRememberComponent>;

@Output() submitted: EventEmitter<User> = new EventEmitter<User>();

ngAfterViewInit() {
//this.message.days = 30;
}

ngAfterContentInit() {
if (this.message) {
this.message.days = 30;
}
if (this.remember) {
this.remember.forEach((item) => {
item.checked.subscribe((checked: boolean) => this.showMessage = checked);
});
}
}

// ...
}

在上面示例中,我们通过 ViewChild 装饰器来获取 AuthRememberComponent 组件,此外我们在 ngAfterContentInit 生命周期钩子中重新设置天数。以上代码成功运行后,页面能够看到期望的结果。

但如果我们在 ngAfterViewInit 生命周期钩子中重新设置天数,那么在控制台将会抛出以下异常:

1
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'null: 7'. Current value: 'null: 30'.

这是为什么呢?这里不详细展开,有兴趣的同学可以参考 Angular-关于ExpressionChangedAfterItHasBeenCheckedError你需要知道的一切

ViewChildren

与 ContentChild 装饰器类似,ViewChild 装饰器也有与之对应的 ViewChildren 装饰。该装饰器用来从模板视图中获取匹配的多个元素,返回的结果是一个 QueryList 集合。

为了能获取多个匹配的元素,我们需要更新一下 AuthFormComponent 模板,即新增两个 AuthMessageComponent 组件:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Component({
selector: 'auth-form',
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<ng-content select="h3"></ng-content>
<label>
邮箱
<input type="email" name="email" ngModel>
</label>
<label>
密码
<input type="password" name="password" ngModel>
</label>
<ng-content select="auth-remember"></ng-content>
<auth-message
[style.display]="(showMessage ? 'inherit' : 'none')">
</auth-message>
<auth-message
[style.display]="(showMessage ? 'inherit' : 'none')">
</auth-message>
<auth-message
[style.display]="(showMessage ? 'inherit' : 'none')">
</auth-message>
<ng-content select="button"></ng-content>
</form>
</div>
`
})
export class AuthFormComponent implements AfterContentInit, AfterViewInit {

showMessage: boolean;

@ViewChildren(AuthMessageComponent) message: QueryList<AuthMessageComponent>;

@ContentChildren(AuthRememberComponent) remember: QueryList<AuthRememberComponent>;

@Output() submitted: EventEmitter<User> = new EventEmitter<User>();

constructor(private cd: ChangeDetectorRef) {}

ngAfterViewInit() {
if (this.message) {
this.message.forEach((message) => {
message.days = 30;
});
this.cd.detectChanges();
}
}

ngAfterContentInit() {
if (this.remember) {
this.remember.forEach((item) => {
item.checked.subscribe((checked: boolean) => this.showMessage = checked);
});
}
}
}

更新完对应的模板,我们也需要同步更新组件类,即引入 ContentChildren 装饰器,并且在 ngAfterViewInit 生命周期内更新 AuthMessageComponent 组件的 days 属性值。细心的读者可能会发现除了更新属性值之外,还执行了 this.cd.detectChanges() 这句语句。该语句是为了避免抛出以下异常:

1
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'null: 7'. Current value: 'null: 30'.

如果你想继续了解强制变化检测的作用,请阅读 Angular-关于ExpressionChangedAfterItHasBeenCheckedError你需要知道的一切 这篇文章。

Viewchild 和 ElementRef

在 ViewChild 小节,我们使用 @ViewChild(AuthMessageComponent) 装饰器来获取 AuthMessageComponent 组件,ViewChild 装饰器除了支持 Type 类型参数外,还支持字符串参数,而字符串的值是模板引用的值。

首先我们来设置模板引用:

1
2
3
4
<label>
邮箱
<input type="email" name="email" ngModel #email>
</label>

接下来更新 AuthFormComponent 组件类,使用 ViewChild 装饰器来获取邮箱输入框的元素引用:

1
@ViewChild('email') email: ElementRef;

最后在 ngAfterViewInit 生命周期钩子中输出 email 属性的值:

1
2
3
4
5
6
7
8
9
ngAfterViewInit() {
console.log(this.email);
if (this.message) {
this.message.forEach((message) => {
message.days = 30;
});
this.cd.detectChanges();
}
}

以上代码成功运行后,控制台会输出以下内容:

1
2
3
ElementRef {nativeElement: input.ng-untouched.ng-pristine.ng-valid}
nativeElement: input.ng-untouched.ng-pristine.ng-valid
__proto__: Object

在控制台中展开 nativeElement 属性,你会发现该属性对应的值是原生的 DOM 元素,因此我们可以在 ngAfterViewInit 生命周期钩子中执行某些 DOM 操作:

1
2
3
4
5
ngAfterViewInit() {
this.email.nativeElement.setAttribute('placeholder', 'Enter your email address');
this.email.nativeElement.classList.add('email');
this.email.nativeElement.focus();
}

现在虽然我们已经能够正确获取原生的 DOM 元素,并能够进行相关的 DOM 操作。但在实际项目中,我们是不推荐直接使用 DOM API 执行 DOM 操作的,我们要尽量减少应用层与渲染层之间强耦合关系,从而让我们应用能够灵活地运行在不同环境。

为了能够支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer2 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。

1
2
3
4
5
6
7
8
9
10
constructor(
private cd: ChangeDetectorRef,
private renderer: Renderer2) {
}

ngAfterViewInit() {
this.renderer.setAttribute(this.email.nativeElement,
'placeholder', 'Enter your email address');
this.renderer.addClass(this.email.nativeElement, 'email');
}

欢迎小伙伴们订阅前端修仙之路,及时阅读 Angular、RxJS、TypeScript 和 Node.js 最新文章。

qrcode