Creating instance of component and passing to another component rendering as [object HTMLelement]

1.5k views Asked by At

From my component (ex. Component), I'm trying to instantiate an Angular component (ex. CustomComponent), set some properties, and send it over to a table (ex. CustomTable) for rendering, but I keep getting [object HTMLElement] instead of the rendered element in the table cell. Here's my setup:

Component.html

<custom-table [data]="tableData"...></custom-table>

<custom-component #rowDetailTemplate></custom-component>

Component.ts

@Input() data: Array<CustomDataSource>;
@ViewChild('rowDetailTemplate') template: ElementRef;

public tableData: Array<CustomTableData> = new Array<CustomTableData>();

...

private mapper(dataSource: CustomDataSource): CustomTableData {
    var detailComponent = this.template.nativeElement;
    detailComponent.phone = dataSource.phone;

    var tableRow = new CustomTableData();
    tableRow.textColumn = "test";
    tableRow.detailComponent = detailComponent;

    return tableRow;
}

CustomComponent.html

<div>
    <span>{{phone}}</span>
</div>

CustomComponent.ts

@Component({
    selector: `[custom-component]`,
    templateUrl: 'CustomComponent.html'
})
export class CustomComponent {
    @Input() phone: string;
}

CustomTable.html

<mat-table [dataSource]="dataSource">
    <ng-container matColumnDef...>
        <mat-cell *matCellDef="let element;">
            <div [innerHTML]="element.textColumn"></div>
            <div [innerHTML]="element.detailComponent"></div>
        </mat-cell>
    </ng-container>
</mat-table>

My text column renders fine, its just the custom-component that isn't rendering properly.

Any suggestions?

Note that CustomTable needs to be able to accept any type of component/element in detailComponent, not just my CustomComponent.

1

There are 1 answers

1
TabsNotSpaces On BEST ANSWER

Instead of trying to pass the component into the table, I ended up passing the table a ComponentFactory, then the table would take care of instantiating the component from a factory and attaching it to a placeholder once the table was done loading the data (otherwise it would try to attach the component to a placeholder that doesn't exist yet).

Here is what I ended up with:

Component.html

<custom-table [data]="tableData"...></custom-table>

Component.ts

@Input() data: Array<CustomDataSource>;

public tableData: Array<CustomTableData> = new Array<CustomTableData>();
...
private mapper(dataSource: CustomDataSource): CustomTableData {
    var detailComponentFactory: TableExpandableFactoryColumn = {
            componentFactory: this.componentFactoryResolver.resolveComponentFactory(CustomComponent),
            properties: {
                "phone": dataSource.phone;
            }
        }    

    var tableRow : TableExpandableDataRow = {
        rowId: dataSource.rowID,
        columns: {
            "detailComponentFactory": detailComponentFactory,
            "textColumn": "test"
        }
    }
    return tableRow;
}

CustomComponent.html

<div>
    <span>{{phone}}</span>
</div>

CustomComponent.ts

@Component({
    selector: `[custom-component]`,
    templateUrl: 'CustomComponent.html'
})
export class CustomComponent {
    @Input() phone: string;
}

CustomTable.html

<mat-table [dataSource]="dataSource">
    <ng-container matColumnDef...>
        <mat-cell *matCellDef="let row;">
            <div [innerHTML]="row.textColumn"></div>
            <div id="detail-placeholder-{{row.internalRowId}}" className="cell-placeholder"></div>
        </mat-cell>
    </ng-container>
</mat-table>

CustomTable.ts (the meat of the solution)

...
@Input() data: any;
public placeholders: { placeholderId: string, factoryColumn: TableExpandableFactoryColumn }[];
public dataSource: MatTableDataSource<any>;
...
constructor(private renderer: Renderer2,
        private injector: Injector,
        private applicationRef: ApplicationRef) {

}
...
public ngOnChanges(changes: SimpleChanges) {
    if (changes['data']) {
        // Wait to load table until data input is available
        this.setTableDataSource();
        this.prepareLoadTableComponents();
    }
}
...
private setTableDataSource() {
    this.placeholders = [];

    this.dataSource = new MatTableDataSource(this.data.map((row) => {
        let rowColumns = {};

        // process data columns
        for (let key in row.columns) {
            if ((row.columns[key] as TableExpandableFactoryColumn).componentFactory != undefined) {
                // store component data in placeholders to be rendered after the table loads
                this.placeholders.push({
                    placeholderId: "detail-placeholder-" + row.rowId.toString(),
                    factoryColumn: row.columns[key]
                });
                rowColumns[key] = "[" + key + "]";
            } else {
                rowColumns[key] = row.columns[key];
            }
        }

        return rowColumns;
    }));
}

private prepareLoadTableComponents() {
    let observer = new MutationObserver((mutations, mo) => this.loadTableComponents(mutations, mo, this));
    observer.observe(document, {
        childList: true,
        subtree: true
    });
}

private loadTableComponents(mutations: MutationRecord[], mo: MutationObserver, that: any) {
    let placeholderExists = document.getElementsByClassName("cell-placeholder"); // make sure angular table has rendered according to data
    if (placeholderExists) {
        mo.disconnect();

        // render all components
        if (that.placeholders.length > 0) {
            that.placeholders.forEach((placeholder) => {
                that.createComponentInstance(placeholder.factoryColumn, placeholder.placeholderId);
            });
        }
    }

    setTimeout(() => { mo.disconnect(); }, 5000); // auto-disconnect after 5 seconds
}

private createComponentInstance(factoryColumn: TableExpandableFactoryColumn, placeholderId: string) {
    if (document.getElementById(placeholderId)) {
        let component = this.createComponentAtElement(factoryColumn.componentFactory, placeholderId);
        // map any properties that were passed along
        if (factoryColumn.properties) {
            for (let key in factoryColumn.properties) {
                if (factoryColumn.properties.hasOwnProperty(key)) {
                    this.renderer.setProperty(component.instance, key, factoryColumn.properties[key]);
                }
            }

            component.changeDetectorRef.detectChanges();
        }
    }
}

private createComponentAtElement(componentFactory: ComponentFactory<any>, placeholderId: string): ComponentRef<any> {
    // create instance of component factory at specified host
    let element = document.getElementById(placeholderId);
    let componentRef = componentFactory.create(this.injector, [], element);
    this.applicationRef.attachView(componentRef.hostView);

    return componentRef;
}

...
export class TableExpandableFactoryColumn {
    componentFactory: ComponentFactory<any>;
    properties: Dictionary<any> | undefined;
}
export class TableExpandableDataRow {
    rowId: string;
    columns: Dictionary<any>;
}