0%

Angular中的ng-content使用

基础使用

当复用一个组件时,大部分的内容是相同的,只有一部分的内容是不同的,这时可以使用ng-content指令来提高组件的复用性。
内容投影,即通过使用指令来实现内容投影的功能。

以下User的定义如下:

1
2
3
4
5
User定义内容:
export interface User {
email: string;
password: string;
}

定义一个ChildComponent组件:

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
import { Component, Output, EventEmitter } from "@angular/core";

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

@Component({
selector: "app-child",
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<ng-content></ng-content>
<label>
邮箱
<input type="email" name="email" ngModel>
</label>
<label>
密码
<input type="password" name="password" ngModel>
</label>
<button type="submit">
提交
</button>
</form>
</div>
`
})
export class ChildComponent {
@Output() submitted: EventEmitter<User> = new EventEmitter<User>();

onSubmit(value: User) {
this.submitted.emit(value);
}
}

在ParentComponent组件中,使用已经定义的ChildComponent组件:

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
import { Component } from "@angular/core";

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

@Component({
selector: "app-parent",
template: `
<div>
<app-child
(submitted)="createUser($event)">
<h3>注册</h3>
<button type="submit">
注册
</button>
</app-child>
<app-child
(submitted)="loginUser($event)">
<h3>登录</h3>
<button type="submit">
登录
</button>
</app-child>
</div>
`
})
export class ParentComponent {
createUser(user: User) {
console.log("Create account", user);
}

loginUser(user: User) {
console.log("Login", user);
}
}

在app-parent中app-child标签内的多余的html会被投影到ChildComponent组件的’ng-content’中。

select

select属性支持CSS选择器(element,class,attribute[name=xxx])来匹配所需要的内容。如果ng-content上没有设置select属性,他将接收全部内容,或接收不匹配的任何其他的ng-content元素的内容。如果有多个相同element,class等都会被ng-content投影。

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
<div>
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" novalidate>
<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="app-auth-remember"></ng-content>
<ng-content select="button"></ng-content>
</form>
</div>


// app-auth-remember组件内容

import {Component, OnInit, Output, EventEmitter} from '@angular/core';
@Component({
selector: 'app-auth-remember',
template: `
<label>
<input type="checkbox" (change)="onChecked($event.target.checked)">
Keep me logged in
</label>
`,
styleUrls: ['./auth-remember.component.css']
})
export class AuthRememberComponent implements OnInit {
@Output() checked: EventEmitter<boolean> = new EventEmitter<boolean>();

constructor() {
}

ngOnInit() {
console.log('初始化已完成!');
}

onChecked(value: boolean) {
this.checked.emit(value);
}
}

app-child的组件中ng-content的select属性分别对应h3和button,也就是分别映射h3标签和button标签。如果有多个ng-content标签都没有select属性,则默认最后一个ng-content匹配所有的标签。

warning:如果想要正确根据select属性投射内容,限制就是这些标签必须是组件标签的直接子节点。
如果不是直接子节点,将不会被匹配到,如果要解决这个问题就必须在需要被匹配到的标签上,加上属性ngProjectAs="xxx",就可以通过select="xxx"进行投影。

组件投影

组件必须在带有ng-content的组件标签内,这样ng-content才能匹配到组件。当然这样操作会对性能有一定的影响,因为ng-content不会产生内容,他只是投影现有的内容,因此这些组件一开始已经被制造,投影内容的生命周期将被绑定到他被声明的地方,而不是在显示的地方。

child-component

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

@Component({
selector: 'demo-child-component',
template: '<h3>我是child-component组件</h3>'
})
export class ChildComponent implements OnInit {

constructor() {
}

ngOnInit() {
console.log('child-component初始化完成!');
}
}

parent-component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'parent-component',
template: `
<button (click)="show = !show">
{{ show ? 'Hide' : 'Show' }}
</button>
<div class="content" *ngIf="show">
<ng-content></ng-content>
</div>
`
})
export class ParentComponent implements OnInit {
show = true;

constructor() {
}

ngOnInit() {
}
}

然后将child-component投射到parent-component中

1
2
3
<parent-component>
<child-component></child-component>
</parent-component>

在控制台只能看到一次’child-component初始化完成!’,点击按钮切换,不在打印,说明该组件只被实例化了一次——未被销毁和重新创建。当然可以在parent-component标签中对要投影的组件进行控制。

ContentChild

获取ng-content投射组件的内容

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-child",
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<ng-content select="h3"></ng-content>
<label>
Email address
<input type="email" name="email" ngModel>
</label>
<label>
Password
<input type="password" name="password" ngModel>
</label>
<ng-content select="app-auth-remember"></ng-content>
<div *ngIf="showMessage">
保持登录状态30天
</div>
<ng-content select="button"></ng-content>
</form>
</div>
`
})
export class AuthFormComponent implements AfterContentInit {
showMessage: boolean;

@ContentChild(AuthRememberComponent) remember: AuthRememberComponent;

ngAfterContentInit() {
if (this.remember) {
// 因为在app-auth-remember组件已经将值checked进行EventEmitter处理且进行了emit,故而这里可以动态的获取值。
this.remember.checked.subscribe(
(checked: boolean) => (this.showMessage = checked)
);
}
}
// ...
}

通过 ContentChild(AuthRememberComponent) 来设置获取的组件类型,此外我们在生命周期钩子 ngAfterContentInit 中通过订阅 remember 的 checked 输出属性来监听 checkbox 输入框的变化。

ContentChildren

与ContentChild类似通过Content Projection方式设置的视图中获取匹配的多个元素,返回的结果是一个QueryList集合。

在parent-component中添加多个app-auth-remember:

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
@Component({
selector: "app-parent",
template: `
<div>
<app-child
(submitted)="createUser($event)">
<h3>注册</h3>
<button type="submit">
注册
</button>
</app-child>
<app-child
(submitted)="loginUser($event)">
<h3>登录</h3>
<app-auth-remember (checked)="rememberUser($event)"></app-auth-remember>
<app-auth-remember (checked)="rememberUser($event)"></app-auth-remember>
<app-auth-remember (checked)="rememberUser($event)"></app-auth-remember>
<button type="submit">
登录
</button>
</app-child>
</div>
`
})
export class AppComponent {
// ...
}

然后在ChildComponent中引入ContentChildren装饰器

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
import { Component, Output, EventEmitter, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { AuthRememberComponent } from './auth-remember.component';
import { User } from './auth-form.interface';

@Component({
selector: 'app-child',
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="app-auth-remember"></ng-content>
<div *ngIf="showMessage">
保持登录30天
</div>
<ng-content select="button"></ng-content>
</form>
</div>
`
})
export class AuthFormComponent implements AfterContentInit {

showMessage: boolean;

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

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

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

// ...
}

ContentChildren 装饰器返回的是一个 QueryList 集合,在 ngAfterContentInit 生命周期钩子中,我们通过 QueryList 实例提供的 forEach 方法来遍历集合中的元素。QueryList 实例除了提供 forEach() 方法之外,它还提供了数组常用的方法,比如 map()、filter()、find()、some() 和 reduce() 等方法。