Search code examples
angularangular-materialangular-cdk

matSort is not working with cdk-virtual-scroll-viewport


I have used below code to render data in table while scrolling using cdk-virtual-scroll-viewport in Angular application. Material sorting feature has also been implemented using matSort. Please refer below code.

table.component.html:

    <ng-container matColumnDef="position">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> No. </th>
      <td mat-cell *matCellDef="let element"> {{element.position}} </td>
    </ng-container>

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

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

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

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <ng-template let-row matRowDef cdkVirtualFor [matRowDefColumns]="displayedColumns" [cdkVirtualForOf]="rows">
      <tr mat-row></tr>
    </ng-template>
  </table>
</cdk-virtual-scroll-viewport>

table.component.ts:

import { Component, OnInit, ViewChild, Inject } from '@angular/core';
import { VIRTUAL_SCROLL_STRATEGY } from "@angular/cdk/scrolling";
import { Observable, of, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import {MatSort, MatTableDataSource} from '@angular/material';
import { TableVirtualScrollStrategy } from './table-vs-strategy.service';

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'},
  {position: 11, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
  {position: 12, name: 'Helium', weight: 4.0026, symbol: 'He'},
  {position: 13, name: 'Lithium', weight: 6.941, symbol: 'Li'},
  {position: 14, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
  {position: 15, name: 'Boron', weight: 10.811, symbol: 'B'},
  {position: 16, name: 'Carbon', weight: 12.0107, symbol: 'C'},
  {position: 17, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
  {position: 18, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
  {position: 19, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
  {position: 20, name: 'Neon', weight: 20.1797, symbol: 'Ne'}
];
@Component({
  selector: 'app-table',
  templateUrl: 'table.component.html',
  providers: [{
    provide: VIRTUAL_SCROLL_STRATEGY,
    useClass: TableVirtualScrollStrategy,
  }],
})
export class TableComponent implements OnInit {

  // Manually set the amount of buffer and the height of the table elements
  static BUFFER_SIZE = 3;
  rowHeight = 48;
  headerHeight = 56;

  rows: Observable<Array<any>> = of(ELEMENT_DATA);

  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];

  dataSource = new MatTableDataSource(ELEMENT_DATA);

  gridHeight = 400;
@ViewChild(MatSort) sort: MatSort;

  constructor(@Inject(VIRTUAL_SCROLL_STRATEGY) private readonly scrollStrategy: TableVirtualScrollStrategy) {}

  public ngOnInit() {
    const range = Math.ceil(this.gridHeight / this.rowHeight) + TableComponent.BUFFER_SIZE;
    this.scrollStrategy.setScrollHeight(this.rowHeight, this.headerHeight);

    this.dataSource = combineLatest([this.rows, this.scrollStrategy.scrolledIndexChange]).pipe(
      map((value: any) => {

        // Determine the start and end rendered range
        const start = Math.max(0, value[1] - TableComponent.BUFFER_SIZE);
        const end = Math.min(value[0].length, value[1] + range);

        // Update the datasource for the rendered range of data
        return value[0].slice(start, end);
      })

    );
    this.dataSource.sort = this.sort;
  }
}

Material sorting has not been working with virtual scroll. How to solve the issue?


Solution

  • Actually you have two options here:

    • handle (matSortChange) event like in the first example of Angular Material documentation

    • since MatSort directive already exposes observables to handle changes you can get access to MatSort instance like in the second example and handle events from MatSort in your data source:

    component.ts

    import { MatSort } from '@angular/material';
    
    @ViewChild(MatSort, {static: true}) sort: MatSort;
    

    If you would use MatTableDataSource(again like in the second example in doc) then you should only pass that instance there:

    dataSource = new MatTableDataSource(ELEMENT_DATA);
    
    @ViewChild(MatSort, {static: true}) sort: MatSort;
    
    ngOnInit() {
      this.dataSource.sort = this.sort;
    }
    

    Then MatTableDataSource will take care of all the magic under the hood.

    But since in your case you use Observable then you need to handle sort changes by yourself, e.g like this:

    public ngOnInit() {
      ...    
      const sortChange = merge(this.sort.sortChange, this.sort.initialized);
    
      this.dataSource = combineLatest([this.rows, this.scrollStrategy.scrolledIndexChange, sortChange]).pipe(
        map(([rows, index, sort]: [any[], number, Sort|void]) => {
    
          if (sort) {
            rows = this.sortData(rows.slice(), sort);
          }
    
          // Determine the start and end rendered range
          const start = Math.max(0, index - AppComponent.BUFFER_SIZE);
          const end = Math.min(rows.length, index + range);
    
          // Update the datasource for the rendered range of data
          return rows.slice(start, end);
        })
      );
    
    }
    
      /**
     * Your implementation here
     */
    sortData(data: any[], sort: Sort) {
      console.log(sort)
      return data.sort((a, b) => {
        return sort.direction === 'asc' ? a[sort.active] - b[sort.active] : b[sort.active] - a[sort.active]
      });
    }
    

    Stackblitz Example

    To keep it consistent you can create your own custom data source or maybe even reuse MatTableDataSource that already handles all the cases.

    For example, here's an idea of how you can reuse MatTableDataSource:

    this.subscription = combineLatest([this.rows, this.scrollStrategy.scrolledIndexChange]).pipe(
      tap(([rows, index]: [any[], number]) => {
        // Determine the start and end rendered range
        const start = Math.max(0, index - AppComponent.BUFFER_SIZE);
        const end = Math.min(rows.length, index + range);
    
        // Update the datasource for the rendered range of data
        const data = rows.slice(start, end);
    
        this.dataSource = new MatTableDataSource(data);
        this.dataSource.sort = this.sort;
      })
    ).subscribe();
    

    Stackblitz Example