2

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.

dhilt
  • 18,707
  • 8
  • 70
  • 85

2 Answers2

4

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.

markdBC
  • 56
  • 3
  • 1
    Looks like it's your first answer on SO and it is so helpful! Please continue to contribute, the community needs you! – dhilt Feb 25 '20 at 23:49
2

Change the selector of your component from [my-comp] to tbody [my-comp] and your will have a <tbody my-comp> instead of a <div my-comp> which would be sufficient if I understood you correctly.

AlexG
  • 76
  • 2
  • I updated the Question... seems we have a problem with `tbody`, browser is too smart and your approach causes nested `tbody` appearance in real environment – dhilt Nov 14 '19 at 14:56