Angular开发实践(八): 使用ng-content进行组件内容投射

在Angular中,组件属于特殊的指令,它的特殊之处在于它有本身的模板(html)和样式(css)。所以使用组件可使咱们的代码具备强解耦、可复用、易扩展等特性。一般的组件定义以下:javascript

demo.component.ts:css

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component',
	templateUrl: './demo.component.html',
	styleUrls: ['./demo.component.scss']
})
export class DemoComponent implements OnInit {

	constructor() {
	}

	ngOnInit() {
	}
}
复制代码

demo.component.html:html

<div class="demo">
	<h2>
		demo-component - 我是一个简单的组件
	</h2>
</div>
复制代码

demo.component.scss:java

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}
}
复制代码

此时咱们引用该组件,就会呈现该组件解析以后的内容:node

<demo-component></demo-component>
复制代码

假设如今有这样的需求,这个组件可以接受外部投射进来的内容,也就是说组件最终呈现的内容不只仅是自己定义的那些,那该怎么作呢?这时就要请出本文的主角 ng-contentapi

简单投射

咱们先从最简单开始,在 demo.component.html 中添加 ,修改后的 demo.component.html 和 demo.component.scss 以下:app

demo.component.html:ide

<div class="demo">
	<h2>
		demo-component - 可嵌入外部内容的组件
	</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
</div>
复制代码

demo.component.scss:性能

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}

	.content {
		padding: 10px;
		margin-top: 10px;
		line-height: 20px;
		color: #FFFFFF;
		background-color: #de7d28;
	}
}
复制代码

为了效果展现特地将 所在的容器背景色定义为橙色。ui

这时咱们在引用该组件时能够从外部投射内容,外部内容将在橙色区域显示:

<demo-component>
	我是外部嵌入的内容
</demo-component>
复制代码

针对性投射

若是同时存在几个 ,那外部内容将如何进行投射呢?

咱们先看个示例,为了区别,我再新增一个蓝色区域的 ,修改后的 demo.component.html 和 demo.component.scss 以下:

demo.component.html:

<div class="demo">
	<h2>
		demo-component - 可嵌入外部内容的组件
	</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
	<div class="content blue">
		<ng-content></ng-content>
	</div>
</div>
复制代码

demo.component.scss:

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}

	.content {
		padding: 10px;
		margin-top: 10px;
		line-height: 20px;
		color: #FFFFFF;
		background-color: #de7d28;
		
		&.blue {
			background-color: blue;
		}
	}
}
复制代码

引用该组件:

<demo-component>
	我是外部嵌入的内容
</demo-component>
复制代码

此时,咱们将看到外部内容投射到了蓝色区域:

固然,若是你将橙色区域代码放在蓝色区域代码的后面,那么外部内容就会投射到橙色区域:

因此从上面的示例咱们能够看出,若是同时存在简单的 ,那么外部内容将投射在组件模板最后的那个 中。

那么知道这个问题,咱们可能会想,能不能将外部内容有针对性的投射相应的 中呢?答案显然是能够的。

为了处理这个问题, 支持一个 select 属性,可让你在特定的地方投射具体的内容。该属性支持 CSS 选择器(标签选择器、类选择器、属性选择器、...)来匹配你想要的内容。若是 ng-content 上没有设置 select 属性,它将接收所有内容,或接收不匹配任何其余 ng-content 元素的内容。

直接看例子,修改后的 demo.component.html 和 demo.component.scss 以下:

demo.component.html:

<div class="demo">
	<h2>
		demo-component - 可嵌入外部内容的组件
	</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
	<div class="content blue">
		<ng-content select="header"></ng-content>
	</div>
	<div class="content red">
		<ng-content select=".demo2"></ng-content>
	</div>
	<div class="content green">
		<ng-content select="[name=demo3]"></ng-content>
	</div>
</div>
复制代码

demo.component.scss:

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}

	.content {
		padding: 10px;
		margin-top: 10px;
		line-height: 20px;
		color: #FFFFFF;
		background-color: #de7d28;

		&.blue {
			background-color: blue;
		}

		&.red {
			background-color: red;
		}

		&.green {
			background-color: green;
		}
	}
}
复制代码

从上面代码能够看到,蓝色区域将接收 标签 header 那部份内容,红色区域将接收 class为"demo2"的div 的那部份内容,绿色区域将接收 属性name为"demo3"的div 的那部份内容,橙色区域将接收其他的外部内容(开始,我是外部嵌入的内容,结束)。

引用该组件:

<demo-component>
	开始,我是外部嵌入的内容,
	<header>
		我是外部嵌入的内容,我在header中
	</header>
	<div class="demo2">
		我是外部嵌入的内容,我所在div的class为"demo2"
	</div>
	<div name="demo3">
		我是外部嵌入的内容demo,我所在div的属性name为"demo3"
	</div>
	结束
</demo-component>
复制代码

此时,咱们将看到外部内容投射到了指定的 中。

扩展知识

ngProjectAs

如今咱们知道经过 ng-content 的 select 属性能够指定外部内容投射到指定的 中。

而要能正确的根据 select 属性投射内容,有个限制就是 - 不论是 标签 headerclass为"demo2"的div仍是 属性name为"demo3"的div,这几个标签都是做为 组件标签 的直接子节点

