Open andrewseguin opened 6 years ago
TL;DR: This prototype was incredibly helpful as I've tried to share column definitions between tables. Is there an angular8 version? I'm trying to adapt it, without success.
DETAIL: Here is my component (details removed for brevity):
@Component({
selector: 'app-abstract-list-column-select',
template: `
<ng-container matColumnDef>
<th mat-header-cell *matHeaderCellDef>HEADER</th>
<th mat-footer-cell *matFooterCellDef>FOOTER</th>
<td mat-cell *matCellDef="let row">CELL</td>
</ng-container>`
})
export class AbstractListColumnSelectComponent implements OnInit {
// NOTE: order: constructor(), static @ViewChild, @Input, OnInit(), non-static @ViewChild
@Input()
get name() { return this._name; }
set name(name : string) {
console.log('name=', name, this.columnDef); this._name = name;
if (this.columnDef) { this.columnDef.name = name; }
}
_name : string = 'select_default';
@ViewChild(MatColumnDef, { static : true })
get columnDef() { return this._columnDef; }
set columnDef(columnDef) {
console.log('cdef=', columnDef); this._columnDef = columnDef;
if (this.table) { this.table.addColumnDef(this.columnDef); }
}
_columnDef ! : MatColumnDef;
constructor(readonly table : MatTable<any>) { console.log('new', table); }
ngOnInit() {
console.log('OnInit()', this, this.table, this.columnDef);
if (this.table && this.columnDef) {
this.table.addColumnDef(this.columnDef);
}
}
// TODO: add ngOnDestroy()
}
Here is the (start of) my code for the table using the component, where tableColumns
includes "select" but not "selectN".
<table mat-table matSort [dataSource]="dataSource">
<tr mat-header-row *matHeaderRowDef="tableColumns; sticky: true"></tr>
<tr mat-footer-row *matFooterRowDef="tableColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: tableColumns;"></tr>
<app-abstract-list-column-select name="selectN"></app-abstract-list-column-select>
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>HEADER</th>
<th mat-footer-cell *matFooterCellDef>FOOTER</th>
<td mat-cell *matCellDef="let row">CELL</td>
</ng-container>
As written, it runs and I see the expected column. In the debugger, matTable.columnDefsByName
shows both "select" and "selectN", and both have templates in value.cell
, value.headerCell
, and value.footerCell
. However, if I swap "selectN" and "select" (to replace the original column with my component), I get an error:
ERROR TypeError: Cannot read property 'template' of undefined
at MatHeaderRowDef.extractCellTemplate (table.js:357)
at table.js:1975
at Function.from (<anonymous>)
at MatTable._getCellTemplates (table.js:1965)
at MatTable._renderRow (table.js:1920)
at table.js:1779
at Array.forEach (<anonymous>)
at MatTable._forceRenderHeaderRows (table.js:1774)
at MatTable.ngAfterContentChecked (table.js:1251)
at callProviderLifecycles (core.js:32324)
and if I look at matTable.columndefsByName
, "select" (my new template) is missing value.cell
, value.headerCell
, and value.footerCell
, which (I assume) causes the error message.
I would appreciate any suggestions.
FYI, I finally got this to work, by explicitly connecting the columnDef to its children. I don't know if this happens later, or if it should happen sooner but doesn't for some reason.
@ViewChild( MatColumnDef, { static : true }) columnDef ! : MatColumnDef;
@ViewChild( MatCellDef, { static : true }) cellDef ! : MatCellDef;
@ViewChild(MatHeaderCellDef, { static : true }) headerCellDef ! : MatHeaderCellDef;
@ViewChild(MatFooterCellDef, { static : true }) footerCellDef ! : MatFooterCellDef;
ngOnInit() {
if (this.table && this.columnDef) {
this.columnDef.cell = this.cellDef;
this.columnDef.headerCell = this.headerCellDef;
this.columnDef.footerCell = this.footerCellDef;
this.table.addColumnDef(this.columnDef);
}
}
Thank you, @kussmaul!
Here's what I cobbled together from your code and @andrewseguin's prototype.
@Component({
selector: 'app-column-template',
template: `
<ng-container matColumnDef>
<th mat-header-cell *matHeaderCellDef>{{ label || capitalize(name) }}</th>
<td mat-cell *matCellDef="let row">
<ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: row }"></ng-container>
</td>
</ng-container>
`,
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
class: 'column-template cdk-visually-hidden',
'[attr.ariaHidden]': 'true',
},
})
export class ColumnTemplateComponent implements OnDestroy, OnInit {
@Input() name = '';
@Input() label: string | null = null;
@Input() align: 'before' | 'after' = 'before';
constructor(@Optional() public table: MatTable<unknown>) {}
@ViewChild(MatColumnDef, { static: true }) columnDef!: MatColumnDef;
@ViewChild(MatCellDef, { static: true }) cellDef!: MatCellDef;
@ViewChild(MatHeaderCellDef, { static: true }) headerCellDef!: MatHeaderCellDef;
@ViewChild(MatFooterCellDef, { static: true }) footerCellDef!: MatFooterCellDef;
@ContentChild('cell', { static: false })
cellTemplate: TemplateRef<unknown> | null = null;
ngOnInit(): void {
if (this.table && this.columnDef) {
this.columnDef.name = this.name;
this.columnDef.cell = this.cellDef;
this.columnDef.headerCell = this.headerCellDef;
this.columnDef.footerCell = this.footerCellDef;
this.table.addColumnDef(this.columnDef);
}
}
ngOnDestroy(): void {
if (this.table) {
this.table.removeColumnDef(this.columnDef);
}
}
capitalize(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
export type CellValueNeededFn = (data: Record<string, unknown>, name: string) => string;
@Component({
selector: 'app-column',
template: `
<ng-container matColumnDef>
<th mat-header-cell *matHeaderCellDef>{{ label || capitalize(name) }}</th>
<td mat-cell *matCellDef="let row">{{ getCellValue(row) }}</td>
</ng-container>
`,
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
class: 'column cdk-visually-hidden',
'[attr.ariaHidden]': 'true',
},
})
export class ColumnComponent implements OnDestroy, OnInit {
@Input() name = '';
@Input() label: string | null = null;
@Input() align: 'before' | 'after' = 'before';
@Input() cellValueNeeded: CellValueNeededFn | null = null;
constructor(@Optional() public table: MatTable<unknown>) {}
@ViewChild(MatColumnDef, { static: true }) columnDef!: MatColumnDef;
@ViewChild(MatCellDef, { static: true }) cellDef!: MatCellDef;
@ViewChild(MatHeaderCellDef, { static: true }) headerCellDef!: MatHeaderCellDef;
@ViewChild(MatFooterCellDef, { static: true }) footerCellDef!: MatFooterCellDef;
@ContentChild('cell', { static: false })
cellTemplate: TemplateRef<unknown> | null = null;
ngOnInit(): void {
if (this.table && this.columnDef) {
this.columnDef.name = this.name;
this.columnDef.cell = this.cellDef;
this.columnDef.headerCell = this.headerCellDef;
this.columnDef.footerCell = this.footerCellDef;
this.table.addColumnDef(this.columnDef);
}
}
ngOnDestroy(): void {
if (this.table) {
this.table.removeColumnDef(this.columnDef);
}
}
capitalize(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}
getCellValue(row: Record<string, unknown>): unknown {
return this.cellValueNeeded ? this.cellValueNeeded(row, this.name) : row[this.name];
}
}
An attempt to build ColumnComponent
based on ColumnTemplateComponent
for me ends up in the familiar
Error: Could not find column with id "...".
at getTableUnknownColumnError (table.js:1078) [angular]
blah-blah-blah...
@mykolav here's an updated version as a angular 14 standalone component that I have working:
import { CommonModule } from '@angular/common';
import { Component, HostBinding, Input, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
import { MatSortModule } from '@angular/material/sort';
import {
MatCellDef,
MatColumnDef,
MatFooterCellDef,
MatHeaderCellDef,
MatTable,
MatTableModule,
} from '@angular/material/table';
@Component({
standalone: true,
imports: [MatTableModule, CommonModule, MatSortModule],
selector: 'evt-table-column-currency',
template: `
<ng-container matColumnDef>
<th mat-header-cell *matHeaderCellDef>
<span mat-sort-header> {{ label ?? capitalize(prop) }}</span>
</th>
<td mat-cell *matCellDef="let row">{{ row[prop] | currency }}</td>
</ng-container>
`,
})
export class TableCurrencyColumnComponent implements OnDestroy, OnInit {
@Input() prop = '';
@Input() label: string | undefined;
constructor(@Optional() public table: MatTable<unknown>) {}
@HostBinding('attr.ariaHidden') ariaHidden!: true;
@HostBinding('class') classes!: 'column-template cdk-visually-hidden';
@ViewChild(MatColumnDef, { static: true }) columnDef!: MatColumnDef;
@ViewChild(MatCellDef, { static: true }) cellDef!: MatCellDef;
@ViewChild(MatHeaderCellDef, { static: true }) headerCellDef!: MatHeaderCellDef;
@ViewChild(MatFooterCellDef, { static: true }) footerCellDef!: MatFooterCellDef;
ngOnInit(): void {
if (this.table && this.columnDef) {
this.columnDef.name = this.prop;
this.columnDef.cell = this.cellDef;
this.columnDef.headerCell = this.headerCellDef;
this.columnDef.footerCell = this.footerCellDef;
this.table.addColumnDef(this.columnDef);
}
}
ngOnDestroy(): void {
if (this.table) {
this.table.removeColumnDef(this.columnDef);
}
}
capitalize(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
Just made it work now. Thanks a lot for posting your solution.
@b-johnse How exactly is youR template where you instantiate the component. Trying your solution but I am definitely making something wrong. I still get the "Could not find column with id" error.
Introduce a new table component that would use
<mat-table>
and come with built-in features including sorting, pagination, selection, etc. Would accept an array-based data input so users do not need to create a data source to use the table.This would allow people to quickly use a table but would limit their ability to customize behavior and would be very opinionated on user experience.
Example concept:
Notes: