You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular Material表格第二页及以后排序失效问题咨询

Angular Material表格分页加载时第二页排序失效问题

我正在使用Angular Material表格,采用分页单独加载数据的方式(例如第一页加载50条数据)。第一页的排序功能正常,但从第二页开始,点击任意列的排序按钮后,所有行都会消失,具体表现为显示matNoDataRow无数据行。

请问分页单独加载数据的方式是否是导致第二页及以后排序失效的原因?


HTML代码详情

<div class="mat-elevation-z8">
    <mat-progress-bar mode="indeterminate" *ngIf="isLoading"></mat-progress-bar>
    <table mat-table [dataSource]="dataSource" matSort>

        <!-- Title Column -->
        <ng-container matColumnDef="title">
            <th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by title">
                {{'createevent.clone.eventitle' | translate}}</th>
            <td mat-cell *matCellDef="let element" class="max-w-prose truncate pr-2" [matTooltip]="element.title"> <span
                    class="mobile-label mr-15">{{'createevent.clone.eventitle' | translate}}: </span>
                <span>{{element.title}}</span>
            </td>
        </ng-container>

        <!-- Location Column -->
        <ng-container matColumnDef="fullLocation">
            <th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by location">
                {{'createevent.clone.location' | translate}} </th>
            <td mat-cell *matCellDef="let element" class="max-w-prose truncate pr-2"
                [matTooltip]="element.fullLocation">
                <span class="mobile-label mr-15">{{'createevent.clone.location' | translate}}: </span>
                <span>{{element.fullLocation}}</span>
            </td>
        </ng-container>

        <!-- Date Created Column -->
        <ng-container matColumnDef="createdDate">
            <th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by Date Created">
                {{'createevent.clone.datecreated' | translate}} </th>
            <td mat-cell *matCellDef="let element"> <span class="mobile-label mr-15">{{'createevent.clone.datecreated' |
                    translate}}: </span> <span>{{element.createdDate | date: 'dd/MM/yyyy'}}</span> </td>
        </ng-container>

        <!-- Start Date Column -->
        <ng-container matColumnDef="startDate">
            <th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by Start Date">
                {{'createevent.clone.startdate' | translate}} </th>
            <td mat-cell *matCellDef="let element"> <span class="mobile-label mr-15">{{'createevent.clone.startdate' |
                    translate}}: </span> <span>{{element.startDate | date: 'dd/MM/yyyy'}}</span> </td>
        </ng-container>

        <!-- Button Column -->
        <ng-container matColumnDef="action">
            <th mat-header-cell *matHeaderCellDef> Action </th>
            <td mat-cell *matCellDef="let row"> <button mat-button type="button" color="primary"
                    (click)="cloneEvent(row.id)">
                    <mat-icon>content_copy</mat-icon> <span
                        class="hidden tablet:inline-block">{{'createevent.clone.button' |
                        translate}}</span>
                </button> </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

        <!-- Row shown when there is no matching data. -->
        <tr class="mat-row" *matNoDataRow>
            <td class="mat-cell" *ngIf="!isLoading" colspan="4">No data matching the filter "{{searchInput.value}}"</td>
        </tr>
    </table>

    <mat-paginator #paginator [pageSize]="[query.pageSize]" [pageSizeOptions]="[5, 10, 25, 50]"
        [pageIndex]="query.pageNumber - 1" (page)="updateTable($event)" showFirstLastButtons aria-label="Event List">
    </mat-paginator>
</div>

初始化变量

query: EventSearchQuery = {
  organizerId: 0,
  eventVersion: 2,
  searchText: '',
  pageSize: 5,
  pageNumber: 1
};

@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
displayedColumns: string[] = ['title', 'fullLocation', 'createdDate', 'startDate', 'action'];
dataSource = new MatTableDataSource();

TypeScript代码详情

