2

Folks.

I need help. I'm creating a pivot table that must contain a global filter and column filters. But the way I'm coding I'm not getting it.

Follow the code and the link of stackblitz

<p>data-table-dynamic works!</p>

<mat-form-field *ngIf="filter">
  <input
    matInput
    (keyup)="applyFilter($event)"
    placeholder="{{ filterPlaceholder }}"
  />
</mat-form-field>

<div class="mat-elevation-z8">
  <table
    mat-table
    [dataSource]="dataSource"
    matSort
    [ngStyle]="{ 'min-width': +tableMinWidth + 'px' }"
  >
    <ng-container *ngFor="let column of columns">
      <ng-container matColumnDef="{{ column.columnDef }}">
        <th class="header" mat-header-cell *matHeaderCellDef mat-sort-header>
          <div fxFlexFill>
            {{ column.header }}
          </div>
        </th>
        <td mat-cell *matCellDef="let row">{{ column.cell(row) }}</td>
      </ng-container>
    </ng-container>

    <ng-container *ngIf="buttons.length >= 0">
      <ng-container matColumnDef="actions">
        <th mat-header-cell *matHeaderCellDef>
          <button
            *ngIf="columnsFilter"
            mat-icon-button
            matTooltip="Toggle Filters"
            (click)="toggleFilters = !toggleFilters"
          >
            <mat-icon>search</mat-icon>
          </button>
        </th>
        <td
          mat-cell
          *matCellDef="let row"
          [ngStyle]="{ 'min-width': 'calc(55px * ' + buttons.length + ')' }"
        >
          <div class="btn-group" *ngFor="let button of buttons">
            <button
              mat-icon-button
              [matMenuTriggerFor]="menu"
              [matMenuTriggerData]="{ data: row }"
            >
              <mat-icon>more_vert</mat-icon>
            </button>
          </div>
        </td>
      </ng-container>
    </ng-container>

    <ng-container *ngFor="let column of columns; let i = index">
      <ng-container matColumnDef="{{ column.columnSearch }}">
        <th class="header" mat-header-cell *matHeaderCellDef>
          <div
            fxFlexFill
            class="filters-container"
            [class.animate]="toggleFilters"
          >
            <mat-form-field *ngIf="i >= 0" appearance="outline">
              <input
                matInput
                placeholder="Press 'Enter' to search"
                [(ngModel)]="filtersModel[i]"
                (keyup)="searchColumns()"
              />
              <mat-icon matSuffix>search</mat-icon>
            </mat-form-field>
          </div>
        </th>
        <td mat-cell *matCellDef="let row">{{ column.cell(row) }}</td>
      </ng-container>
    </ng-container>

    <ng-container matColumnDef="filter" *ngIf="columnsFilter">
      <th mat-header-cell *matHeaderCellDef class="filterHeaderCell">
        <div class="filters-container" [class.animate]="toggleFilters">
          <button
            mat-icon-button
            matTooltip="Clear Filters"
            (click)="clearFilters()"
          >
            <mat-icon>search_off</mat-icon>
          </button>
        </div>
      </th>
    </ng-container>

    <!-- Disclaimer column - with nullable approach -->
    <ng-container matColumnDef="disclaimer" *ngIf="footer">
      <td mat-footer-cell *matFooterCellDef colspan="100%">
        <strong>{{ footer }}</strong>
      </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <ng-container *ngIf="columnsFilter">
      <tr
        mat-header-row
        *matHeaderRowDef="displayedColumnsSearch"
        class="mat-header-filter"
      ></tr>
      <!-- class="no-default-height" -->
    </ng-container>
    <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>

    <!-- <tr mat-header-row *matHeaderRowDef="searchColumn"></tr> -->

    <ng-container *ngIf="footer">
      <!-- Make footer nullable -->
      <tr
        mat-footer-row
        *matFooterRowDef="['disclaimer']"
        class="second-footer-row"
      ></tr>
    </ng-container>
  </table>

  <mat-paginator
    [pageSizeOptions]="pagination"
    [pageSize]="pageSize"
    [ngStyle]="{ 'min-width': +tableMinWidth + 'px' }"
  ></mat-paginator>

  <mat-menu #menu="matMenu">
    <ng-template matMenuContent let-data="data">
      <div *ngFor="let button of menuButtons">
        <button
          mat-menu-item
          (click)="this.buttonClick.emit([button.action, button.payload(data)])"
        >
          <mat-icon>{{ button.icon }}</mat-icon>
          {{ button.description }}
        </button>
      </div>
    </ng-template>
  </mat-menu>
