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