在 Angular 结构指令中使用动态组件会产生额外的 HTML 标记。如何去除或更换它?

Using dynamic component within Angular structural directive produces extra HTML tag. How to remove or replace it?

我的项目中有相当复杂的基础设施,其中包含

简化版如下所示。

主机组件

@Component({
  selector: 'my-app',
  template: `
<table>
  <tr *myDir="let item of data">
    <td>{{item.text}}</td>
  </tr>
</table>`
})
export class AppComponent  {
  data = [ { text: 'item 1' }, { text: 'item 2' } ];
}

结构指令

import { MyComp } from './myComp';

@Directive({ selector: '[myDir][myDirOf]' })
export class MyDir implements OnInit {
  private data: any;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private resolver: ComponentFactoryResolver
  ) {
  }

  @Input() set myDirOf(data: any) {
    this.data = data;
  }

  ngOnInit() {
    const templateView = this.templateRef.createEmbeddedView({});
    const compFactory = this.resolver.resolveComponentFactory(MyComp);
    const componentRef = this.viewContainer.createComponent(
      compFactory, undefined, this.viewContainer.injector, [templateView.rootNodes]
    );
    componentRef.instance.data = this.data;
    componentRef.instance.template = this.templateRef;
  }
}

结构指令的组件

@Component({
  selector: '[my-comp]',
  template: `
<tr><td>custom td</td></tr>
<ng-template *ngFor="let item of data"
  [ngTemplateOutlet]="template"
  [ngTemplateOutletContext]="{ $implicit: item }"
></ng-template>`
})
export class MyComp {
  public template: TemplateRef<any>;
  public data: any;
}

输出为

custom td
item 1
item 2

除了标记

<table>
  <div my-comp>
    <tr><td>custom td</td></tr>
    <tr><td>item 1</td></tr>
    <tr><td>item 2</td></tr>
  </div>
</table>

问题

我想从结果视图中删除中间 <div my-comp> 或至少用 <tbody> 替换它。要查看我准备的完整图片 Stackblitz DEMO,希望它会有所帮助...此外,这个示例可能很明显是人为的,但这就是我试图用最少的代码重现问题的原因。所以问题应该在给定的基础设施中有一个解决方案。

更新

@AlexG 找到了用 tbody 替换中间 div 的简单方法,stackblitz 演示一开始显示了一个不错的结果。但是当我尝试在本地将它应用到我的项目时,我遇到了新问题:浏览器在 table 的动态内容准备好呈现之前安排自己的 tbody,这导致两个嵌套的 tbody 在最终视图中,根据 html 规格

似乎不一致
<table>
  <tbody>
      <tbody my-comp>
        <tr><td>custom td</td></tr>
        <tr><td>item 1</td></tr>
        <tr><td>item 2</td></tr>
    </tbody>
  </tbody>
</table>

Stackblitz 演示没有这样的问题,只有 tbody my-comp 存在。但是我本地开发环境中的项目完全相同。所以我仍在努力寻找一种方法如何 删除 中间 my-comp 容器。

更新 2

demo已根据@markdBC建议的解决方案更新。

将您的组件的选择器从 [my-comp] 更改为 tbody [my-comp] 并且您将有一个 <tbody my-comp> 而不是 <div my-comp> 如果我理解正确的话这就足够了.

我的回答受到 Slim 对此处发现的类似问题的回答的启发:

您可以通过

移除中间体<div my-comp>
  1. 正在 MyComp 组件内创建一个 TemplateRef 对象,表示 MyComp 组件的模板。
  2. 正在从结构指令访问此 TemplateRef 对象。
  3. 使用结构指令中的 TemplateRef 对象在视图容器内创建嵌入式视图。

生成的代码类似于:

MyComp分量

@Component({
  selector: "[my-comp]",
  template: `
    <ng-template #mytemplate>
      <tr>
        <td>custom td</td>
      </tr>
      <ng-template
        *ngFor="let item of data"
        [ngTemplateOutlet]="template"
        [ngTemplateOutletContext]="{ $implicit: item }"
      ></ng-template>
    </ng-template>
  `
})
export class MyComp {
  public template: TemplateRef<any>;
  public data: any;
  @ViewChild("mytemplate", { static: true }) mytemplate: TemplateRef<any>;
}

MyDir 指令

export class MyDir implements OnInit {
  private version: string;
  private data: any;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private resolver: ComponentFactoryResolver
  ) {}

  @Input() set myDirOf(data: any) {
    this.data = data;
  }

  ngOnInit() {
    const compFactory = this.resolver.resolveComponentFactory(MyComp);
    const componentRef = compFactory.create(this.viewContainer.injector);
    componentRef.instance.data = this.data;
    componentRef.instance.template = this.templateRef;
    this.viewContainer.createEmbeddedView(componentRef.instance.mytemplate);
  }
}

结果 HTML 类似于:

<table>
  <tr><td>custom td</td></tr>
  <tr><td>item 1</td></tr>
  <tr><td>item 2</td></tr>
</table>

我已经在 https://stackblitz.com/edit/table-no-div-wrapper 上准备了一个 StackBlitz 演示。