ngAfterViewInit调用createEmbeddedView做内容投影到子组件不显示如何解决
问题根源分析
ExpressionChangedAfterItHasBeenCheckedError报错原因:
ngAfterViewInit是父组件视图完成首轮变更检测后才触发的生命周期钩子,此时直接调用createEmbeddedView修改了视图内容,和Angular刚完成的变更检测结果不一致,就会抛出该错误。你用的setTimeout是将操作推到下一个宏任务执行,绕开了当前轮变更检测,但确实不符合最佳实践。- 被
ngIf包裹的投影插槽初始为true时不显示的原因:
当子组件的ngIf初始值为true时,子组件会在自己的初始化阶段就完成ng-content插槽的内容收集与渲染,此时父组件还未执行到ngAfterViewInit,插入点还是空的,所以渲染出来的插槽没有内容。当ngIf从false切换为true时,父组件已经完成了模板插入操作,插槽重新渲染时就能拿到已经生成的内容。
修复方案
方案1:最小改动修复现有实现
无需setTimeout,手动触发变更检测即可解决报错,同时调整子组件插槽渲染时机解决不显示问题:
- 父组件注入
ChangeDetectorRef,插入模板后手动触发变更检测:
import { ChangeDetectorRef } from '@angular/core'; ... constructor(private cdr: ChangeDetectorRef) {} ngAfterViewInit() { [ "insertedElement3", "insertedElement1", "insertedElement2" ].forEach(element => { this.insertionPoint.createEmbeddedView(this[element]); }); // 手动触发一轮变更检测,同步新插入的视图内容,避免报错 this.cdr.detectChanges(); }
- 给子组件包裹
ng-content的ngIf绑定一个父组件控制的变量,初始设为false,等父组件插入完成后再设为true:
父组件ts新增控制变量:
showChildSlot = false; ngAfterViewInit() { // ... 原有插入模板的逻辑 this.cdr.detectChanges(); this.showChildSlot = true; }
父组件html传参:
<child-component [showSlot]="showChildSlot"> <span #insertionPoint></span> </child-component>
子组件内部用输入属性控制ngIf:
<div *ngIf="showSlot"> <ng-content></ng-content> </div>
方案2:更优雅的解耦实现(推荐)
避免跨组件操作投影的插入点,直接将需要渲染的模板列表作为输入传给子组件,由子组件自行渲染:
- 父组件传入排序后的模板数组:
parent.component.ts:
childTemplates: TemplateRef<any>[] = []; ngAfterViewInit() { // 按动态顺序组装模板数组 this.childTemplates = [ this.insertedElement3, this.insertedElement1, this.insertedElement2 ]; this.cdr.detectChanges(); }
parent.component.html:
<ng-template #insertedElement1>...</ng-template> <ng-template #insertedElement2>...</ng-template> <ng-template #insertedElement3>...</ng-template> <child-component [templates]="childTemplates"></child-component>
- 子组件内部接收模板数组,自行渲染:
child.component.ts:
import { Component, Input, ViewChild, ViewContainerRef, TemplateRef } from '@angular/core'; @Component({ selector: 'child-component', template: ` <div *ngIf="showSlot"> <ng-container #container></ng-container> </div> ` }) export class ChildComponent { @Input() templates: TemplateRef<any>[] = []; @Input() showSlot = true; @ViewChild('container', { static: false, read: ViewContainerRef }) container: ViewContainerRef; ngOnChanges(changes: any) { // 模板数组更新时重新渲染 if (changes.templates && this.container && this.templates.length) { this.container.clear(); this.templates.forEach(tpl => { this.container.createEmbeddedView(tpl); }); } } }
这种实现完全避开了投影生命周期不同步的问题,后续调整模板顺序或者新增模板都不需要修改子组件逻辑,可维护性更高。
内容的提问来源于stack exchange,提问作者MattTreichel
相关产品推荐
相关产品推荐

