Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Material mat-table define reusable column in component

Anybody know if it is possible to create a “column” component for use with mat-table, I have tried creating a component for a commonly used column definition but when adding to the table i get an error that was unable to find the column selector, my column definition is below:

@Component({
  selector: 'iam-select-column',
  template: `
  <ng-container matColumnDef="select">
    <mat-header-cell *matHeaderCellDef>
      <mat-checkbox></mat-checkbox>
    </mat-header-cell>
    <mat-cell *matCellDef="let row">
      <mat-checkbox></mat-checkbox>
    </mat-cell>
  </ng-container>
  `,
  styles: [`
  `]
})
export class SelectColumnComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

and using this in the table

<mat-table class="mat-elevation-z8">

  <iam-select-column></iam-select-column>

  <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
  <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>

</mat-table>

and the displayedColumns are:

  displayedColumns = [
    'select'
  ];

Is it possible to do this as I would like to avoid the duplication in tables where I have a select column?

like image 366
Neil Stevens Avatar asked Nov 01 '18 11:11

Neil Stevens


2 Answers

In order to make it work you have to add that columnDef manually to table by using table.addColumnDef method.

@Component({
  selector: 'iam-select-column',
  template: `
    <ng-container matColumnDef="select">
        ...
    </ng-container>
  `
})
export class SelectColumnComponent implements OnInit {
  @ViewChild(MatColumnDef) columnDef: MatColumnDef;

  constructor(@Optional() public table: MatTable<any>, private cdRef: ChangeDetectorRef) { }

  ngOnInit() {
    if (this.table) {
      this.cdRef.detectChanges();
      this.table.addColumnDef(this.columnDef);
    }
  }
}

But before doing this we have to make sure that matColumnDef directive has already finished bindings initialization so that it has name. For that we have to run detectChanges on that component.

Ng-run Example

Another way is to provide that name in parent component as it described in angular material issue https://github.com/angular/material2/issues/13808#issuecomment-434417804:

parent.html

<mat-table class="mat-elevation-z8">

  <iam-select-column name="select"></iam-select-column>

SelectColumnComponent

@Input()
get name(): string { return this._name; }
set name(name: string) {
    this._name = name;
    this.columnDef.name = name;
}
like image 173
yurzui Avatar answered Nov 14 '22 22:11

yurzui


Here's what worked for me with Angular 12 and @angular/material 12. This code is based on code snippets from https://github.com/angular/components/issues/5889

@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...
like image 1
Nik Avatar answered Nov 14 '22 22:11

Nik