[Angular] ViewChild & ContentChild

ViewChild(ren) & ContenChild(ren) 可以讓Component取得template裡的其他Component,取得後就可以操作該component了,而這功能搭配使用,所產生出來的效果是非常強大的.

在之前的文章有提到View和Content的差異性,Angular2的ViewChild(ren)和ContentChild(ren)也是根據該原則搜尋取得Component(s)

  • [View|Content]Child : 回傳單一物件
  • [View|Content]Children: 回傳QueryList<型別>, 是一個陣列

ViewChild & ViewChildren

宣告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export interface ViewChildDecorator {
(selector: Type<any>|Function|string, {read}?: {read?: any}): any;
new (selector: Type<any>|Function|string, {read}?: {read?: any}): ViewChild;
}

export const ViewChild: ViewChildDecorator = makePropDecorator(
'ViewChild',
[
['selector', undefined], {
first: true,
isViewQuery: true,
descendants: true,
read: undefined,
}
],
Query);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export interface ViewChildrenDecorator {
(selector: Type<any>|Function|string, {read}?: {read?: any}): any;
new (selector: Type<any>|Function|string, {read}?: {read?: any}): ViewChildren;
}

export const ViewChildren: ViewChildrenDecorator = makePropDecorator(
'ViewChildren',
[
['selector', undefined], {
first: false,
isViewQuery: true,
descendants: true,
read: undefined,
}
],
Query);

使用方式

1
@ViewChild(@selector) variable: typeOf Variable(can be any)