</div>

import {
  Component,
  EventEmitter,
  Input,
  OnChanges,
  OnInit,
  Output,
  SimpleChanges,
  ViewChild,
} from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { TableBtn, TableColumn } from '../../core/interfaces';
import { TableMenu } from '../../core/interfaces/table-menu';

@Component({
  selector: 'app-data-table-dynamic',
  templateUrl: './data-table-dynamic.component.html',
  styleUrls: ['./data-table-dynamic.component.scss'],
})
export class DataTableDynamicComponent implements OnChanges, OnInit {
  @Input() columns: TableColumn[] = [];
  @Input() buttons: TableBtn[] = [];
  @Input() menuButtons: TableMenu[] = [];
  @Input() data: any[] = [];
  @Input() filter: boolean = false;
  @Input() filterPlaceholder: string = 'Filter';
  @Input() columnsFilter: boolean = false;
  @Input() footer: string = null;
  @Input() pagination: number[] = [];
  @Input() pageSize: number;
  @Input() tableMinWidth: number = 500;
  @Output() filteredData = new EventEmitter<any[]>();
  @Output() buttonClick = new EventEmitter<string[]>();

  dataSource: MatTableDataSource<any>;
  displayedColumns: string[];
  displayedColumnsSearch: string[];

  headers: string[] = this.columns.map((x) => x.columnDef);
  headersFilters = this.headers.map((x, i) => x + '_' + i);
  filtersModel = [];
  filterKeys = {};

  toggleFilters = true;

  @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
  @ViewChild(MatSort, { static: true }) sort: MatSort;

  constructor() {}

  ngOnInit(): void {}

  ngOnChanges(changes: SimpleChanges): void {
    console.log(changes);
    if (this.data) {
      if (changes.data) {
        this.dataSource = new MatTableDataSource(this.data);
        this.dataSource.filterPredicate = (item, filter: string) => {
          const colMatch = !Object.keys(this.filterKeys).reduce(
            (remove, field) => {
              return (
                remove ||
                !item[field]
                  .toString()
                  .toLocaleLowerCase()
                  .includes(this.filterKeys[field])
              );
            },
            false
          );
          return colMatch;
        };
        this.dataSource.sort = this.sort;
        this.dataSource.paginator = this.paginator;
        this.displayedColumns = [...this.columns.map((c) => c.columnDef)];
        this.displayedColumnsSearch = [
          ...this.columns.map((c) => c.columnSearch),
          'filter',
        ];

        this.columns.forEach((value, index) => {
          this.filterKeys[this.columns[index].columnDef] = '';
        });
        if (this.buttons.length > 0)
          this.displayedColumns = [...this.displayedColumns, 'actions'];
      }
    }
  }

  applyFilter(filterValue) {
    this.dataSource.filter = filterValue.target.value.trim().toLowerCase();
    this.filteredData.emit(this.dataSource.filteredData);

    if (this.dataSource.paginator) {
      this.dataSource.paginator.firstPage();
    }

    this.dataSource.sort = this.sort;
  }

  searchColumns() {
    this.filtersModel.forEach((each, ind) => {
      this.filterKeys[this.columns[ind].columnDef] = each || '';
    });
    //Call API with filters
    this.dataSource.filter = JSON.stringify(this.filterKeys);
    this.filteredData.emit(this.dataSource.filteredData);

    if (this.dataSource.paginator) {
      this.dataSource.paginator.firstPage();
    }

    this.dataSource.sort = this.sort;
  }

  clearFilters() {
    this.filtersModel = [];
    this.columns.forEach((value, index) => {
      this.filterKeys[this.columns[index].columnDef] = '';
    });
    //Call API without filters
    this.searchColumns();
  }
}

StackBlitz

I know it's not like this, but I did some tests and I couldn't. I put the working code so you can help me. If you have any article or example, it will help a lot.

I took this code as an example and adjusted it to what I need.

  • Really I'm not pretty sure you can get it. When we use a custom filter in a mat-table. You has two things: a "string" filter `this.dataSource.filter` and a customFilterFunction in the way `customFilter = (data: any, filter: string) => {...}`. This is the reason because when we want to filter using two or more variables we usually use JSON.stringify(..an object..); and JSON.parse(the string). In this [SO](https://stackoverflow.com/questions/68495473/how-to-add-multiple-filters-in-angular-mat-table/68496521#68496521) you has a example, I don't know if help – Eliseo May 23 '22 at 21:17
  • Yes Elisha, that's right, but I imagined passing a parameter, perhaps, to identify the shape of the filter. So I couldn't do it. I think I will have to put some option for the user to select to pass this value. Type: Advanced Filter; Simple Filter; I didn't want to do that. I wanted a more automatic way. If you have any ideas it would be nice. – Alexander Oliveira May 24 '22 at 09:49

