如何为含/不含结构指令的投影项分组添加包裹元素?
问题场景
我正在封装Bootstrap网格布局的Angular独立组件,支持动态、静态混合的投影内容(可包含字符串、组件、原生HTML元素),期望的使用方式如下:
<app-layout [columns]="2"> <app-layout-item *ngFor="let item of dynamicItems"> {{ item }} </app-layout-item> <app-layout-item> Some text </app-layout-item> <app-layout-item> <button></button> </app-layout-item> </app-layout>
当前实现的组件能渲染内容,但当投影项混合带结构指令(如*ngFor)和不带结构指令的静态项时,会触发ExpressionChangedAfterItHasBeenCheckedError。经排查,带结构指令的app-layout-item的ngOnViewInit先触发,静态项的则后触发,导致静态项的template在app-layout完成渲染后才更新,引发变更检测错误。
解决方案
方案1:静态获取TemplateRef(最简便)
问题核心是静态app-layout-item的TemplateRef在ngAfterViewInit阶段才被赋值,晚于app-layout的ngAfterContentInit执行时机。修改LayoutItemComponent的@ViewChild配置,设置static: true让组件在初始化阶段就获取模板引用:
@Component({ selector: 'app-layout-item', standalone: true, template: ` <ng-template> <ng-content></ng-content> </ng-template> `, styleUrl: './layout-item.component.css', }) export class LayoutItemComponent extends LifecycleLogger { // 设置static: true,在组件初始化时立即获取TemplateRef @ViewChild(TemplateRef, { static: true }) public template!: TemplateRef<void>; }
此方案能让所有app-layout-item的template在app-layout计算行数据前就完成赋值,彻底避免后续变更引发的错误。
方案2:异步更新行数据并手动触发变更检测
如果无法修改LayoutItemComponent的static属性,可在LayoutComponent中通过异步延迟更新行数据,确保所有子组件视图初始化完成后再更新,并手动触发变更检测:
import { ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-layout', standalone: true, imports: [CommonModule], template: ` <div *ngFor="let row of rows" class="row"> <div *ngFor="let item of row" class="col"> <ng-template [ngTemplateOutlet]="item.template"></ng-template> </div> </div> `, styleUrl: './layout.component.css', }) export class LayoutComponent { @Input({ required: true }) columns!: number; @ContentChildren(LayoutItemComponent) items!: QueryList<LayoutItemComponent>; rows!: LayoutItemComponent[][]; constructor(private cdr: ChangeDetectorRef) {} ngAfterContentInit() { this.updateRows(); // 监听投影项变化,动态更新行结构 this.items.changes.subscribe(() => this.updateRows()); } private updateRows() { // 异步执行,等待所有子组件视图初始化完成 Promise.resolve().then(() => { this.rows = chunk( this.items.toArray().filter(item => !!item.template), this.columns ); // 手动触发变更检测,同步视图 this.cdr.detectChanges(); }); } }
方案3:改为结构指令(彻底统一逻辑)
将app-layout-item改为结构指令,强制所有投影项通过结构指令处理,让Angular统一管理视图初始化顺序:
// 结构指令实现 import { Directive, TemplateRef } from '@angular/core'; @Directive({ selector: '[appLayoutItem]', standalone: true, }) export class LayoutItemDirective { constructor(public template: TemplateRef<void>) {} }
使用方式调整为:
<app-layout [columns]="2"> <ng-template appLayoutItem *ngFor="let item of dynamicItems"> {{ item }} </ng-template> <ng-template appLayoutItem> Some text </ng-template> <ng-template appLayoutItem> <button></button> </ng-template> </app-layout>
同时修改LayoutComponent的内容查询目标:
@Component({ // ... 其他配置不变 }) export class LayoutComponent { // ... 其他属性不变 @ContentChildren(LayoutItemDirective) items!: QueryList<LayoutItemDirective>; ngAfterContentInit() { this.rows = chunk(this.items?.toArray().filter(Boolean), this.columns); } }
此方案彻底消除了静态/动态项的生命周期差异问题,逻辑更统一。
内容的提问来源于stack exchange,提问作者Shlang

