angularsortingangular-materialangular-material-table

Angular Material Table - Sort arrows not displaying correctly in mat-sort-header


I'm working on an Angular project where I'm using Angular Material's mat-table with sorting enabled. The sorting functionality works, but the arrows in the column headers are not displaying correctly when I change the sort with ts code.

HTML:

<div class="cpage">
  <div class="left-margin"></div>
  <div class="left">
    <form-card header="Left" [enableBottomPadding]="true">
      <button mat-raised-button (click)="actionB1()">B1</button>
    </form-card>
  </div>
  <div class="gap-between"></div>
  <div class="right">
    <form-card header="DataTable" [enableBottomPadding]="true">
      <div style="width: 100%; max-height:90vh; overflow-x:auto">
        <mat-table [dataSource]="dataSource" matSort #sort1="matSort">
          <ng-container matColumnDef="position">
            <mat-header-cell mat-sort-header *matHeaderCellDef> No.</mat-header-cell>
            <mat-cell *matCellDef="let element"> {{ element.position }}</mat-cell>
          </ng-container>

          <ng-container matColumnDef="name">
            <mat-header-cell mat-sort-header *matHeaderCellDef> Name</mat-header-cell>
            <mat-cell *matCellDef="let element"> {{ element.name }}</mat-cell>
          </ng-container>

          <ng-container matColumnDef="weight">
            <mat-header-cell mat-sort-header *matHeaderCellDef> Weight</mat-header-cell>
            <mat-cell *matCellDef="let element"> {{ element.weight }}</mat-cell>
          </ng-container>

          <!-- additional weight columns -->
          <ng-container matColumnDef="weight1">
            <mat-header-cell *matHeaderCellDef> Weight</mat-header-cell>
            <mat-cell *matCellDef="let element"> {{ element.weight }}</mat-cell>
          </ng-container>

          <ng-container matColumnDef="symbol">
            <mat-header-cell *matHeaderCellDef> Symbol</mat-header-cell>
            <mat-cell *matCellDef="let element"> {{ element.symbol }}</mat-cell>
          </ng-container>

          <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
          <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
        </mat-table>
      </div>
    </form-card>
  </div>
  <div class="right-margin"></div>
</div>

TS:

import { AfterViewInit, ChangeDetectorRef, Component, ViewChild } from '@angular/core';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from "@angular/material/table";

export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

