概述、意义及必要性
组件内容投影是组件通讯的一种高级用法。
有时,我们在创建组件的时候想要把组件内部的标记作为一个参数传给组件。这种技术就叫做内容投影(content projection)。在Angular中,内容投影通过<ng-content>标签来实现。
如果没有“内容投影”,有些事情我们就没法做了,典型的有两类:
- 组件标签不能嵌套使用。
- 不能优雅地包装原生的 HTML 标签。
比如你自己编写了两个组件 my-comp-1 和 my-comp-2,如果没有内容投影,这两个组件就没办法嵌套使用,比如你想这样用是不行的:
1 2 3
| <my-comp-1> <my-comp-2></my-comp-2> </my-comp-1>
|
如果没有“内容投影”机制,my-comp-1 无法感知到 my-comp-2 的存在,也无法和它进行交互。
基础用法
对于一个组件来说,有些区域的内容应设置为可变的,让调用者能把它所需的内容传进来。这时候“内容投影”机制就可以派上用场了,以上面的嵌套组件为例,我们可以这样来编写组件的模板:
my-comp-1.html
1 2 3 4 5 6 7 8 9
| <div class="panel panel-primary"> <div>这是my-comp-1的标题</div> <my-comp-2> <h3>这些是my-comp-1投影进来的内容</h3> <p class="my-class">利用了CSS选择器</p> <p>利用了标签选择器</p> </my-comp-2> <div>这是my-comp-1的底部</div> </div>
|
my-comp-2.html
1 2 3 4 5 6 7
| <div class="panel panel-primary"> <div>这是my-comp-2的标题</div> <ng-content select="h3"></ng-content> <ng-content select=".my-class"></ng-content> <ng-content select="p"></ng-content> <div>这是my-comp-2的底部</div> </div>
|
运行起来的效果应该是这样的:
这是my-comp-1的标题
这是my-comp-2的标题
这些是my-comp-1投影进来的内容
利用了CSS选择器
利用了标签选择器
这是my-comp-2的底部
这是my-comp-1的底部
注意以上模板里面的 <ng-content></ng-content>,你看可以把它想象成一个占位符,我们用它来先占住一块空间,等使用方把参数传递进来之后,再用真实的内容来替换它。
而 <ng-content></ng-content> 里面的那个 select 参数,其作用和 CSS 选择器非常类似。
投影一个复杂的组件
如果在主组件想把把自己编写的一个组件投影到另一个组件进去,那又应该怎么办呢?
my-comp-1.html
1 2 3 4 5 6 7
| <div class="panel panel-primary"> <div>组件一标题</div> <my-comp-2> <my-comp-3 (sayhello)="doSomething()"></my-comp-3> </my-comp-2> <div>组件一底部</div> </div>
|
my-comp-2.html
1 2 3 4 5
| <div class="panel panel-primary"> <div>组件二标题</div> <ng-content select="my-comp-3"></ng-content> <div>组件二底部</div> </div>
|
my-comp-3.html
1 2 3 4
| <div class="panel panel-primary"> <div>组件三</div> <button (click)="sayHello()">sayhello</button> </div>
|
运行起来的效果应该是这样的:
组件一标题
组件二标题
组件三
sayhello按钮
组件二底部
组件一底部
同时,对于被投影的组件三来说,我们同样可以利用小圆括号的方式来进行事件绑定:
my-comp-3.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { Component, OnInit,Output,EventEmitter } from '@angular/core';
@Component({ }) export class ChildThreeComponent implements OnInit { @Output() public sayhello:EventEmitter<any>=new EventEmitter<any>();
constructor() { }
ngOnInit() { }
public sayHello():void{ this.sayhello.emit("sayhello"); } }
|
然后在 my-comp-1.ts 中自定义dosomething()函数,例如弹一个窗。点击组件三的按钮,就会发射一个 sayhello 事件,组件一收到后就会去执行弹窗。
@ContentChild 和 @ContentChildren
以上的用法都是在模板中直接操控,那如何在组件内部获得被投影组件的实例及接口呢?
例如在一个组件中,使用了 child-one 组件,并投影进去若干个 child-two 组件
1 2 3 4 5
| <child-one> <child-two></child-two> <child-two></child-two> <child-two></child-two> </child-one>
|
child-one 组件想要获取到 child-two ,可以利用 @ContentChild 这个装饰器。
代码示例:
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
| import { Component, ContentChild, ContentChildren, ElementRef, OnInit, QueryList } from '@angular/core';
import { ChildTwoComponent } from '../child-two/child-two.component';
@Component({ selector: 'child-one', templateUrl: './child-one.component.html', styleUrls: ['./child-one.component.scss'] }) export class ChildOneComponent implements OnInit {
@ContentChild(ChildTwoComponent) childTwo:ChildTwoComponent; @ContentChildren(ChildTwoComponent) childrenTwo:QueryList<ChildTwoComponent>; constructor() { }
ngOnInit() { }
ngAfterContentInit():void{ console.log(this.childTwo); console.log(this.childrenTwo); this.childrenTwo.forEach((item)=>{ console.log(item); }); } }
|

根据以上的 console 结果,@ContentChild 获取到的是第一个 child-one 组件,而 @ContentChildren 获取到了完整的可操作列表,遍历它可对每个组件进行操作。