搜尋的@selector可以傳入的型別有 Type<any>|Function|string,可以直接搜尋ReferenceId或是Component Class.

  • selector by string: 搜尋ReferenceId(eg. #id), 如果找到相對應的Element有對應的component, 會回傳該Component, 如果沒有找到對應的Component,則回傳 ElementRef型別,如果ReferenceId找不到時,就回傳Undefined
  • selector by ComponentClass: 搜尋By Component Class, 如果有符合時,回傳Component Type, 否則回傳Undefined

LifeHook

ViewChild(ren) 會在 AfterViewInit 之後,才能取得到資料,

範例

Sample 1 (利用ReferenceId)

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
@Component({
selector: 'app-input',
template: `
<input type="text" />
`
})
export class InputComponent {
name: string = 'this is inputComponent';
}

@Component({
selector: 'app-root',
template: `
<label>by referebce id #input</label>
<input type="text" #input1 />
<br/>
<label>by component</label>
<app-input #input2></app-input>
<br />
<button type="button" (click)="getValue()">getValue</button>
`,
styles: []
})
export class AppComponent implements AfterViewInit {
@ViewChild('input1') input1: ElementRef;
@ViewChild('input2') input2: InputComponent;

constructor() { }

ngAfterViewInit() {
console.log('#input afterViewInit:', this.input1);
console.log('app-input afterViewInit:', this.input2);
}

getValue() {
console.log(this.input1.nativeElement.value);
}
}

Console log的結果

Sample2 (by ComponentClass)

1
@ViewChild(InputComponent) input2: InputComponent;

結果如上

Sample3 (ViewChildren)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component({
selector: 'app-root',
template: `
...
<app-input #input2></app-input>
<app-input></app-input>
<app-input></app-input>
<app-input></app-input>
...
`,
styles: []
})
export class AppComponent implements AfterViewInit {
...
@ViewChildren(InputComponent) appInputs: QueryList<InputComponent>;
...
}

輸出結果

ContentChild & ContentChildren

宣告

1
2
3
4
5
6
7
export interface ContentChildDecorator {
/**
* @docsNotRequired
*/
(selector: Type<any>|Function|string, {read}?: {read?: any}): any;
new (selector: Type<any>|Function|string, {read}?: {read?: any}): ContentChild;
}

使用方式

1
@ContentChild(@selector) variable: typeOf Variable(can be any)

搜尋的@selector可以傳入的型別有 Type<any>|Function|string,可以直接搜尋ReferenceId或是Component Class.

  • selector by string: 搜尋ReferenceId(eg. #id), 如果找到相對應的Element有對應的component, 會回傳該Component, 如果沒有找到對應的Component,則回傳 ElementRef型別,如果ReferenceId找不到時,就回傳Undefined
  • selector by ComponentClass: 搜尋By Component Class, 如果有符合時,回傳Component Type, 否則回傳Undefined
  • 這裡所指的ReferenceId是指ParentControl在使用子Component時,於Content Area所指定的ReferenceId. 基於此理由,搜尋by ComponentClass會比較準確,也比較直覺

LifeHook

ContentChild(ren) 會在 AfterContentInit 之後,才能取得到資料,

範例

sample (search By Component Class)

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
@Component({
selector: 'app-input',
template: `
<input type="text" />
<ng-content select="app-label"></ng-content>
`
})
export class InputComponent implements AfterContentInit {
@ContentChild(LabelComponent) content: any;

name: string = 'this is inputComponent';
constructor() { }

ngAfterContentInit() {
console.log('ChildComponent AfterContentInit: ', this.content);
}
}

@Component({
selector: 'app-label',
template: `
<p>
label works!
</p>
`
})
export class LabelComponent implements OnInit {
name: string = 'This is a label component';

constructor() { }

ngOnInit() {
}

someFunction() {

}
}

// Parent Component
template: `
...
<app-input>
<app-label></app-label>
</app-input>
...


輸出結果

Sample 2(search by ReferenceId)

這裡所指的ReferenceId是指在parentComponent內所定義的Id

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
@Component({
selector: 'app-input',
template: `
<input type="text" />
<ng-content select="app-label"></ng-content>
`
})
export class InputComponent implements AfterContentInit {
@ContentChild('label') content: any;

name: string = 'this is inputComponent';
constructor() { }

ngAfterContentInit() {
console.log('ChildComponent AfterContentInit: ', this.content);
}
}

// ParentComponent
<app-input #input2>
<app-label #label></app-label>
</app-input>
<app-input>
<app-label #label></app-label>
</app-input>

輸出結果同上,這裡有另外一個需要注意的地方,#[id] 如果是在View階層時,是不能有重複的,但是如果是Content階層時,在不同的View Component是可以重複的,因為是生存不在不同的scope.

應用

一個Tbas套件的組成可以是一個tabs下面有多個tab, 大概會是長這樣

1
2
3
4
5
6
7
8
9
10
11
<tabs>
<tab>
<p>This Tab Content 1</p>
</tab>
<tab>
<p>This Tab Content 1</p>
</tab>
<tab>
<p>This Tab Content 1</p>
</tab>
</tabs>

所以我們至少需要兩個Components, tabstab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {Component,Input} from 'angular2/core';

@Component({
selector: 'tab',
template: `
<p [hidden]="!show">
<ng-content></ng-content>
</p>
`
})
export class TabComponent {
@Input()
tabTitle:string;

show:boolean = false;
}
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
import {Component,ContentChildren,QueryList,AfterContentInit} from 'angular2/core';
import {TabComponent} from './tab';

@Component({
selector: 'tabs',
template: `
<ul class="tab-list">
<li *ngFor="#tab of tabs" [class.active]="selectedTab===tab" (click)="onSelect(tab)">
{{tab.tabTitle}}
</li>
</ul>
<ng-content></ng-content>
`,
styles: [`
.tab-list{
list-style:none;
overflow:hidden;
padding:0;
}

.tab-list li{
cursor:pointer;
float:left;
width:60px;
height:30px;
line-height:30px;
text-align:center;
background-color:gray;
}

.tab-list li.active{
background-color:red;
}
`]
})
export class TabsComponent implements AfterContentInit {
@ContentChildren(TabComponent)
tabs:QueryList<TabComponent>;

selectedTab:TabComponent;

ngAfterContentInit() {
this.select(this.tabs.first);
}

onSelect(tab) {
this.select(tab);
}

select(tab) {
this.tabs.forEach((item)=>{
item.show = false;
});

this.selectedTab = tab;
this.selectedTab.show = true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component({
selector: 'my-app',
template: `
<h2>App Component</h2>
<tabs>
<tab tabTitle="First">
<p>This Tab Content 1</p>
</tab>
<tab tabTitle="Second">
<p>This Tab Content 2</p>
</tab>
<tab tabTitle="third">
<p>This Tab Content 3</p>
</tab>
</tabs>
`,
directives: [TabsComponent,TabComponent]
})
export class AppComponent {
}

顯示效果

延伸閱讀