阅读(2540) (0)

Angular 内容投影

2022-06-28 10:39:01 更新

内容投影

本主题描述如何使用内容投影来创建灵活的可复用组件。

要查看或下载本主题中用到的示例代码,请参见现场演练 / 下载范例 。

内容投影是一种模式,你可以在其中插入或投影要在另一个组件中使用的内容。例如,你可能有一个 ​Card ​组件,它可以接受另一个组件提供的内容。

以下各节介绍了 Angular 中内容投影的常见实现,包括:

  • 单插槽内容投影。使用这种类型的内容投影,组件可以从单一来源接受内容。
  • 多插槽内容投影。在这种情况下,组件可以从多个来源接受内容。
  • 有条件的内容投影。使用条件内容投影的组件仅在满足特定条件时才渲染内容。

单插槽内容投影

内容投影的最基本形式是单插槽内容投影。单插槽内容投影是指创建一个组件,你可以在其中投影一个组件。

要创建使用单插槽内容投影的组件,请执行以下操作:

  1. 创建一个组件。
  2. 在组件模板中,添加 ​<ng-content>​ 元素,让你希望投影的内容出现在其中。

例如,以下组件使用 ​<ng-content>​ 元素来显示消息。

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

@Component({
  selector: 'app-zippy-basic',
  template: `
    <h2>Single-slot content projection</h2>
    <ng-content></ng-content>
  `
})
export class ZippyBasicComponent {}

有了 ​<ng-content>​ 元素,该组件的用户现在可以将自己的消息投影到该组件中。例如:

<app-zippy-basic>
  <p>Is content projection cool?</p>
</app-zippy-basic>

<ng-content>​ 元素是一个占位符,它不会创建真正的 DOM 元素。​<ng-content>​ 的那些自定义属性将被忽略。

多插槽内容投影

一个组件可以具有多个插槽。每个插槽可以指定一个 CSS 选择器,该选择器会决定将哪些内容放入该插槽。该模式称为多插槽内容投影。使用此模式,你必须指定希望投影内容出现在的位置。你可以通过使用 ​<ng-content>​ 的 ​select ​属性来完成此任务。

要创建使用多插槽内容投影的组件,请执行以下操作:

  1. 创建一个组件。
  2. 在组件模板中,添加 ​<ng-content>​ 元素,让你希望投影的内容出现在其中。
  3. 将 ​select ​属性添加到 ​<ng-content>​ 元素。 Angular 使用的选择器支持标签名、属性、CSS 类和​ :not​ 伪类的任意组合。
  4. 例如,以下组件会使用两个 ​<ng-content>​ 元素。

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-zippy-multislot',
      template: `
        <h2>Multi-slot content projection</h2>
    
        Default:
        <ng-content></ng-content>
    
        Question:
        <ng-content select="[question]"></ng-content>
      `
    })
    export class ZippyMultislotComponent {}

使用 ​question ​属性的内容将投影到带有 ​select=[question]​ 属性的 ​<ng-content>​ 元素。

<app-zippy-multislot>
  <p question>
    Is content projection cool?
  </p>
  <p>Let's learn about content projection!</p>
</app-zippy-multislot>
不带 SELECT 属性的 NG-CONTENT
如果你的组件包含不带 ​select ​属性的 ​<ng-content>​ 元素,则该实例将接收所有与其他 ​<ng-content>​ 元素都不匹配的投影组件。
在前面的示例中,只有第二个 ​<ng-content>​ 元素定义了 ​select ​属性。结果,第一个 ​<ng-content>​ 就会元素接收投影到组件中的任何其他内容。

有条件的内容投影

如果你的组件需要有条件地渲染内容或多次渲染内容,则应配置该组件以接受一个 ​<ng-template>​ 元素,其中包含要有条件渲染的内容。

在这种情况下,不建议使用 ​<ng-content>​ 元素,因为只要组件的使用者提供了内容,即使该组件从未定义 ​<ng-content>​ 元素或该 ​<ng-content>​ 元素位于 ​ngIf ​语句的内部,该内容也总会被初始化。

使用 ​<ng-template>​ 元素,你可以让组件根据你想要的任何条件显式渲染内容,并可以进行多次渲染。在显式渲染 ​<ng-template>​ 元素之前,Angular 不会初始化该元素的内容。

