我们知道,可以使用ng-content将内容注入到组件内部。但同样地,我们也可以用ngTemplateOutlet来实现这一点。最终,在大多数情况下,由于类型安全、简洁性以及ng-content在扩展性方面的问题,实际案例使我倾向于默认使用ngTemplateOutlet。
以card为例。假设我们需要支持自定义的card-title和card-body。
类型安全与简洁性
我用ng-content实现了card-title,而用ngTemplateOutlet实现了card-body。类型安全版本如下:
const CARD_TITLE_SELECTOR = "card-title";
@Component({
selector: CARD_TITLE_SELECTOR,
imports: [],
template: `<ng-content></ng-content> `,
styles: ``,
})
export class CardTitleComponent {}
@Component({
selector: "card",
imports: [NgTemplateOutlet],
template: `
<ng-content [select]="cardTitleSelector"></ng-content>
<div class="card-divider"></div>
<ng-container *ngTemplateOutlet="cardBody"></ng-container>
`,
styles: ``,
})
export class CardComponent {
@Input({ required: true }) cardBody!: TemplateRef<any>;
cardTitleSelector = CARD_TITLE_SELECTOR;
}
使用方式如下:
<card [cardBody]="cardBody">
<card-title>card title</card-title>
<ng-template #cardBody> card body </ng-template>
</card>
如你所见,它们确实很相似。
关于简洁性,ngTemplateOutlet版本更胜一筹,因为ng-content版本看起来稍微冗长一些,需要声明CardTitleComponent和CARD_TITLE_SELECTOR。
关于类型安全,ngTemplateOutlet版本也更优,因为如果未提供cardBody,编辑器会显示错误,因为它是一个必需的输入项。错误信息类似于:
必须指定来自CardComponent组件的必需输入'cardBody'
扩展性
条件内容投影
不建议在条件内容投影的情况下使用ng-content,这可以在Angular的主页上找到相关信息。
不应该有条件地包含带有@if, @for或@switch的
<ng-content>。即使该<ng-content>占位符被隐藏,Angular总是实例化并为渲染到<ng-content>占位符的内容创建DOM节点。有关组件内容的条件渲染,请参阅模板片段。
例如,给定以下代码:
<card [cardBody]="cardBody">
<card-title>card title</card-title>
<ng-template #cardBody> card body </ng-template>
</card>
@Component({
selector: CARD_TITLE_SELECTOR,
imports: [],
template: `<ng-content></ng-content> `,
styles: ``,
})
export class CardTitleComponent {
constructor() {
alert("card title");
}
}
@Component({
selector: "card",
imports: [NgTemplateOutlet, NgIf],
template: `
<ng-container *ngIf="false">
<ng-content [select]="cardTitleSelector"></ng-content>
</ng-container>
<div class="card-divider"></div>
<ng-container *ngTemplateOutlet="cardBody"></ng-container>
`,
styles: ``,
})
export class CardComponent {
@Input({ required: true }) cardBody!: TemplateRef<any>;
cardTitleSelector = CARD_TITLE_SELECTOR;
}
即使card组件中带有*ngIf="false"和cardTitleSelector,card-title组件仍然会被渲染,并且你会看到警告框。
在实际情况中,我们可能需要根据内部组件状态的变化来隐藏或显示某些东西。在这种情况下,尽管我们可以使其工作,但不推荐使用ng-content。
复杂情况下的上下文
对于与组件状态交互的情况,ngTemplateOutlet支持context,这对于复杂情况非常有用。例如,
@Component({
selector: "card",
imports: [NgTemplateOutlet, NgIf],
template: `
<ng-container *ngIf="false">
<ng-content [select]="cardTitleSelector"></ng-content>
</ng-container>
<div class="card-divider"></div>
<ng-container
*ngTemplateOutlet="cardBody; context: { isActive: false}"
></ng-container>
`,
styles: ``,
})
export class CardComponent {
@Input({ required: true }) cardBody!: TemplateRef<any>;
cardTitleSelector = CARD_TITLE_SELECTOR;
}
<card [cardBody]="cardBody">
<card-title>card title</card-title>
<ng-template #cardBody let-isActive="isActive">
card body is active? {{ isActive }}
</ng-template>
</card>
有时,context可以用Output事件替代,但有时候这样做并不容易。
实际上,有一个开放的问题作用域插槽(类似Vue),针对ng-content中的类似功能。
最后的话
不得不说,我认为ng-content更加直观,这也是我在使用Angular时的初始选择。我以为它与Vue中的Slots API相似(我之前使用过Vue)。
然而,上述原因使我倾向于在开发过程中使用ngTemplateOutlet。我不想首先实现直观的ng-content,然后有一天由于context或条件内容投影限制而切换到ngTemplateOutlet。或者添加一些非直观的技巧来使ng-content继续工作也不是我想要的。
实际上,对于这两个不同的API,Vue只提供了一个。我真的不知道Angular团队是怎么考虑的。