Angular中如何通过ng-content将带*customCellTemplate指令的内容投影到嵌套的inner-table组件?
问题本质
先帮你理清楚为什么直接用<ng-content>不行:当你在app.component里写<ng-container *customCellTemplate="...">时,Angular编译器会先把这个带*的结构型指令「脱糖」成带customCellTemplate指令的<ng-template>,而且这个指令的解析和绑定是在**内容声明的上下文(也就是app.component)**中完成的,不是在最终投影到的inner-table上下文里。
当你用<ng-content>把这个内容投影到inner-table时,inner-table看到的已经是一个被解析后的模板,而不是带有它需要的customCellTemplate指令的原始结构,自然无法识别并应用到对应列上。
下面给你几个可落地的解决方案,按推荐优先级排序:
方案1:收集父组件传入的模板,在inner-table中重新渲染(推荐)
这个方案完全保留用户使用inner-table的原有写法,内部逻辑透明。核心是在app-table中收集所有父组件传入的带customCellTemplate指令的模板,然后在inner-table内部重新渲染这些模板,让指令在inner-table的上下文中生效。
步骤1:在app-table中收集模板
首先需要导入inner-table的customCellTemplate指令(假设它的类名为CustomCellTemplateDirective),然后用@ContentChildren查询所有相关模板:
// app-table.component.ts import { Component, Input, ContentChildren, QueryList, AfterContentInit } from '@angular/core'; import { CustomCellTemplateDirective } from 'path-to-inner-table/custom-cell-template.directive'; // 替换为实际路径 @Component({ selector: 'app-table', templateUrl: './app-table.component.html' }) export class AppTableComponent implements AfterContentInit { @Input() redefinedColumns!: any; // 收集所有父组件传入的带customCellTemplate指令的模板 @ContentChildren(CustomCellTemplateDirective, { descendants: true }) private cellTemplates!: QueryList<CustomCellTemplateDirective>; // 转成数组方便模板遍历 public cellTemplateList!: CustomCellTemplateDirective[]; ngAfterContentInit(): void { this.cellTemplateList = this.cellTemplates.toArray(); } }
步骤2:在app-table模板中传递模板给inner-table
在app-table.component.html中,遍历收集到的模板,在inner-table内部重新应用customCellTemplate指令:
<!-- app-table.component.html --> <div id="app-table-container"> <inner-table [columns]="redefinedColumns"> <!-- 遍历所有收集到的模板,让指令在inner-table上下文生效 --> <ng-container *ngFor="let template of cellTemplateList"> <ng-container *customCellTemplate="let item; name: template.name" > <!-- 渲染用户定义的模板内容 --> <ng-container [ngTemplateOutlet]="template.templateRef" [ngTemplateOutletContext]="{ $implicit: item }" ></ng-container> </ng-container> </ng-container> </inner-table> </div>
效果
用户可以完全按照原来用inner-table的写法使用app-table,不需要任何改变:
<!-- app.component.html --> <app-table [redefinedColumns]="columns"> <ng-container *customCellTemplate="let item; name: 'firstnameTemplate'"> {{ item.firstname + ' !' }} </ng-container> </app-table>
方案2:自定义中转指令(适合无法访问inner-table内部指令的场景)
如果inner-table的customCellTemplate是私有指令,你无法直接导入它,那可以自己写一个中转指令,让用户使用这个自定义指令,然后在app-table中转发到inner-table的指令。
步骤1:创建中转指令
// app-custom-cell-template.directive.ts import { Directive, Input, TemplateRef } from '@angular/core'; @Directive({ selector: '[appCustomCellTemplate]' }) export class AppCustomCellTemplateDirective { // 对应原指令的name参数 @Input() appCustomCellTemplateName!: string; // 对应原指令的let-item上下文 @Input() appCustomCellTemplateLetItem!: any; constructor(public templateRef: TemplateRef<any>) {} }
步骤2:在app-table中收集中转指令的模板
// app-table.component.ts import { Component, Input, ContentChildren, QueryList, AfterContentInit } from '@angular/core'; import { AppCustomCellTemplateDirective } from './app-custom-cell-template.directive'; @Component({ selector: 'app-table', templateUrl: './app-table.component.html' }) export class AppTableComponent implements AfterContentInit { @Input() redefinedColumns!: any; @ContentChildren(AppCustomCellTemplateDirective) private appCellTemplates!: QueryList<AppCustomCellTemplateDirective>; public appCellTemplateList!: AppCustomCellTemplateDirective[]; ngAfterContentInit(): void { this.appCellTemplateList = this.appCellTemplates.toArray(); } }
步骤3:在app-table模板中转发到inner-table指令
<!-- app-table.component.html --> <div id="app-table-container"> <inner-table [columns]="redefinedColumns"> <ng-container *ngFor="let template of appCellTemplateList"> <ng-container *customCellTemplate="let item; name: template.appCustomCellTemplateName" > <ng-container [ngTemplateOutlet]="template.templateRef" [ngTemplateOutletContext]="{ $implicit: item }" ></ng-container> </ng-container> </ng-container> </inner-table> </div>
效果
用户只需要把原来的*customCellTemplate改成*appCustomCellTemplate即可:
<!-- app.component.html --> <app-table [redefinedColumns]="columns"> <ng-container *appCustomCellTemplate="let item; name: 'firstnameTemplate'"> {{ item.firstname + ' !' }} </ng-container> </app-table>
方案3:通过Input传递模板(适合简单场景)
如果你的场景比较简单,用户也接受改变写法,可以让用户把模板作为@Input传递给app-table,替代内容投影。
步骤1:在app-table中添加模板输入
// app-table.component.ts import { Component, Input, TemplateRef } from '@angular/core'; @Component({ selector: 'app-table', templateUrl: './app-table.component.html' }) export class AppTableComponent { @Input() redefinedColumns!: any; @Input() cellTemplates!: Record<string, TemplateRef<any>>; }
步骤2:在app-table模板中渲染模板
<!-- app-table.component.html --> <div id="app-table-container"> <inner-table [columns]="redefinedColumns"> <ng-container *ngFor="let [templateName, tpl] of cellTemplates | keyvalue"> <ng-container *customCellTemplate="let item; name: templateName"> <ng-container [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="{ $implicit: item }" ></ng-container> </ng-container> </ng-container> </inner-table> </div>
效果
用户在父组件中定义模板变量,然后传递给app-table:
<!-- app.component.html --> <app-table [redefinedColumns]="columns" [cellTemplates]="{ firstnameTemplate: firstnameTpl }" > <ng-template #firstnameTpl let-item> {{ item.firstname + ' !' }} </ng-template> </app-table>
方案总结
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 方案1 | 完全保留用户原有写法,内部逻辑透明 | 需要能访问inner-table的customCellTemplate指令 | 可以获取到inner-table内部指令的场景 |
| 方案2 | 不依赖inner-table的内部指令,用户写法改动小 | 用户需要替换指令前缀 | 无法访问inner-table内部指令的场景 |
| 方案3 | 逻辑简单,代码量少 | 和原inner-table写法差异大 | 场景简单,用户接受新写法的场景 |

