[译]深入分析Angular中的ViewChild,ElementRef和ViewChildren

原文 In-depth analysis of ViewChild, ElementRef and ViewChildren in Angular

Angular @ViewChild 是一个最简单但同时也是最难的装饰器。你可以在做编码的时候学习它。@ViewChild是最常用的装饰器之一。该装饰器提供了大量的功能。在本文中,我们将深入研究@ViewChildElementRefViewChildren装饰器。我们将看到如何使用它的例子,并讨论实际应用中的问题。

什么是@ViewChild装饰器

@ViewChild是一个属性装饰器。它提供了一个强大的方式来访问子元素和属性。当我们想要访问父组件中的子组件元素、表单属性脏检查、触发事件,以及父组件中的指令时,@ViewChild是一种非常简单的访问方式。@ViewChild是一种非常简单的访问子元素的方法。

@ViewChild元数据

  • Selector(选择器)。指令名称,如果同一个子控件在页面上使用多次。这是一个非常有用的属性。

  • Static(静态的)。这是Angular新版本中的必填字段。默认的属性是false。在Angular 9版本中,静态数据默认为false。当你想在ngOnInit中访问子元素时,请使用static:true。

@ViewChild装饰器和ngAfterViewInit

当父组件开始渲染时,ViewChild装饰器不可用。它将不会在ngOnInit()生命周期钩子中可用。它将在ngAfterViewInit生命周期钩子中可用。
在我们的例子中,我们有一个父元素是div。有三个子元素。这三个子元素都是不同类型的。第一个是一个div元素,第二个是一个段落,第三个是h5标题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div #parent>

<div #divelement1>
Visible elements
</div>

<ng-container *ngIf=”show”>
<p #divelement2>
Hidden element 1
</p>
</ng-container>

<ng-container *ngIf=”show”>
<h5 #divelement3>
Hidden element 2
</h5>
</ng-container>

</div>

这里我们有三个子元素作为ViewChild元素,它在父组件中可以ngAfterViewInit。注意,对于Angular 8以上版本来说,你必须确保设置,{ static: false },这是其他Angular版本的默认设置。

1
2
3
4
5
6
@ViewChild("divelement1", { static: false });
ngAfterViewInit() {
console.log('--->ngAfterViewInit');
console.log('divelement1', this.divelement1);
}

在页面渲染后,你可以看到元素细节在组件中被初始化。

如果我们要访问ngOnInit生命周期钩子上的第一个开发元素。它将以未定义的形式出现。

1
2
3
4
ngOnInit() {
console.log(‘+++++ngOnInit++++++’);
console.log(‘divelement1’, this.divelement1);
}

如何用*ngIf指令使用@ViewChild。ElementRef

*ngIf@ViewChild的一个重要指令。如果组合使用时,很有可能会错过父组件中@ViewChild的值。下面是使用@ViewChild和*ngIf的最优雅的方法。在父组件的HTML中使用以下代码行。现在我们在父组件中定义divelement2为@ViewChild。我们在这里使用ng-container只在我们想显示子组件的时候才显示,即在演示中,我们想在点击显示子组件按钮时显示子组件。

1
2
3
4
5
<ng-container *ngIf="show">
<p #divelement2>
Hidden element 1
</p>
</ng-container>

在父组件中,我们使用divelement2作为ElementRef。现在写一个setter方法,当子元素可见时将设置ele2。如果子元素不可见,就不会设置this.divelement2。一旦按钮显示子组件被点击,你可以在控制台中看到这个值。

1
2
3
4
5
@ViewChild('divelement2')
set ele2(v: ElementRef) {
console.log('This element is set when ngIf is true', v);
this.divelement2 = v;
}

现在点击显示隐藏子按钮。我们可以看到元素现在已经设置好了。

@ViewChildren的迭代器与@ViewChild的对比

@ViewChild提供了对子元素的控制,但是@ViewChildren提供了更多对元素的控制。@ViewChildren提供了一个引用列表,而@ViewChild只是一个单一的引用。@ViewChildren提供了一个对多个元素的引用,可以作为一个迭代列表使用。