那若是不是做为直接子节点,会是什么状况呢?咱们简单修改下引用 demo-component 组件的代码,将 标签header 放在一个div中,修改以下:

<demo-component>
	开始,我是外部嵌入的内容,
	<div>
		<header>
			我是外部嵌入的内容,我在header中
		</header>
	</div>
	<div class="demo2">
		我是外部嵌入的内容,我所在div的class为"demo2"
	</div>
	<div name="demo3">
		我是外部嵌入的内容demo,我所在div的属性name为"demo3"
	</div>
	结束
</demo-component>
复制代码

此时,咱们看到 标签 header 那部份内容再也不投射到蓝色区域中了,而是投射到橙色区域中了。缘由就是 <ng-content select="header"></ng-content> 没法匹配到以前的 标签 header,故而将这部份内容投射到了橙色区域的 <ng-content></ng-content> 中了。

为了解决这个问题,咱们必须使用 ngProjectAs 属性,它能够应用于任何元素上。具体以下:

<demo-component>
	开始,我是外部嵌入的内容,
	<div ngProjectAs="header">
		<header>
			我是外部嵌入的内容,我在header中
		</header>
	</div>
	<div class="demo2">
		我是外部嵌入的内容,我所在div的class为"demo2"
	</div>
	<div name="demo3">
		我是外部嵌入的内容demo,我所在div的属性name为"demo3"
	</div>
	结束
</demo-component>
复制代码

经过设置 ngProjectAs 属性,让 标签header 所在的 div 指向了 select="header",此时 标签 header 那部份内容有投射到蓝色区域了:

<ng-content> 不“产生”内容

作个试验

作个试验,先定义一个 demo-child-component 组件:

import { Component, OnInit } from '@angular/core';

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

	constructor() {
	}

	ngOnInit() {
	    console.log('demo-child-component初始化完成!');
	}
}
复制代码

demo-component 组件修改成:

import { Component, OnInit } from '@angular/core';

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

	constructor() {
	}

	ngOnInit() {
	}
}
复制代码

而后在 demo-component 中 投射 demo-child-component:

<demo-component>
	<demo-child-component></demo-child-component>
</demo-component>
复制代码

此时,在控制台咱们看到打印出 demo-child-component初始化完成! 这些文字。可是当咱们点击按钮进行切换操做时,demo-child-component初始化完成! 就再也不打印了,这意味着咱们的 demo-child-component 组件只被实例化了一次 - 从未被销毁和从新建立。

为何会出现这样的状况呢?

出现缘由

<ng-content> 不会 "产生" 内容,它只是投影现有的内容。你能够认为它等价于 node.appendChild(el) 或 jQuery 中的 $(node).append(el) 方法:使用这些方法,节点不被克隆,它被简单地移动到它的新位置。所以,投影内容的生命周期将被绑定到它被声明的地方,而不是显示在地方。

这也从原理解释了前面那个问题:若是同时存在几个 ,那外部内容将如何进行投射呢?

这种行为有两个缘由:指望一致性和性能。什么 "指望的一致性" 意味着做为开发人员,能够基于应用程序的代码,猜想其行为。假设我写了如下代码:

<demo-component>
	<demo-child-component></demo-child-component>
</demo-component>
复制代码

很显然 demo-child-component 组件将被实例化一次,但如今假如咱们使用第三方库的组件:

<third-party-wrapper>
    <demo-child-component></demo-child-component>
</third-party-wrapper>
复制代码

若是第三方库可以控制 demo-child-component 组件的生命周期,我将没法知道它被实例化了多少次。其中惟一方法就是查看第三方库的代码,了解它们的内部处理逻辑。将组件的生命周期被绑定到咱们的应用程序组件而不是包装器的意义是,开发者能够掌控计数器只被实例化一次,而不用了解第三方库的内部代码。

性能的缘由 更为重要。由于 ng-content 只是移动元素,因此能够在编译时完成,而不是在运行时,这大大减小了实际应用程序的工做量。

解决方法

为了让组件可以控制投射进来的子组件的实例化,咱们能够经过两种方式完成:在咱们的内容周围使用 <ng-template> 元素及 ngTemplateOutlet,或者使用带有 "*" 语法的结构指令。为简单起见,咱们将在示例中使用 <ng-template> 语法。

demo-component 组件修改成:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component',
	template: ` <button (click)="show = !show"> {{ show ? 'Hide' : 'Show' }} </button> <div class="content" *ngIf="show"> <ng-container [ngTemplateOutlet]="template"></ng-container> </div> `
})
export class DemoComponent implements OnInit {
    @ContentChild(TemplateRef) template: TemplateRef;
    show = true;

	constructor() {
	}

	ngOnInit() {
	}
}
复制代码

而后咱们将 demo-child-component 包含在 ng-template 中:

<demo-component>
    <ng-template>
        <demo-child-component></demo-child-component>
    </ng-template>
</demo-component>
复制代码

此时,咱们在点击按钮进行切换操做时,控制台都会打印出 demo-child-component初始化完成! 这些文字。

参考资源

ng-content: The hidden docs


转载请注明出处,谢谢!

相关文章
相关标签/搜索