I have quite complex infrastructure in my project which contains of
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.
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
TemplateRef
object representing the template of the MyComp
component inside the MyComp
component.TemplateRef
object from the structural directive.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.