ng-content 中隐藏的内容

阅读 Angular 6/RxJS 最新教程,请访问 前端修仙之路

若是你尝试在 Angular 中编写可重复使用的组件,则可能会接触到内容投射的概念。而后你发现了 <ng-content>,并找到了一些关于它的文章,进而实现了所需的功能。html

接下来咱们来经过一个简单的示例,一步步介绍 <ng-content> 所涉及的内容。前端

Simple example

在本文中咱们使用一个示例,来演示不一样的方式实现内容投影。因为许多问题与Angular 中的组件生命周期相关,所以咱们的主要组件将显示一个计数器,用于展现它已被实例化的次数:node

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

let instances = 0;

@Component({
  selector: 'counter',
  template: '<h1>{{this.id}}</h1>'
})
class Counter {
  id: number;
  
  constructor() {
    this.id = ++instances;
  }
}

上面示例中咱们定义了 Counter 组件,组件类中的 id 属性用于显示本组件被实例化的次数。接着咱们继续定义一个 Wrapper 组件:typescript

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

@Component({
  selector: 'wrapper',
  template: `
    <div class="box">
      <ng-content></ng-content>
    </div>
  `
})
class Wrapper {}

如今咱们来验证一下效果:app

<wrapper>
  <counter></counter>
  <counter></counter>
  <counter></counter>
</wrapper>

Targeted projection

有时你但愿将包装器的不一样子项投影到模板的不一样部分。为了处理这个问题,<ng-content> 支持一个 select 属性,可让你在特定的地方投射具体的内容。该属性支持 CSS 选择器(my-element,.my-class,[my-attribute],...)来匹配你想要的内容。若是 ng-content 上没有设置 select 属性,它将接收所有内容,或接收不匹配任何其余 ng-content 元素的内容。长话短说:ide

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

@Component({
  selector: 'wrapper',
  template: `
  <div class="box red">
    <ng-content></ng-content>
  </div>
  <div class="box blue">
    <ng-content select="counter"></ng-content>
  </div>
  `,
  styles: [`
    .red {background: red;}
    .blue {background: blue;}
  `]
})
export class Wrapper { }

上面示例中,咱们引入了 select 属性,来选择投射的内容:性能

<wrapper>
  <span>This is not a counter</span>
  <counter></counter>
</wrapper>

上述代码成功运行后,counter 组件被正确投影到第二个蓝色框中,而 span 元素最终会在所有红色框中。请注意,目标 ng-content 会优先于 catch-all,即便它在模板中的位置靠后。测试

ngProjectAs

有时你的内部组件会被隐藏在另外一个更大的组件中。有时你只须要将其包装在额外的容器中便可应用 ngIfngSwitch。不管什么缘由,一般状况下,你的内部组件不是包装器的直接子节点。为了演示上述状况,咱们将 Counter 组件包装在一个 <ng-container> 中,看看咱们的目标投影会发生什么:this

<wrapper>
  <ng-container>
    <counter></counter>
  </ng-container>
</wrapper>

如今咱们的 couter 组件会被投影到第一个红色框中。由于 ng-container 容器再也不匹配 select="counter"。为了解决这个问题,咱们必须使用 ngProjectAs 属性,它能够应用于任何元素上。具体以下:spa

<wrapper>
  <ng-container ngProjectAs="counter">
    <counter></counter>
  </ng-container>
</wrapper>

经过设置 ngProjectAs 属性,终于让咱们的 counter 组件重回蓝色框的怀抱了。

Time to poke and prod

咱们从一个简单的实验开始:将两个 <ng-content> 块放在咱们的模板中,没有选择器。会出现什么状况?

页面中会显示一个或两个框,若是咱们包含两个框,它们的内容是显示 1 和 1 或 1 和 2?

<div class="box red">
    <ng-content></ng-content>
</div>
<div class="box blue">
    <ng-content></ng-content>
</div>

