简体   繁体   中英

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?

Try to add a new ViewChild setter which sets sort when it exists.

@ViewChild(MatSort)set matSortSetter(value: MatSort) {
  if(this.dataSource && value) {
    this.dataSource.sort = value;
  }
}

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, eg 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM