Search code examples
javascriptangularangular-directiveangular2-directivesangular-dynamic-components

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


I have quite complex infrastructure in my project which contains of

  • host component
  • structural directive used in host component's template (MyDir)
  • another component used in structural directive (MyComp)

Simplified version looks like the following.

host component

@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' } ];
}

structural directive

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;
  }
}

structural directive's component

@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;
}

The output is

custom td
item 1
item 2

which is fine except the markup which is

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

Problem

I want to remove intermediate <div my-comp> from the result view or at least replace it with <tbody>. To see the whole picture I prepared Stackblitz DEMO, hope it will help... Also, it might be obvious the example is artificial, but this is what I came with trying to reproduce the issue with minimal code. So the problem should have a solution in the given infrastructure.

Update

@AlexG found simple way to replace intermediate div with tbody and stackblitz demo showed a good result at first. But when I tried to apply it to my project locally I faced new issue: browser arranges its own tbody before the dynamic contents of the table are ready to render, which results in two nested tbody in the end view, which seems inconsistent per html specs

<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 demo has no such problem, only tbody my-comp is present. But exactly the same project in my local dev environment does. So I'm still trying to find a way how to remove intermediate my-comp container.

Update 2

The demo had been updated in accordance with the solution suggested by @markdBC.


Solution

  • My answer is inspired by Slim's answer to a similar question found here: https://stackoverflow.com/a/56887630/12962012.

    You can remove the intermediate <div my-comp> by

    1. Creating a TemplateRef object representing the template of the MyComp component inside the MyComp component.
    2. Accessing this TemplateRef object from the structural directive.
    3. Creating an embedded view inside the view container with the TemplateRef object from the structural directive.

    The resulting code looks something like:

    MyComp component

    @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 directive

    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);
      }
    }
    

    The resulting HTML looks something like:

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

    I've prepared a StackBlitz demo at https://stackblitz.com/edit/table-no-div-wrapper.