Juan Marino
Juan Marino

Reputation: 51

getting ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: when trying to filter a mat table

I have a mat table with a filter that uses chips to filter multiple criterias.

When I go to filter the first time and save the criteria,I get the error ExpressionChangedAfterItHasBeenCheckedError. It says it went from undefined to a number.

I've seen in multiple treads saying that I need to change the way I initialize the table and data to be loaded to the table. I've added ngAfterContentChecked() with this.ref.detectChanges(); and it gets rid of the error, but not the wrong behavior.

I start with ngOnInit getting the data from a service.

What should I do to get the correct behavior?

See the code attached!

thanks!

DetentionValidation.component.ts

import { AfterViewInit, Component, ViewChild, OnInit } from '@angular/core';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { SelectionModel } from '@angular/cdk/collections';
import { DetentionValidationService } from './detentionValidation.service';
import { Subscription } from 'rxjs';
import { centerInterface } from './centers-data.model';
import { StopsInterface } from './stops-data.model';
import { ChangeDetectorRef } from '@angular/core';
import { parse } from 'dotenv';

//data interfaces
interface operation {
  value: string;
  viewValue: string;
}

// export interface StopsInterface {
//   StopNbr: number;
//   Wgt: number;
// }
export interface SearchItem {
  name: string;
}

@Component({
  selector: 'app-detentionValidation',
  templateUrl: './detentionValidation.component.html',
  styleUrls: ['./detentionValidation.component.css'],
})
export class DetentionValidationComponent implements AfterViewInit, OnInit {
  isLoading = true;
  selectedCenter!: string;
  selectedOperation!: string;

  operations: operation[] = [
    { value: 'Pickup', viewValue: 'Pickup' },
    { value: 'Delivery', viewValue: 'Delivery' },
  ];

  centers: centerInterface[] = [];
  stops: StopsInterface[] = [];
  private allDataSubs: Subscription;

  //which rows are selected
  selection = new SelectionModel<StopsInterface>(true, []);

  public globalFilter = '';
  public searchItems: SearchItem[] = [];
  public reset_data = this.stops;
  displayedColumns: string[] = [
    'WAND_STOP_NBR',
    'UNIT_NBR',
    'AGGR_SHP_WGT',
    'select',
  ];
  dataSource = new MatTableDataSource(this.stops);
  stopsDataSource = [];

  selectable = true;
  removable = true;
  readonly separatorKeysCodes: number[] = [COMMA, ENTER];
  public searchTerms: string[] = [];

  constructor(
    private _liveAnnouncer: LiveAnnouncer,
    public detentionValidationService: DetentionValidationService,
    private ref: ChangeDetectorRef
  ) {}

  @ViewChild(MatSort, { static: false }) set content(sort: MatSort) {
    this.dataSource.sort = sort;
  }

  ngAfterViewInit() {}

  async ngOnInit() {
    const response = await this.detentionValidationService.getCenters();

    //loading Service centers for drop down
    this.detentionValidationService
      .getCentersUpdateListener()
      .subscribe((centers: centerInterface[]) => {
        this.centers = centers;
        // this.isLoading = false;
      });

    //loading all data
    this.detentionValidationService
      .getStopsUpdateListener()
      .subscribe((stops: StopsInterface[]) => {
        stops.forEach((element) => {
          this.stops.push({
            WAND_STOP_NBR: element.WAND_STOP_NBR,
            UNIT_NBR: element.UNIT_NBR,
            AGGR_SHP_WGT: element.AGGR_SHP_WGT,
          });
          console.log(element.WAND_STOP_NBR);
        });

        this.stopsDataSource = this.stops;
        this.dataSource.data = this.stopsDataSource;
        // this.dataSource.filterPredicate = this.customFilterPredicate();
        this.isLoading = false;
      });
  }

  ngAfterContentChecked() {
    this.dataSource.sort = this.dataSource.sort;
    this.dataSource.filterPredicate = this.customFilterPredicate();
    this.ref.detectChanges();
  }

  onStopToggled(stop: StopsInterface) {
    this.selection.toggle(stop);
    // console.log(this.selection.selected);
  }

