持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
插槽内容投影ng-content
ng-content是一个用来插入外部或者动态内容的占位符,利用插槽ng-content内容投影可以实现父子组件通信,也就是可以把父组件的模板片段内容投影到子组件中,从而使组件模板更加灵活。简单点说,就是组件的一部分模板内容可以通过外部传进来。
单插槽内容投影
内容投影最基本的形式就是单插槽内容投影。
app-user.html:
<div class="container">
Hello?
<ng-content></ng-content>
</div>
父组件:
<app-user [options]="userInfo" [age]="age">
<h2>I am Tom!</h2>
</app-user>
也就是把app-user标签中的内容投影到了组件ng-content的位置上。ng-content 元素是一个占位符,它不会创建真正的 DOM 元素。ng-content 的那些自定义属性将被忽略。
多插槽内容投影
一个组件可以有多个插槽,每个插槽可以指定一个选择器,这个选择器会决定哪些内容放入该插槽,这种模式称为多插槽内容投影。使用此模式,我们必须指定希望投影内容出现在的位置。可以通过使用 ng-content 的 select 属性来完成此任务。
父组件:
<app-user #user [options]="userInfo">
<h2>I am Tom! - 1</h2>
<div name="container1">This is a DIV. - 2</div>
<div class="single-page">Single Page - 3</div>
<div id="childPage">Child Page - 4</div>
</app-user>
子组件:
<div class="container">
Hello?
<ng-content select="#childPage"></ng-content>
<ng-content select=".single-page"></ng-content>
<ng-content select="[name=container1]"></ng-content>
<ng-content></ng-content>
</div>
- 组件模板含有多个 ng-content 标签。
- 为了区分投影的内容可以投影到对应 ng-content 标签,需要使用 ng-content 标签上的 select 属性作为识别。
- select 属性支持标签名、属性、CSS 类和 :not 伪类的任意组合。
- 不添加 select 属性的 ng-content 标签将作为默认插槽。所有未匹配的投影内容都会投影在该 ng-content 的位置。
ngProjectAs
在有些情况下,我们需要使用ng-container把一些内容包裹起来传递到子组件中。
父组件:
<h2>Parent</h2>
<user-child>
<ng-container>
<header>
<h2>Header</h2>
</header>
</ng-container>
<footer>
<div>Footer</div>
</footer>
</user-child>
子组件:
<div style="border: 1px solid #24292e;width:200px;">
<h2>Child</h2>
<ng-content select="header"></ng-content>
<ng-content select="footer"></ng-content>
<ng-content></ng-content>
</div>
第一个ng-content并没有显示出来,是因为header被ng-container包裹住了,导致没有匹配到,所以匹配到了最后一个默认的ng-content。那怎么办呢?就需要ngProjectAs指令了。
父组件:
<ng-container ngProjectAs="header">
<header>
<h2>Header</h2>
</header>
</ng-container>
注意,如果在ng-container上使用选择器,是可以正常显示的哦。如下面的代码。
<ng-container name="header1">
<div>
<h2>Header1</h2>
</div>
</ng-container>
ng-template内容投影
如果你的子组件需要从父组件中获取渲染内容,并在响应的地方进行渲染,有两种情况:
- 利用ng-content来实现,但是ng-content中的内容均来自父组件;
- 利用ng-container来实现,父组件中将ng-template投影到子组件中,但是父组件中定义的template的数据来组子组件;
第一种情况,就是单,多插槽的情况,我们已经实现过了。
第二种情况比较特殊,因为在父组件中传递ng-template的时候,用到了子组件中的值,也就是,在父组件中用子组件中的值传递template。\
父组件:
<h2>Parent</h2>
<user-child>
<ng-template #nodeTemplate1 let-age="age">
<div style="border:1px solid aqua;">haha - {{age}}</div>
</ng-template>
</user-child>
子组件:
<h2>Child</h2>
<ng-container *ngTemplateOutlet="childRef; context: myContext"></ng-container>
<!-- <ng-container [ngTemplateOutlet]="childRef" [ngTemplateOutletContext]="myContext"></ng-container> -->
import { Component, ContentChild, OnInit, Query, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
import { MyTemplateDirective } from './Template.directive';
@Component({
selector: 'user-child',
templateUrl: 'UserChild.component.html'
})
export class UserChildComponent implements OnInit {
@ContentChild('nodeTemplate1', {static: true}) childRef!: TemplateRef<any>;
myContext: any = {
age: 22
};
constructor() { }
ngOnInit() {
}
}
ContentChild用于访问父组件传递过来的ng-template。
之前说过,父组件的ng-template中 let-age="age"语法,但是我在项目中看到过一些“奇怪”的写法。
<tree-root>
<ng-template let-node>
{{node}}
</ng-template>
</tree-root>
实际上,“context”有一些规则。
- 可空参数;
- context 是一个对象,这个对象可以包含一个 $implicit 的 key 作为默认值,使用时在模板 中用 let-key 语句进行绑定;
- context 的非默认字段需要使用 let-templateKey=contentKey 语句进行绑定;
改造一下代码:
父组件:
....
<ng-template #nodeTemplate1 let-name>
<div style="border:1px solid aqua;">haha - {{name}}</div>
</ng-template>
...
子组件:
import { Component, ContentChild, OnInit, Query, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
import { MyTemplateDirective } from './Template.directive';
@Component({
selector: 'user-child',
templateUrl: 'UserChild.component.html'
})
export class UserChildComponent implements OnInit {
myContext: any = {
$implicit: 666,
age: 22
};
@ContentChild('nodeTemplate1', {static: true}) childRef!: TemplateRef<any>;
}
关于内容投影大概就这些内容了,但是现在新的问题来?Vue和React中的插槽怎么使用,都有哪些类型的插槽?
Vue
Vue中的插槽slot应用也比较多,而且Vue2.0和Vue3.0插槽使用上也有些变化,下面的例子是Vue2.0的语法,Vue3.0是一样。Vue中的插槽有默认插槽,具名插槽,和作用域插槽,分别对应于Angular中的单插槽内容投影,多插槽内容投影,和ng-template内容插槽。
默认插槽
父组件Parent.Vue:
<template>
<div>
<p>我是Parent组件</p>
<Child>
<p>我是插槽的内容</p>
<!--也可以放组件-->
</Child>
</div>
</template>
<script>
import Child from './Child.vue';
export default {
name:'Parent',
components: {
Child
},
data(){
return {
}
}
}
</script>
子组件Child:
<template>
<div>
<p>我是Child组件</p>
<slot></slot>
</div>
</template>
<script>
export default {
name:'Child',
data(){
return {
}
}
}
</script>
slot使用起来是比较简单的,在子组件中使用slot组件预留了一个位置,,如果在父组件,使用其组件包裹内容(可以实模板代码,也可以是HTML,也可以是其他组件),则该内容就会被分发到处。
如果Child组件中没有包含,则组件包裹的内容会被抛弃掉。
默认插槽内容
有时候为一个插槽设置具体的默认内容也是很有用的,它只会在没有提供内容的的时候被渲染。
子组件:
<template>
<div>
<slot>
<p>我是插槽的默认内容</p>
</slot>
</div>
</template>
父组件:
<template>
<div>
<Child>
</Child>
</div>
</template>
具名插槽
具名插槽,就是起了个名字的插槽。有些情况下,我们需要多个插槽。用到了template,和Angular里的Angular有一点点像。
在向具名插槽提供内容的时候,我们可以在一个 template 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称。v-slot也可以缩写,直接写#也是可以的。 父组件:
<template>
<div>
<p>我是父组件</p>
<Child>
<template v-slot:header>
<p>我是header部分</p>
</template>
<p>我是main(默认插槽)部分</p>
<template #footer>
<p>我是footer部分</p>
</template>
</Child>
</div>
</template>
子组件:
<template>
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
作用域插槽
作用域插槽就是让插槽内容可以放问子组件中的数据。
子组件:
<template>
<div>
<p>我是Child组件</p>
<slot :obj="obj"></slot>
</div>
</template>
父组件:
<template>
<div class="main">
<p>我是父组件</p>
<B>
<template v-slot:default="data">
{{data.obj.lastName}}
</template>
</B>
</div>
</template>
React
React中的插槽就更加随意和简单了,有多种实现插槽的方式。
props实现
父组件:props传递JSX。
import React, { Component } from 'react';
import Child from "./Child"
class Parent extends Component {
render() {
return (
<div>
<Child slot1={<span>Slot 1</span>} slot2={<p>Slot 2</p>} />
</div>
);
}
}
export default Parent;
子组件:
import React, { Component } from 'react';
class Child extends Component {
render() {
console.log(this.props);
const { slot1, slot2} = this.props;
return (
<div className="tab">
<div className="tab-left">{slot1}</div>
<div className="tab-right">{slot2}</div>
</div>
);
}
}
export default Child;
children实现
也就是组件里的内容包裹想要传递的模板。
父组件:
import React, { Component } from 'react';
import Child from "./Child"
class Parent extends Component {
render() {
return (
<div>
<Child>
<span name="slot1">Slot 1</span>
<p name="slot2">Slot 2</p>
</Child>
</div>
);
}
}
export default Parent;
子组件:
import React, { Component } from 'react';
class Child extends Component {
render() {
console.log(this.props);
const [slot1, slot2] = this.props.children;
return (
<div className="tab">
<div className="tab-left">{slot1}</div>
<div className="tab-right">{slot2}</div>
</div>
);
}
}
export default Child;
props.children是一个数组,使用起来也很灵活。在父组件中可以给插槽内容设置一些attribute,在子组件中可以通过这些attribute来确定插槽的内容,这样即使哪天父组件中插槽内容的顺序发生了变化也不影响子组件。
renderProps
使用renderProps可以实现Vue中类似的作用域插槽。上面的代码都是用Class组件写的,下面的例子我会Function组件写吧。下面的例子是用children实现的,当然也可以通过属性来实现。
父组件:
function Parent(){
return (
<>
<Child>
{
v =>(<div>{v}</div>)
}
</Child>
</>
)
}
export default Parent;
子组件:
function Child(props){
return (
<>
{
props.children('传递给子组件的数据')
}
</>
)
}
至此,我们总结了Angular中单插槽内容投影,多插槽内容投影和ng-template内容投影的用法。还顺便了解了Vue,React中各自的实现方式。Angular使用起来稍微有点麻烦,Vue使用起来更加简单,React中则更为随意一点。