1

Question: Is there a way to programatically display the sorting indicators (the little arrows) in an angular material table without triggering the sorting event?

I have a page with a mat-table that allows for pagination, filtering and sorting. (Filtering logic omitted from example code because not relevant to the problem.) I want to save the state of the pagination/filter/sort when navigating away from that page and restore it when the user returns to the page. If the user clicks on the column header (mat-sort-header) to sort the data, the pagination resets to first page.
My problem: When I restore the table state, the code I use to make the up/down sort arrows appear involves triggering the same sort logic that is invoked when the user clicks on a header to sort. This, therefore, resets the page index back to 1 - overriding the index I was restoring.

So, is there a way to get the up/down arrow indicators to display without triggering the sort logic? Here is the code I am using. I have tried setting the active and direction properties on the MatSort without invoking the sortChange emitter and invoking the sortChange emitter without setting the properties but in both cases the arrow did not appear.:

      this.matSort.active = this.sort.active;
      this.matSort.direction = this.sort.direction;
      this.matSort.sortChange.emit(this.sort);

(I've also tried using ViewChildren to get at the relevant MatSortHeader to see if I can toggle its state somehow. I tried calling the _renderArrow() function on the header but that did not cause the arrow to display.)

I've included the full example source code below. I've tried to trim it down to just the relevant parts. (I've gotten around the problem by having a variable to decide whether to reset the page index when sorting but this seems like a really bad hack...)

Thank you for reading my question.

import {AfterViewInit, Component, OnDestroy, ViewChild} from "@angular/core";
import {MatSort, Sort} from "@angular/material/sort";
import {Observable, of, tap} from "rxjs";
import {catchError, delay, map, startWith, switchMap} from "rxjs/operators";
import {MatPaginator} from "@angular/material/paginator";

@Component({
  template: `
    <div class="mat-elevation-z8">
      <div>
        <table [dataSource]="dataSource" mat-table matSort (matSortChange)="sorted($event)">
          <ng-container matColumnDef="name">
            <th *matHeaderCellDef mat-header-cell mat-sort-header>Name</th>
            <td *matCellDef="let row" mat-cell>{{row.name}}</td>
          </ng-container>

          <ng-container matColumnDef="abbrev">
            <th *matHeaderCellDef mat-header-cell mat-sort-header>Abbreviation</th>
            <td *matCellDef="let row" mat-cell>{{row.abbrev}}</td>
          </ng-container>

          <tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
          <tr *matRowDef="let row; let i = index; columns: displayedColumns;" mat-row></tr>
        </table>
        <div *ngIf="dataSource.length === 0 && !isLoadingResults" class="text-center mt-3 mb-2">No Data Found</div>
      </div>

      <mat-paginator [color]="'accent'" [length]="resultsLength" [pageSize]="pageSize"
                     [showFirstLastButtons]="true"></mat-paginator>
     </div>
  `
})
export class MyComponent implements AfterViewInit, OnDestroy {
  resultsLength = 0;
  readonly pageSize = 5;
  displayedColumns: string[] = ['name', 'abbrev'];
  dataSource: any[] = [];
  isLoadingResults = true;
  sort: Sort = {active: 'abbrev', direction: 'asc'};  //default sort
  resetPageIndexOnSort: boolean = true;

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

  ngOnDestroy() {
    this.saveTableState();
  }

  ngAfterViewInit() {
    console.debug("START ngAfterViewInit");
    this.loadTableState();
    this.initializeTableSort();

    this.paginator.page
      .pipe(
        tap(() => { console.error("paginator page event")}),
        startWith({}),
        switchMap(() => {
          this.isLoadingResults = true;
          return this.queryData();
        }),
        map(data => {
          // flip flag to show that loading has finished.
          this.isLoadingResults = false;
          this.resultsLength = data.count;
          return data.collection;
        }),
        catchError(() => {
          this.isLoadingResults = false;
          return of([]);
        })
      ).subscribe(data => this.dataSource = data);

    console.debug("END ngAfterViewInit");
  }

  initializeTableSort() {
    setTimeout(() => {
      //make sure table shows sort state - not sure why all these are needed
      this.matSort.active = this.sort.active;
      this.matSort.direction = this.sort.direction;
      this.matSort.sortChange.emit(this.sort);
    });
  }

  loadTableState() {
    const stored: string = sessionStorage.getItem(STORAGE_KEY);
    if(stored) {
      try {
        const criteria = JSON.parse(stored);
        this.paginator.pageIndex = criteria.pageIndex;
        this.sort = criteria.sort;
        this.resetPageIndexOnSort = false;
      } catch (error) {
        console.error("could not parse saved criteria", error);
      }
    }
  }

  saveTableState() {
    const criteria: any = {
      pageIndex: this.paginator.pageIndex,
      sort: this.sort
    };

    sessionStorage.setItem(STORAGE_KEY, JSON.stringify(criteria));
  }

  sorted(sortState: Sort) {
    console.debug("Sort event");
    if(sortState.direction) {
      this.sort = sortState;
    } else {
      this.sort = null;
    }

    of({}).pipe(
      delay(0), //needed to prevent error from changing loading variable
      switchMap( () => {
        this.isLoadingResults = true;
        if(this.resetPageIndexOnSort) {
          //if the user sorts the data, return to first page of results
          this.paginator.pageIndex = 0;
        } else {
          //dont reset the index, because we are loading the index from storage
          //however, reset the flag because future sorts will be from user
          //clicking on the column header
          this.resetPageIndexOnSort = true;
        }
        return this.queryData();
      }),
      map(data => {
        // Flip flag to show that loading has finished.
        this.isLoadingResults = false;
        this.resultsLength = data.count;
        return data.collection;
      }),
      catchError( error => {
        this.isLoadingResults = false;
        return of([]);
      })
    ).subscribe(data => this.dataSource = data);
  }


  queryData(): Observable<QueryResult<any>> {
    //mimic fetching data from the server
    const pageSize = this.paginator.pageSize;
    const pageIndex = this.paginator.pageIndex;

    let startIndex = pageSize * pageIndex;
    let endIndex = startIndex + pageSize;
    startIndex = Math.max(0, startIndex);
    endIndex = Math.min(DATA.length, endIndex);

    const data = DATA.slice(startIndex, endIndex);

    const queryResult: QueryResult<any> = {
      collection: data,
      count: DATA.length
    }

    return of(queryResult);
  }

}

interface QueryResult<T> {
  collection: T[];
  count: number;
}

const STORAGE_KEY = "table_saved_state";

const DATA: any[] = [
  { name: 'Alabama', abbrev: 'AL' }, { name: 'Alaska', abbrev: 'AK' }, { name: 'Arizona', abbrev: 'AZ' },
... most records removed to save space ...
  { name: 'Washington', abbrev: 'WA' }, { name: 'West Virginia', abbrev: 'WV' }, { name: 'Wisconsin', abbrev: 'WI' },
  { name: 'Wyoming', abbrev: 'WY' }
];
Scott Turnquist
  • 135
  • 3
  • 8
  • You could simply include the icons in the header by youself. Unrelated tipp: [color]="'accent'" should be just color="accent" – MoxxiManagarm Aug 18 '23 at 07:08

0 Answers0