  isAllSelected() {
    return (
      // console.log(this.dataSource.filteredData),
      this.selection.selected?.length == this.dataSource.filteredData.length
    );
  }

  toggleAll() {
    if (this.isAllSelected()) {
      this.selection.clear();
    } else {
      this.selection.select(...this.dataSource.filteredData);
    }
  }

  add(event: MatChipInputEvent): void {
    const input = event.chipInput.inputElement;
    const value = event.value;

    // Add new search term
    if ((value || '').trim()) {
      this.searchItems.push({ name: value.trim() });
    }
    // Reset the input value
    if (input) {
      input.value = '';
    }
    this.searchTerms = this.searchItems.map(function (searchItem) {
      return searchItem.name;
    });
    this.globalFilter = '';
    // console.log('search terms', this.searchTerms);
  }

  remove(item: SearchItem): void {
    const index = this.searchItems.indexOf(item);
    if (index >= 0) {
      this.searchItems.splice(index, 1);
      this.searchTerms = this.searchItems.map(function (searchItem) {
        return searchItem.name;
      });
      this.dataSource.data = [...this.reset_data];
      this.dataSource.filter = JSON.stringify(this.searchTerms);
    }
  }

  applyFilter(filterValue: string) {
    console.log(filterValue);
    this.globalFilter = filterValue;
    this.dataSource.filter = JSON.stringify(this.searchTerms);
  }

  customFilterPredicate() {
    const myFilterPredicate = (
      data: StopsInterface,
      filter: string
    ): boolean => {
      var globalMatch = !this.globalFilter;

      if (this.globalFilter) {
        // search all text fields
        globalMatch =
          data.WAND_STOP_NBR.toString()
            .trim()
            .toLowerCase()
            .indexOf(this.globalFilter.toLowerCase()) !== -1 ||
          data.UNIT_NBR.toString()
            .trim()
            .toLowerCase()
            .indexOf(this.globalFilter.toLowerCase()) !== -1 ||
          data.AGGR_SHP_WGT.toString()
            .trim()
            .toLowerCase()
            .indexOf(this.globalFilter.toLowerCase()) !== -1;
      }

      if (!globalMatch) {
        return false;
      }

      let parsedSearchTerms = JSON.parse(filter);
      let isMatchToAllTerms = true;
      for (const term of parsedSearchTerms) {
        isMatchToAllTerms =
          isMatchToAllTerms &&
          (data.WAND_STOP_NBR.toString()
            .toLowerCase()
            .trim()
            .indexOf(term.toLowerCase()) !== -1 ||
            data.UNIT_NBR.toString()
              .toLowerCase()
              .trim()
              .indexOf(term.toLowerCase()) !== -1 ||
            data.AGGR_SHP_WGT.toString()
              .toLowerCase()
              .trim()
              .indexOf(term.toLowerCase()) !== -1);
      }
      return isMatchToAllTerms;
    };
    return myFilterPredicate;
  }

  announceSortChange(sortState: Sort) {
    if (sortState.direction) {
      this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
      console.log('sorting');
    } else {
      this._liveAnnouncer.announce('Sorting cleared');
    }
  }
}

DetentionValidation.component.html

<body>
  <div
    style="
      position: absolute;
      height: 80%;
      width: 80%;
      align-items: center;
      display: flex;
      justify-content: space-around;
      align-content: space-around;
    "
  >
    <mat-spinner *ngIf="isLoading">
      <span class="sr-only">Loading...</span>
    </mat-spinner>
  </div>

  <div *ngIf="!isLoading">
    <div>
      <form>
        <div>
          <h2>Detention Processing Validation</h2>
          <div style="display: flex">
            <div style="padding-right: 2%">
              <mat-form-field appearance="fill">
                <mat-label>Select Service Center</mat-label>
                <mat-select [(ngModel)]="selectedCenter" name="car">
                  <mat-option
                    *ngFor="let center of centers"
                    [value]="center.ORIG_LOC_CD"
                  >
                    {{ center.ORIG_LOC_CD }}
                  </mat-option>
                </mat-select>
              </mat-form-field>
              <p>Selected SC: {{ selectedCenter }}</p>
            </div>

            <div>
              <mat-form-field appearance="fill">
                <mat-label>Pickup/Delivery</mat-label>
                <mat-select [(ngModel)]="selectedOperation" name="operation">
                  <mat-option
                    *ngFor="let operation of operations"
                    [value]="operation.value"
                  >
                    {{ operation.viewValue }}
                  </mat-option>
                </mat-select>
              </mat-form-field>
              <p>Selected Operation: {{ selectedOperation }}</p>
            </div>
            <div class="fedex-button-row">
              <button mat-raised-button color="primary">Submit</button>
            </div>
          </div>
        </div>
      </form>
    </div>

    <!-- table div -->
    <div class="stops-table-div">
      <mat-chip-list #chipList>
        <mat-chip
          *ngFor="let item of searchItems"
          [selectable]="selectable"
          [removable]="removable"
          (removed)="remove(item)"
        >
          {{ item.name }}
          <mat-icon matChipRemove *ngIf="removable"> cancel </mat-icon>
        </mat-chip>
      </mat-chip-list>

      <mat-form-field>
        <input
          matInput
          [ngModel]="globalFilter"
          (ngModelChange)="applyFilter($event)"
          [matChipInputFor]="chipList"
          [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
          (matChipInputTokenEnd)="add($event)"
          placeholder="Search (press enter for multiple)"
          class="form-control input-md"
          appearance="fill"
        />
      </mat-form-field>

      <table
        mat-table
        class="stops-table mat-elevation-z8"
        [dataSource]="dataSource"
        matSort
        (matSortChange)="announceSortChange($event)"
      >
        <ng-container matColumnDef="WAND_STOP_NBR">
          <th
            mat-header-cell
            *matHeaderCellDef
            mat-sort-header
            sortActionDescription="Sort by WAND_STOP_NBR"
          >
            Stop Nbr
          </th>
          <td mat-cell *matCellDef="let stop">{{ stop.WAND_STOP_NBR }}</td>
        </ng-container>

        <ng-container matColumnDef="UNIT_NBR">
          <th
            mat-header-cell
            *matHeaderCellDef
            mat-sort-header
            sortActionDescription="Sort by UNIT_NBR"
          >
            Unit Nbr
          </th>
          <td mat-cell *matCellDef="let stop">{{ stop.UNIT_NBR }}</td>
        </ng-container>

        <ng-container matColumnDef="AGGR_SHP_WGT">
          <th
            mat-header-cell
            *matHeaderCellDef
            mat-sort-header
            sortActionDescription="Sort by AGGR_SHP_WGT"
          >
            Weight
          </th>
          <td mat-cell *matCellDef="let stop">{{ stop.AGGR_SHP_WGT }}</td>
        </ng-container>

        <ng-container matColumnDef="select">
          <th mat-header-cell *matHeaderCellDef>
            <mat-checkbox
              color="primary"
              [checked]="selection.hasValue() && isAllSelected()"
              (change)="toggleAll()"
              [indeterminate]="selection.hasValue() && !isAllSelected()"
            >
            </mat-checkbox>
          </th>
          <td mat-cell *matCellDef="let stop">
            <mat-checkbox
              color="primary"
              (change)="onStopToggled(stop)"
              [checked]="selection.isSelected(stop)"
            >
            </mat-checkbox>
          </td>
        </ng-container>

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

        <tr mat-row *matRowDef="let stop; columns: displayedColumns"></tr>
      </table>
    </div>
  </div>
</body>

Upvotes: 1

Views: 2782

Answers (1)

Luca Angrisani
Luca Angrisani

Reputation: 399

From https://angular.io/errors/NG0100

If the issue exists within ngAfterViewInit, the recommended solution is to use a constructor or ngOnInit to set initial values, or use ngAfterContentInit for other value bindings.

So, you can try to

[...]
export class DetentionValidationComponent implements AfterContentInit {
   [...]
   ngAfterContentInit () {
      //insert content of your ngOnInit
   }

In every case, this is just a dev error, in production there is no additional check and no errors throws

Upvotes: 3

Related Questions