Angular中内容投影的应用总结

909 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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内容投影

如果你的子组件需要从父组件中获取渲染内容,并在响应的地方进行渲染,有两种情况:

  1. 利用ng-content来实现,但是ng-content中的内容均来自父组件;
  2. 利用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”有一些规则。

  1. 可空参数;
  2. context 是一个对象,这个对象可以包含一个 $implicit 的 key 作为默认值,使用时在模板 中用 let-key 语句进行绑定;
  3. 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中则更为随意一点。