<ng-template>​ 进行条件内容投影的典型实现。

  1. 创建一个组件。
  2. 在接受 ​<ng-template>​ 元素的组件中,使用 ​<ng-container>​ 元素渲染该模板,例如:
  3. <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>

    本示例使用 ​ngTemplateOutlet ​指令来渲染给定的 ​<ng-template>​ 元素,你将在后续步骤中对其进行定义。你可以将 ​ngTemplateOutlet ​指令应用于任何类型的元素。本示例就将该指令分配给了 ​<ng-container>​ 元素,因为该组件不需要渲染真实的 DOM 元素。

  4. 将 ​<ng-container>​ 元素包装在另一个元素(例如 ​div ​元素)中,然后应用条件逻辑。
  5. <div *ngIf="expanded" [id]="contentId">
        <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
    </div>
  6. 在要投影内容的模板中,将投影的内容包装在 ​<ng-template>​ 元素中,例如:
  7. <ng-template appExampleZippyContent>
      It depends on what you do with it.
    </ng-template>

    这个 ​<ng-template>​ 元素定义了一个组件可以根据其自身逻辑渲染的内容块。组件可以使用 ​@ContentChild​ 或 ​@ContentChildren​ 装饰器获得对此模板内容的引用(即 ​TemplateRef​)。前面的示例创建了一个自定义指令 ​appExampleZippyContent ​作为 API,以将 ​<ng-template>​ 标记为组件内容。借助这个 ​TemplateRef​,组件可以使用 ​ngTemplateOutlet​指令或​ViewContainerRef.createEmbeddedView()​方法来渲染所引用的内容。

  8. 创建一个属性型指令,它具有与这个模板的自定义属性相匹配的选择器。在此指令中,注入 TemplateRef 实例。
  9. @Directive({
      selector: '[appExampleZippyContent]'
    })
    export class ZippyContentDirective {
      constructor(public templateRef: TemplateRef<unknown>) {}
    }

    在上一步中,你已添加了具有自定义属性 ​appExampleZippyDirective ​的 ​<ng-template>​ 元素。这段代码提供了当 Angular 遇到该自定义属性时要使用的逻辑。在这里,该逻辑指示 Angular 实例化这个模板引用。

  10. 在你要将内容投影到的组件中,使用 ​@ContentChild​ 获取此投影内容的模板。
  11. @ContentChild(ZippyContentDirective) content!: ZippyContentDirective;

    在执行此步骤之前,你的应用具有一个组件,它会在满足某些条件时实例化此模板。你还创建了一个指令,该指令能提供对该模板的引用。在最后一步中,​@ContentChild​ 装饰器指示 Angular 实例化指定组件中的模板。

    如果是多插槽内容投影,则可以使用 ​@ContentChildren​ 获取投影元素的查询列表(QueryList)。

在更复杂的环境中投影内容

如多插槽内容投影中所述,你通常会使用属性、元素、CSS 类或这三者的某种组合来标识将内容投影到何处。例如,在以下 HTML 模板中,p 标签会使用自定义属性 ​question ​将内容投影到 ​app-zippy-multislot​ 组件中。

<app-zippy-multislot>
  <p question>
    Is content projection cool?
  </p>
  <p>Let's learn about content projection!</p>
</app-zippy-multislot>

在某些情况下,你可能希望将内容投影为其他元素。例如,你要投影的内容可能是另一个元素的子元素。可以用 ​ngProjectAs ​属性来完成此操作。

例如,考虑以下 HTML 代码段:

<ng-container ngProjectAs="[question]">
  <p>Is content projection cool?</p>
</ng-container>

本示例使用 ​<ng-container>​ 属性来模拟将组件投影到更复杂的结构中。

注意!
<ng-container>​ 元素是一个逻辑结构,可用于对其他 DOM 元素进行分组;但是,​<ng-container>​ 本身未在 DOM 树中渲染。

在这个例子中,我们要投影的内容位于另一个元素内。为了按预期方式投影此内容,此模板使用了 ​ngProjectAs ​属性。有了 ​ngProjectAs​,就可以用 ​[question]​ 选择器将整个 ​<ng-container>​ 元素投影到组件中。