getEvents() {
  this.isLoading = true;

  // get all organizer events and initialize table
  this.newEventService.getOrganizerEvents(this.query).subscribe(res => {
    // map location name to fullLocation for easier data manipulation
    const events = res.map((event) => {
      const fullLocation = event.location.name;
      return { ...event, fullLocation };
    })

    //initialize table headers
    this.dataSource = new MatTableDataSource<any>(events);

    //initialize pagination
    setTimeout(() => {
      this.paginator.pageIndex = this.query.pageNumber - 1;
      this.paginator.pageSize = this.query.pageSize;
      this.paginator.length = this.filteredEventsCount;
    });
    this.dataSource.paginator = this.paginator;

    // initialize sorting
    this.dataSource.sort = this.sort;

    // filters title and location
    this.dataSource.filterPredicate = (data: OrganizersEvent, filter: string) => {

      return (data.title.trim().toLowerCase().indexOf(filter) !== -1 ||
        data.fullLocation.trim().toLowerCase().indexOf(filter) !== -1);
    };
    this.isLoading = false;
  }, error => {
    console.log(error);
    this.isLoading = false;
  })
}

updateTable(event ?: PageEvent) {
  this.query.pageNumber = event.pageIndex + 1;
  this.query.pageSize = event.pageSize;
  this.getEvents();
  return event;
}

问题分析与解决方案

分页单独加载数据本身不是问题根源,问题出在排序逻辑的处理上:当在第二页点击排序时,MatSort会尝试对当前DataSource里的局部数据(第二页的5条)排序,但你的实现既没把排序参数传给后端,又每次请求都重建DataSource,导致排序状态混乱,最终前端处理后数据集为空,显示无数据行。

具体问题点

  1. 查询参数缺少排序字段:当前query没有携带排序字段和方向,后端返回的还是默认排序的数据,前端局部排序逻辑和分页加载的逻辑冲突。
  2. 重复创建MatTableDataSource:每次getEvents()都新建DataSource,之前绑定的sort、paginator状态会丢失,即使重新赋值也会导致逻辑混乱。
  3. 分页器初始化时机不合理:用setTimeout设置分页参数,可能导致分页器和DataSource的绑定时机不一致,引发显示异常。

修复步骤

  1. 扩展查询参数,加入排序字段
    修改EventSearchQuery接口,新增排序相关字段:

    interface EventSearchQuery {
      organizerId: number;
      eventVersion: number;
      searchText: string;
      pageSize: number;
      pageNumber: number;
      sortField?: string; // 排序字段
      sortDirection?: 'asc' | 'desc'; // 排序方向
    }
    
  2. 监听排序事件,更新查询参数
    在组件初始化阶段监听排序变化,同步更新query并重新请求数据:

    ngAfterViewInit() {
      this.sort.sortChange.subscribe((sortState: Sort) => {
        this.query.sortField = sortState.active;
        this.query.sortDirection = sortState.direction;
        // 排序时重置到第一页
        this.query.pageNumber = 1;
        this.getEvents();
      });
    }
    
  3. 避免重复创建DataSource
    只在初始化时创建一次DataSource,后续直接更新数据即可:

    // 初始化时创建一次
    dataSource = new MatTableDataSource<any>();
    
    getEvents() {
      this.isLoading = true;
      this.newEventService.getOrganizerEvents(this.query).subscribe(res => {
        const events = res.map((event) => {
          const fullLocation = event.location.name;
          return { ...event, fullLocation };
        });
        // 直接更新数据,不重建DataSource
        this.dataSource.data = events;
        
        // 直接设置分页器参数(确保paginator已初始化)
        this.paginator.pageIndex = this.query.pageNumber - 1;
        this.paginator.pageSize = this.query.pageSize;
        this.paginator.length = this.filteredEventsCount;
        
        // filterPredicate只在初始化时设置一次即可
        this.isLoading = false;
      }, error => {
        console.log(error);
        this.isLoading = false;
      });
    }
    
  4. 修正分页器绑定
    移除[pageSize]的数组包裹,直接绑定数值:

    <mat-paginator #paginator 
                   [pageSize]="query.pageSize" 
                   [pageSizeOptions]="[5, 10, 25, 50]"
                   [pageIndex]="query.pageNumber - 1" 
                   (page)="updateTable($event)" 
                   showFirstLastButtons 
                   aria-label="Event List">
    </mat-paginator>
    
  5. 后端适配排序参数
    确保后端接口能接收sortField和sortDirection参数,返回对应排序后的分页数据。

修改后,点击排序按钮时会把排序参数传给后端,重新请求排序后的第一页数据,既保持分页加载的逻辑,又解决了第二页排序无数据的问题。

内容的提问来源于stack exchange,提问作者Tahsin Ahmed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 00:45:34