const ELEMENT_DATA: PeriodicElement[] = [
  { position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' },
  { position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' },
  { position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' },
  { position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' },
  { position: 5, name: 'Boron', weight: 10.811, symbol: 'B' },
  { position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' },
  { position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' },
  { position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' },
  { position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' },
  { position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' },
];

@Component({
  selector: 'app-debug-table',
  templateUrl: './debug-table.component.html',
  styleUrls: ['./debug-table.component.scss']
})
export class DebugTableComponent implements AfterViewInit {
  displayedColumns: string[] = ['position', 'name', 'weight', 'weight1', 'symbol'];
  dataSource = new MatTableDataSource(ELEMENT_DATA);
  @ViewChild('sort1', { static: false }) sort1: MatSort;

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.dataSource.sort = this.sort1;
  }

  actionB1() {
    if (this.sort1.active == 'name') {
      this.sort1.active = 'weight';
    } else {
      this.sort1.active = 'name';
    }
    this.dataSource.sort = this.sort1;
    this.cdr.detectChanges();
  }
}

Problem: Sorting works, but the sort arrows next to the column headers don't update correctly. Clicking the "B1" button toggles the sort between "name" and "weight," but the arrows on the mat-sort-header headers don't appear to follow this toggle.

What I've tried:

Verified that I have the Angular Material sort module imported. Checked that matSort and mat-sort-header are added properly to mat-table and header cells. Called detectChanges after updating the sort to try and refresh the view. Has anyone encountered this issue before? Any ideas on how to get the arrows to display correctly?

Angular: 16 Angular Material: 16


Solution

  • You can use the sort method of dataSource.sort which takes the interface input of MatSortable.

    export declare interface MatSortable {
        /** The id of the column being sorted. */
        id: string;
        /** Starting sort direction. */
        start: SortDirection;
        /** Whether to disable clearing the sorting state. */
        disableClear: boolean;
    }
    
    export declare type SortDirection = 'asc' | 'desc' | '';
    

    Then we can use the method like below.

    actionB1() {
      let active = null;
      if (this.sort1.active == 'name') {
        active = 'weight';
      } else {
        active = 'name';
      }
      this.dataSource.sort!.sort({
        id: active,
        start: 'asc',
        disableClear: true,
      });
      // this.cdr.detectChanges();
    }
    

    Full Code:

    HTML:

    <button mat-raised-button (click)="actionB1()">B1</button>
    <table
      mat-table
      matSort
      [dataSource]="dataSource"
      class="mat-elevation-z8"
      (matSortChange)="sortData($event)"
    >
      <!--- Note that these columns can be defined in any order.
            The actual rendered columns are set as a property on the row definition" -->
    
      <!-- Position Column -->
      <ng-container matColumnDef="position">
        <th mat-header-cell *matHeaderCellDef mat-sort-header="position">No.</th>
        <td mat-cell *matCellDef="let element">{{element.position}}</td>
      </ng-container>
    
      <!-- Name Column -->
      <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef mat-sort-header="name">Name</th>
        <td mat-cell *matCellDef="let element">{{element.name}}</td>
      </ng-container>
    
      <!-- Weight Column -->
      <ng-container matColumnDef="weight">
        <th mat-header-cell *matHeaderCellDef mat-sort-header="weight">Weight</th>
        <td mat-cell *matCellDef="let element">{{element.weight}}</td>
      </ng-container>
    
      <!-- Symbol Column -->
      <ng-container matColumnDef="symbol">
        <th mat-header-cell *matHeaderCellDef mat-sort-header="symbol">Symbol</th>
        <td mat-cell *matCellDef="let element">{{element.symbol}}</td>
      </ng-container>
    
      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
    

    TS:

    import { Component, ViewChild, ChangeDetectorRef, inject } from '@angular/core';
    import { MatTableDataSource, MatTableModule } from '@angular/material/table';
    import { MatSort } from '@angular/material/sort';
    import { MatSortModule, Sort } from '@angular/material/sort';
    
    export interface PeriodicElement {
      name: string;
      position: number;
      weight: number;
      symbol: string;
    }
    
    const ELEMENT_DATA: PeriodicElement[] = [
      { position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' },
      { position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' },
      { position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' },
      { position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' },
      { position: 5, name: 'Boron', weight: 10.811, symbol: 'B' },
      { position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' },
      { position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' },
      { position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' },
      { position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' },
      { position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' },
    ];
    
    function compare(a: number | string, b: number | string, isAsc: boolean) {
      return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
    }
    
    /**
     * @title Basic use of `<table mat-table>`
     */
    @Component({
      selector: 'table-basic-example',
      styleUrl: 'table-basic-example.css',
      templateUrl: 'table-basic-example.html',
      standalone: true,
      imports: [MatTableModule, MatSortModule],
    })
    export class TableBasicExample {
      displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
      dataSource = new MatTableDataSource(ELEMENT_DATA);
      cdr = inject(ChangeDetectorRef);
      @ViewChild(MatSort, { static: false }) sort1: MatSort;
    
      ngAfterViewInit() {
        this.dataSource.sort = this.sort1;
      }
    
      actionB1() {
        let active = null;
        if (this.sort1.active == 'name') {
          active = 'weight';
        } else {
          active = 'name';
        }
        this.dataSource.sort!.sort({
          id: active,
          start: 'asc',
          disableClear: true,
        });
        // this.cdr.detectChanges();
      }
    
      sortData(sort: Sort) {
        const data = this.dataSource.data.slice();
        if (!sort.active || sort.direction === '') {
          this.dataSource.data = data;
          return;
        }
    
        this.dataSource.data = data.sort((a, b) => {
          const isAsc = sort.direction === 'asc';
          switch (sort.active) {
            case 'name':
              return compare(a.name, b.name, isAsc);
            case 'weight':
              return compare(a.weight, b.weight, isAsc);
            case 'symbol':
              return compare(a.symbol, b.symbol, isAsc);
            default:
              return 0;
          }
        });
      }
    }
    

    Stackblitz Demo