答案是咱们在最后一个 <ng-content> 中获得一个计数器,另外一个是空的!在咱们尝试解释为何以前,让咱们再来验证一个问题,即在 ng-content 指令的外层容器中添加 ngIf 指令:

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

@Component({
  selector: 'wrapper',
  template: `
    <button (click)="show = !show">
      {{ show ? 'Hide' : 'Show' }}
    </button>
    <div class="box" *ngIf="show">
      <ng-content></ng-content>
    </div>
  `
})
class Wrapper {
  show = true;
}

乍一看,彷佛正常运行。可是若是你经过按钮进行切换操做,你会注意到计数器的值不会增长。这意味着咱们的计数器组件只被实例化了一次 - 从未被销毁和从新建立。难道这是 ngIf 指令产生的问题,让咱们测试一下 ngFor 指令,看看是否有一样的问题:

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

@Component({
  selector: 'wrapper',
  template: `
    <div class="box" *ngFor="let item of items">
      <ng-content></ng-content>
    </div>
  `
})
class Wrapper {
  items = [0, 0, 0];
}

以上代码运行后与咱们使用多个 <ng-content> 的效果是同样的,只会显示一个计数器!为何不按照咱们的预期运行?

The explanation

<ng-content> 不会 "产生" 内容,它只是投影现有的内容。你能够认为它等价于 node.appendChild(el) 或 jQuery 中的 $(node).append(el) 方法:使用这些方法,节点不被克隆,它被简单地移动到它的新位置。所以,投影内容的生命周期将被绑定到它被声明的地方,而不是显示在地方。

这种行为有两个缘由:指望一致性和性能。什么 "指望的一致性" 意味着做为开发人员,能够基于应用程序的代码,猜想其行为。假设我写了如下代码:

<div class="my-wrapper">
  <counter></counter>
</div>

很显然计数器将被实例化一次,但如今假如咱们使用第三方库的组件:

<third-party-wrapper>
  <counter></counter>
</third-party-wrapper>

若是第三方库可以控制 counter 组件的生命周期,我将没法知道它被实例化了多少次。其中惟一方法就是查看第三方库的代码,了解它们的内部处理逻辑。将组件的生命周期被绑定到咱们的应用程序组件而不是包装器的意义是,开发者能够掌控计数器只被实例化一次,而不用了解第三方库的内部代码。

性能的缘由更为重要。由于 ng-content 只是移动元素,因此能够在编译时完成,而不是在运行时,这大大减小了实际应用程序的工做量。

The solution

为了让包装器可以控制其子元素的实例化,咱们能够经过两种方式完成:在咱们的内容周围使用 <ng-template> 元素,或者使用带有 "*" 语法的结构指令。为简单起见,咱们将在示例中使用 <ng-template> 语法,咱们的新应用程序以下所示:

<wrapper>
  <ng-template>
    <counter></counter>
  </ng-template>
</wrapper>

包装器再也不使用 <ng-content>,由于它接收到一个模板。咱们须要使用 @ContentChild 访问模板,并使用ngTemplateOutlet 来显示它:

@Component({
  selector: 'wrapper',
  template: `
    <button (click)="show = !show">
      {{ show ? 'Hide' : 'Show' }}
    </button>
    <div class="box" *ngIf="show">
      <ng-container [ngTemplateOutlet]="template"></ng-container>
    </div>
  `
})
class Wrapper {
  show = true;
  @ContentChild(TemplateRef) template: TemplateRef;
}

如今咱们的 counter 组件,每当咱们隐藏并从新显示时都正确递增!让咱们再验证一下 *ngFor 指令:

@Component({
  selector: 'wrapper',
  template: `
    <div class="box" *ngFor="let item of items">
      <ng-container [ngTemplateOutlet]="template"></ng-container>
    </div>
  `
})
class Wrapper {
  items = [0, 0, 0];
  @ContentChild(TemplateRef) template: TemplateRef;
}

上面代码成功运行后,每一个盒子中有一个计数器,显示 1,2 和 3,这正是咱们以前预期的结果。

参考资源