2

I have a flexible table component which has 2 modes:

  1. Normal table - which works
  2. Custom Row Template - which doesn't because angular adds <ng-component> tag

Logic in TableComponent is not important, template is:

<table class="table table-hover table-sm table-middle table-crud">
    <thead role="rowgroup">
        <!-- SNIP -->
    </thead>
    <tbody *ngIf="customRowComponent" crud-table-row-renderer
            [renderComponent]="customRowComponent"
            [rows]="rows"
            [columns]="columns"
            [config]="config"
            [parent]="parent"
            [rowActions]="rowActions"
            [cellActionEmitter]="cellActionEmitter"
            [selectedRows]="selectedRows">
    </tbody>
    <tbody *ngIf="!customRowComponent" crud-table-row
            [rows]="rows"
            [columns]="columns"
            [config]="config"
            [parent]="parent"
            [rowActions]="rowActions"
            [cellActionEmitter]="cellActionEmitter"
            [selectedRows]="selectedRows">
    </tbody>
</table>

And as you can see, everything depends on customRowComponent.

If I don't set it, table works perfectly:

<tbody>
    <tr>
        <td></td>
        <td></td>
        <td></td>
    </tr>
</tbody>

However once I use some custom row component html becomes like this:

<tbody>
    <div custom-row-selector>
        <tr>
            <td></td>
            <td></td>
            <td></td>
        </tr>
    </div>
</tbody>

Which obviously breaks table. Component that is used to render custom component looks like this:

@Component({
    selector: '[crud-table-row-renderer]',
    template: '<ng-template #container></ng-template>',
})
export class RowRenderer implements OnInit, OnChanges {
    @Input() public config = new TableConfig();
    @Input() public columns: FieldConfig[];
    @Input() public rows: any[];
    @Input() public rowActions: RowAction[];
    @Input() public parent: any;
    @Input() public renderComponent: any;
    @Input() public selectedRows: number[];

    @Input() public cellActionEmitter: any;

    @ViewChild('container', {read: ViewContainerRef, static: true}) viewContainer: ViewContainerRef;

    instance: TableRowComponentInterface;

    constructor(
        @Host() public host: ElementRef,
        protected componentFactoryResolver: ComponentFactoryResolver,
    ) {
    }

    ngOnInit(): void {
        this.loadRenderer();
    }

    ngOnChanges(changes: SimpleChanges): void {
        if (changes && changes.rows && changes.rows.currentValue) {
            this.instance.rows = changes.rows.currentValue;
        }
    }

    loadRenderer() {
        const component = this.renderComponent;
        const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
        const viewContainerRef = this.viewContainer;

        const componentRef = viewContainerRef.createComponent(componentFactory);
        this.instance = componentRef.instance as TableRowComponentInterface;
        this.instance.config = this.config;
        this.instance.columns = this.columns;
        this.instance.rows = this.rows;
        this.instance.rowActions = this.rowActions;
        this.instance.parent = this.parent;
        this.instance.selectedRows = this.selectedRows;
        this.instance.cellActionEmitter = this.cellActionEmitter;
    }
}

So the big question is: How to make sure angular doesn't wrap this dynamically created component into div or ng-component tag?

Kaminari
  • 1,387
  • 3
  • 17
  • 32
  • I also have the same issue? Have you figured it out? I can't seem to get Angular to stop wrapping custom components. I have tried `ViewEncapsulation.None` but it's not working. – Mario Subotic Jul 29 '19 at 08:31
  • In what way does it "break" the table? Could you provide a Stackblitz or similar to show the actual problem? – Wilt Apr 06 '23 at 18:28

1 Answers1

1

I assume that you mean with "obviously breaks" the table is that it is no longer rendered correctly because of the ng-component between the tbody and tr elements. Since you create a custom component and you didn't declare a custom selector the angular component will be rendered with the default ng-component selector. A component is simply wrapped in a selector; that is (as far as I know) nothing that you can prevent. A component simply needs to be wrapped in a selector when rendered in the html output. If you don't want selectors, you should use directives (attributes) instead.

But rendering the table correctly can be simply resolved by adding the following style to your component:

:host {
  display: contents;
}

Read more on display: contents here on MDN. This simply will ignore the component completely when the browser is rendering it in html and only consider it's contents.

So the declaration of your custom row component could look something like this:

@Component({
  //selector: 'my-row-component',
  templateUrl: './row.component.html',
  styles: [':host { display: contents }']
}

I commented out the selector, but you will see that if you uncomment it the default ng-component selector will be replaced in the html with your custom value accordingly, but it will not make a difference in rendering.

I made a Stackblitz here to demonstrate this solution. In this Stackblitz I simplified your code example from above to the bear minimum to simply show how this would work.

Wilt
  • 41,477
  • 12
  • 152
  • 203