Angular开发实践(八): 使用ng-content进行组件内容投射
在Angular中,组件属于特殊的指令,它的特殊之处在于它有自己的模板(html)和样式(css)。因此使用组件可以使我们的代码具有强解耦、可复用、易扩展等特性。通常的组件定义如下:
demo.component.ts:
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:
<div class="demo">
<h2>
demo-component - 我是一个简单的组件
</h2>
</div>
demo.component.scss:
.demo {
padding: 10px;
border: 2px solid red;
h2 {
margin: 0;
color: #262626;
}
}
此时我们引用该组件,就会呈现该组件解析之后的内容:
<demo-component></demo-component>
假设现在有这样的需求,这个组件能够接受外部投射进来的内容,也就是说组件最终呈现的内容不仅仅是本身定义的那些,那该怎么做呢?这时就要请出本文的主角 ng-content。
简单投射
我们先从最简单开始,在 demo.component.html 中添加 ,修改后的 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>
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;
}
}
为了效果展示特意将 所在的容器背景色定义为橙色。
这时我们在引用该组件时可以从外部投射内容,外部内容将在橙色区域显示:
<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 属性投射内容,有个限制就是 - 不管是 标签 header、class为"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初始化完成! 这些文字。
参考资源
Angular开发实践(八): 使用ng-content进行组件内容投射的更多相关文章
- Angular开发实践(一):环境准备及框架搭建
引言 在工作中引入Angular框架将近一年了,在这一年中不断的踩坑和填坑,当然也学习和积累了很多的知识,包括MVVM框架.前后端分离.前端工程化.SPA优化等等.因此想通过Angular开发实践这系 ...
- Angular开发实践(七): 跨平台操作DOM及渲染器Renderer2
在<Angular开发实践(六):服务端渲染>这篇文章的最后,我们也提到了在服务端渲染中需要牢记的几件事件,其中就包括不要使用window. document. navigator等浏览器 ...
- [转]Angular开发(十八)-路由的基本认识
angular router https://angular.io/guide/router 本文转自:https://blog.csdn.net/kuangshp128/article/detail ...
- Angular开发实践(三):剖析Angular Component
Web Component 在介绍Angular Component之前,我们先简单了解下W3C Web Components 定义 W3C为统一组件化标准方式,提出Web Component的标准. ...
- Angular开发实践(四):组件之间的交互
在Angular应用开发中,组件可以说是随处可见的.本篇文章将介绍几种常见的组件通讯场景,也就是让两个或多个组件之间交互的方法. 根据数据的传递方向,分为父组件向子组件传递.子组件向父组件传递及通过服 ...
- Angular开发实践(六):服务端渲染
Angular Universal Angular在服务端渲染方面提供一套前后端同构解决方案,它就是 Angular Universal(统一平台),一项在服务端运行 Angular 应用的技术. 标 ...
- Angular开发实践(五):深入解析变化监测
什么是变化监测 在使用 Angular 进行开发中,我们常用到 Angular 中的绑定--模型到视图的输入绑定.视图到模型的输出绑定以及视图与模型的双向绑定.而这些绑定的值之所以能在视图与模型之间保 ...
- ionic,Angular 开发实践
1.实践参考 http://www.jianshu.com/p/ea0dcf1d31c9 原文思路搭建 2. 环境搭建步骤 : a. 安装node b.安装 cordova sudo n ...
- Angular开发实践(二):HRM运行机制
引言 在angular-start项目中启用了模块热替换(HMR - Hot Module Replacement)功能,关于如何在angular-cli启用HRM,请查看HRM配置 那HMR是个什么 ...
随机推荐
- [BZOJ1878] [SDOI2009] HH的项链 (树状数组)
Description HH有一串由各种漂亮的贝壳组成的项链.HH相信不同的贝壳会带来好运,所以每次散步 完后,他都会随意取出一段贝壳,思考它们所表达的含义.HH不断地收集新的贝壳,因此, 他的项链变 ...
- DIY福音:Firefox菜单及右键菜单ID大全
每一个折腾Firefox的Diyer都是上辈子折翼的天使,致自己! 打磨Firefox界面的时候最多的就隐藏一些平常根本用不上的一些菜单,常规的做法就是安装DOM Inspector扩展右键查找大法寻 ...
- 关于JDK和eclipse的安装和汉化
参考网址:http://jingyan.baidu.com/article/f96699bb8b38e0894e3c1bef.html http://titanseason.iteye.com/blo ...
- 使用async和wait进行异步编程
本文来源于博客园-钱智慧,转载请注明出处 代码示例 // 要让一个方法成为异步方法: // - async修饰符. // - 返回类型是 Task 或者 Task<T>. 具体来说,如果函 ...
- es6 Generator生成器函数
生成器函数使用function*声明. 在生成器函数内部,有一种类似return的语法:关键字yield.二者的区别是,普通函数只可以return一次,而生成器函数可以yield多次(当然也可以只yi ...
- curl报35错误码
一.curl常见schannel错误 schannel: SNI or certificate check failed: SEC_E_WRONG_PRINCIPAL<0x80090322> ...
- 笔记:MyBatis Mapper XML文件详解 - 映射和参数
MyBatis 的真正强大在于它的映射语句,也是它的魔力所在.由于它的异常强大,映射器的 XML 文件就显得相对简单.如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% ...
- 使用jQuery动态克隆表格,并且添加至div中(使用前需要引入jQuery)
<!DOCTYPE html> <html> <head> <title></title> <meta charset="u ...
- WebPack的安装
一.前提因为webpack是一个基于node的项目,所以首先需要确保你的电脑里面已经安装了node.js,以及npm.在这里我使用的版本是:node:v5.8.0 ,npm:3.7.3,若是版本问题, ...
- Java集合:HashMap源码剖析
一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap ...