在Angular或丢失的ng-content文档中投影内容

在学习Angular时,他们经常会错过或没有对内容投影这样的概念给予足够的重视。这是用于创建灵活且可重复使用的组件的非常强大的工具。但是它的文档在Lifecycle hooks部分仅提到了几段让我们尝试解决此遗漏。



使用ng-content投影内容


内容投影是一种从组件外部导入HTML内容并将其粘贴到特定位置的组件模板中的方法。(文档的免费翻译)
定义非常复杂,但实际上,一切都简单得多。我们有某种组件,其开始和结束标记之间的所有内容都是内容。

<app-parent>
    <!-- content -->
    I'm content!
    <!-- content -->
</app-parent>

Angular允许您使用element将任何HTML代码(内容)嵌入到此组件的模板中ng-content

让我们尝试通过一个示例找出为什么需要这样做以及它是如何工作的。假设我们有一个简单的按钮组件。我们通过将该按钮的文本传递给模板input property

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: '<button>{{text}}</button>'
})
export class ButtonComponent {
  @Input() text: string;
}

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button [text]="'Button'"></app-button>`,
})
export class AppComponent {
}

看起来不错。但是突然之间,我们需要为某些按钮的文本添加一个图标。我们已经有一个图标组件。您只需要将其添加到按钮模板中,附加一个指令,ngIf然后input property为动态显示图标编写另一个指令即可

// icon.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent {
}

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <app-icon *ngIf="showIcon"></app-icon>
               {{text}}
             </button>`,
})
export class ButtonComponent {
  @Input() text: string;
  @Input() showIcon = true;
}

一切正常。但是,如果您需要更改图标相对于文本的位置怎么办?或添加另一个新项目?您必须编辑现有代码,添加新属性等。

使用可以避免所有这些情况ng-content可以将其视为内容占位符它显示您放置在组件的开始和结束标记之间的所有内容。

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <ng-content></ng-content>
             </button>`,
})
export class ButtonComponent {
}

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <app-icon></app-icon>
               Button
             </app-button>`,
})
export class AppComponent {
}

Stackblitz代码

现在,如果我们需要带有图标的按钮,则只需将图标组件放在按钮标签之间。您可以添加任何内容。那不是天堂吗?我们的按钮组件变得灵活漂亮。

select属性对ng-content扮演什么角色?


有时我们需要将某些内容安排在相对于其余内容的某个位置,在这种情况下,我们可以使用select接受选择器(.some-class, some-tag, [some-attr]的属性

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <ng-content></ng-content>
               <div>
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
}

Stackblitz代码

现在,无论其余内容如何,​​图标始终显示在下方。完美!

什么是ngProjectAs?


selecty 属性可ng-content处理位于父组件嵌套第一级的标签。但是,如果我们通过将其包装在标签中来增加图标组件的嵌套级别,会发生什么呢?

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container>
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

我们将看到它select根本不起作用,就好像它根本不存在一样。发生这种情况是因为它<ng-content select="...">仅在父级内容嵌套的第一层进行搜索。有一个属性可以解决此问题ngProjectAs它接受选择器并“掩盖”选择器整个DOM节点。

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container ngProjectAs="app-icon">
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

Stackblitz代码

案例* ngIf + ng-content


让我们研究另一个有趣的情况。假设我们需要单击“隐藏/显示”图标按钮。在负责显示图标的按钮组件类中添加一个布尔属性,通过单击按钮并将其挂起来对其进行更改ngIf

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button (click)="toggleIcon()">
               <ng-content></ng-content>
               <div *ngIf="showIcon">
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

该图标被隐藏/通过单击显示。精细!但是,让我们为挂钩OnInitOnDestroy图标组件添加一些日志一个众所周知的事实是,指令ngIf在条件发生变化时会完全删除/创建一个元素,并且OnDestroy/ OnInit必须每次都相应地工作。

// icon.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent implements OnInit, OnDestroy {
  ngOnInit() {
    console.log('app-icon init');
  }

  ngOnDestroy() {
    console.log('app-icon destroy')
  }
}

关于Stackblitz

代码我们几次单击按钮,确保图标消失然后出现。然后,我们进入开发人员的控制台,希望看到我们梦logs以求的日志,但是……事实并非如此!

创建组件只有一个日志。事实证明,我们的图标组件从不删除,而只是隐藏。为什么会这样呢?

ng-content 不会创建新内容,而只是投影现有内容。因此,声明内容的组件负责创建和删除。对我来说,那是完全不明显的时刻。让我们修复我们的解决方案,以使其最初能够按预期工作。

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
                <ng-content></ng-content>
                <ng-content select="app-icon"></ng-content>
             </button>`,
})
export class ButtonComponent {
}

// app.component.ts
import { Component } from "@angular/core";

@Component({
  selector: 'app-root',
  template: `<app-button (click)="toggleIcon()">
              <div *ngIf="showIcon" ngProjectAs="app-icon">
                <app-icon></app-icon>
              </div>
              Button
            </app-button>`,
})
export class AppComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

Stackblitz上的代码

打开日志后,我们可以看到该图标的组件已被创建和删除。

而不是结论


希望本文对Angular中的内容投影有所帮助。
我绝对不理解为什么官方文档忽略了这个话题。自2017年以来,Angular存储库甚至将此问题挂起了显然,Angular团队还有很多重要的事情要做。

All Articles