1 Answers1

2

Well, we can create a FormGroup using the values of the columns, for this columns' is a property and we use the way @Input('columns') set _(value)`

  form: FormGroup = new FormGroup({});
  columns: TableColumn[] = [];
  @Input('columns') set _columns(value) {
    this.columns = value;
    value.forEach((x) => {
      this.form.addControl(x.columnDef, new FormControl());
    });
    this.form.addControl('_general', new FormControl());
    this.form.valueChanges.subscribe((res) => {
      this.dataSource.filter = JSON.stringify(res);
    });
  }

Our "customFilter" can be like

customFilter = (data: any, filter: string) => {
    const filterData = JSON.parse(filter);
    let ok = true;
    if (filterData._general)
    {
      const search = filterData._general.toLowerCase();
      ok=false;
      for (const prop in data) {
        ok = ok || (''+data[prop]).toLowerCase().indexOf(search) >= 0;
      }

    }
    Object.keys(filterData).forEach((x) => {
      if (x!='_general' && filterData[x]) {
          if (ok) ok = (''+data[x]).toLowerCase().indexOf(filterData[x].toLowerCase())>=0;
      }
    });
    return ok;
  };

At last, enclosed the table in a formGroup.

<form *ngIf="form" [formGroup]="form">
  <mat-form-field *ngIf="filter">
    <input
      matInput formControlName="_general"
      placeholder="{{ filterPlaceholder }}"
    />
  </mat-form-field>

  <div class="mat-elevation-z8">
    <table ...>
      ...
      <!-- our "inputs" filter becomes like-->
       <mat-form-field *ngIf="i >= 0" appearance="outline">
           <input matInput
                placeholder="Press 'Enter' to search"
                [formControlName]="column.columnDef"
              />
              <mat-icon matSuffix>search</mat-icon>
       </mat-form-field>
    ...
    </table>
</form>

Update my bad!!

As we are putting the table under a *ngIf="form", the "paginator" (and the sort) is not accesible until the form is created. For this we need make some changes

  1. We remove the {static:true} in ViewChild(MatPaginator) and ViewChild(MatSort)

      @ViewChild(MatPaginator) paginator: MatPaginator;
      @ViewChild(MatSort) sort: MatSort;
    

    Remember that we only can use {static:true} if the element is always in the component -if it is not under a *ngIf-

  2. Implements in the component AfterViewInit

    export class DataTableDynamicComponent implements OnChanges,AfterViewInit{..}
    
  3. Is in this function we need assing the paginator and sort

      ngAfterViewInit()
      {
        this.dataSource.paginator=this.paginator
        this.dataSource.sort=this.sort
    
      }
    
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • I understood. I will try to do it this way. Thanks – Alexander Oliveira May 24 '22 at 17:11
  • It worked Eliseo. However the ordering of columns and pagination of the table stopped. I still can't figure out why it stopped. If you have any ideas, thank you. It is clear. thank you very much for the help with the search field. [link](https://stackblitz.com/edit/angular-ivy-bgk9td) – Alexander Oliveira May 27 '22 at 00:52
  • @AlexanderOliveira my bad!! As we are putting the table under a *ngIf="form", the "paginator" is not accesible until the form is created, see the updated answer. (sorry for the inconveniences). Your [forked stackblitz](https://stackblitz.com/edit/angular-ivy-gd87me?file=src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fcore%2Fmock%2Ffunctions%2Fmock-data.ts,src%2Fapp%2Fapp.component.html,src%2Fapp%2Fcomponents%2Fdata-table-dynamic%2Fdata-table-dynamic.component.html,src%2Fapp%2Fcomponents%2Fdata-table-dynamic%2Fdata-table-dynamic.component.ts) – Eliseo May 27 '22 at 06:36
  • That's right Eliseo. I forgot to remove { static: true }. As soon as I posted I remembered the ngAfterViewInit, I put it but I forgot to remove the Viewe from the static. Thank you very much. It's little coffee in the veins lol. – Alexander Oliveira May 27 '22 at 11:13
  • Eliseo. Can you help me with another point I'm having trouble with? I created another post, could you help me? https://stackoverflow.com/questions/73244165/angular-material-reusable-table-pagination-server-not-working – Alexander Oliveira Aug 09 '22 at 12:15