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.

2
  • 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 you has a example, I don't know if help Commented May 23, 2022 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. Commented May 24, 2022 at 9:49

1 Answer 1

3

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
    
      }
    
Sign up to request clarification or add additional context in comments.

5 Comments

I understood. I will try to do it this way. Thanks
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
@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
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.
Eliseo. Can you help me with another point I'm having trouble with? I created another post, could you help me? stackoverflow.com/questions/73244165/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.