1
2
3
4
5
<ng-container>
<h5 #divelement3>
Hidden element 2
</h5>
</ng-container>

在parent中,组件将parent定义为QueryList<ElementRef>。在ngAfterViewInit中订阅子元素的变化。下面是完整的代码片段。所以在这里你可以得到所有子元素的元素以及每个元素的所有属性,如元素类型输入控件等。@ViewChildren的功能非常强大,你甚至可以得到DOM元素的 align 属性。在这里,我们可以订阅子元素的任何变化。

1
2
3
4
5
6
7
8
9
10
@ViewChildren(‘parent’) children: QueryList<ElementRef>;
ngAfterViewInit() {
this.children.changes.subscribe((comps: QueryList<ElementRef>) => {
console.log(‘====>ngAfterViewInit.changes’);
console.log(‘divelement1’, this.divelement1);
console.log(‘divelement2’, this.divelement2);
console.log(‘divelement3’, this.divelement3);
});
}

一旦你点击显示隐藏的子按钮,显示属性为真,这意味着有子组件的变化,所以订阅者会得到通知。现在订阅者日志如下。

现在,一旦我们点击控制台中的Log Child,就会出现下面的样子。

HTML模板的代码。

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
<h1>ViewChild Example with *ngif</h1>
<h5>Note, for Angular 8 you have to make sure to set static: false , which is a default setting in other Agnular
versions</h5>
<div #parent>

<div #divelement1>
Visible elements
</div>

<ng-container *ngIf="show">
<p #divelement2>
Hidden element 1
</p>
</ng-container>

<ng-container>
<h5 #divelement3>
Hidden element 2
</h5>
</ng-container>

</div>

<div class="row">
<div class="col-md-2">
<button *ngIf="true" id="scrollHere" class="btn btn-primary" (click)="showIt()">Show Hidden Child
</button>
</div>
</div>
<div class="row">
<div class="col-md-12">&nbsp;</div>
</div>
<div class="row">
<div class="col-md-2">
<button *ngIf="true" id="scrollHere" class=" btn btn-primary" (click)="logElements()">Log Child in Console</button>
</div>
</div>

组件模板的代码。

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
59
60
61
62
63
64
65
66
import { Component, ViewChild, ElementRef, ViewChildren, QueryList, OnInit, AfterViewInit } from '@angular/core';

@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {

show = false;

// view children to show that can listen to changes on child elements
@ViewChildren('parent') children: QueryList<ElementRef>;

@ViewChild("divelement1", { static: false })

divelement1: ElementRef;
divelement2: ElementRef;

// @ViewChild('divelement1')
// set ele1(v: ElementRef) {
// console.log('set ele1', v);
// this.divelement1 = v;
// }

@ViewChild('divelement2')
set ele2(v: ElementRef) {
console.log('This element is set when ngIf is true', v);
this.divelement2 = v;
}

@ViewChild('divelement3') divelement3: ElementRef;

ngOnInit() {
console.log('+++++ngOnInit++++++');
console.log('divelement1', this.divelement1);
console.log('divelement2', this.divelement2);
console.log('divelement3', this.divelement3);

}

showIt() {
this.show = true;
}

logElements() {
console.log('divelement1', this.divelement1);
console.log('divelement2', this.divelement2);
console.log('divelement3', this.divelement3);
}

ngAfterViewInit() {
console.log('--->ngAfterViewInit');
console.log('divelement1', this.divelement1);
console.log('divelement2', this.divelement2);
console.log('divelement3', this.divelement3);

this.children.changes.subscribe((comps: QueryList<ElementRef>) => {
console.log('====>ngAfterViewInit.changes');
console.log('divelement1', this.divelement1);
console.log('divelement2', this.divelement2);
console.log('divelement3', this.divelement3);
});
}
}

概要

在这个文章中,我们已经学习了@ViewChild,以及当与ngIf指令一起使用时如何渲染它。我们还看到了如何使用ElementRef和@ViewChildren装饰符。通过示例,我们已经看到了如何初始化这些装饰符。
附上作者stackblitz,以查看更多的代码。