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

Angular 18 Material表格排序功能失效无报错问题

Angular 18 Material表格排序功能失效排查与修复

问题描述

在Angular 18 Material中实现表格排序功能,代码逻辑看似正确,但点击列头时排序完全不生效。即使注释掉过滤功能,排序仍无法正常工作。

相关代码

users.component.html

<div class="example-container mat-elevation-z8">
  <div class="example-header">
    <mat-form-field>
      <input matInput (keyup)="applyFilter(getInputValue($event))" placeholder="Search">
    </mat-form-field>
  </div>

  <mat-table [dataSource]="dataSource" matSort>

    <!-- Id Column -->
    <ng-container matColumnDef="id">
      <mat-header-cell *matHeaderCellDef mat-sort-header> Id </mat-header-cell>
      <mat-cell *matCellDef="let user"> {{user.id}} </mat-cell>
    </ng-container>

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

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;" (click)="showUserDialog(row)"></mat-row>
  </mat-table>
</div>

users.component.ts

import {Component, OnInit, ViewChild, Input} from '@angular/core';
import {UserDetailComponent} from "../user-detail/user-detail.component";
import { MatTableDataSource } from '@angular/material/table';
import {MatDialog} from "@angular/material/dialog";
import {MatSort, Sort} from '@angular/material/sort';
import { User } from '../models/user.model';
import { UserService } from '../services/user.service';
import {ActivatedRoute, Router} from '@angular/router';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
  styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
  displayedColumns: string[] = ['id', 'name'];
  dataSource: MatTableDataSource<User>;
  users: User[] = [];

  constructor(private userService: UserService,
              private dialog: MatDialog,
              private route: ActivatedRoute,
              private router: Router) {
    this.dataSource = new MatTableDataSource<User>();
  }

  @ViewChild(MatSort) sort!: MatSort;

  @Input() filter!: User;

  ngOnInit() {
    this.userService.getUsers().subscribe((data: User[]) => {
      this.users = data;
      this.dataSource.data = this.users;
      this.dataSource.sort = this.sort;
    });
    this.dataSource.filterPredicate = (data, filter: string): boolean => {
      return data.name.toLowerCase().includes(filter) || data.id.toString() === filter;
    };
  }

  getInputValue(event: Event): string {
    const target = event.target as HTMLInputElement;
    return target ? target.value : '';
  }

  applyFilter(filterValue: string) {
    filterValue = filterValue.trim().toLowerCase();
    this.dataSource.filter = filterValue;
    // Update the URL with the new filter value
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: { find: filterValue },
      queryParamsHandling: 'merge'
    });
  }

  showUserDialog(user: User): void {
    this.dialog.open(UserDetailComponent, {
      width: '500px',
      data: { user: user }
    });
  }
}

main.ts

import {CdkTableModule} from '@angular/cdk/table';
import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {bootstrapApplication, BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {MatButtonModule} from "@angular/material/button";
import {MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatCardModule} from "@angular/material/card";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatChipsModule} from "@angular/material/chips";
import {MatStepperModule} from "@angular/material/stepper";
import {MatDatepickerModule} from "@angular/material/datepicker";
import {MatDialogModule} from "@angular/material/dialog";
import {MatDividerModule} from "@angular/material/divider";
import {MatExpansionModule} from "@angular/material/expansion";
import {MatGridListModule} from "@angular/material/grid-list";
import {MatIconModule} from "@angular/material/icon";
import {MatInputModule} from "@angular/material/input";
import {MatListModule} from "@angular/material/list";
import {MatMenuModule} from "@angular/material/menu";
import {MatNativeDateModule, MatRippleModule} from "@angular/material/core";
import {MatPaginatorModule} from "@angular/material/paginator";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MatRadioModule} from "@angular/material/radio";
import {MatSelectModule} from "@angular/material/select";
import {MatSidenavModule} from "@angular/material/sidenav";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatSnackBarModule} from "@angular/material/snack-bar";
import {MatTabsModule} from "@angular/material/tabs";
import {MatToolbarModule} from "@angular/material/toolbar";
import {MatTooltipModule} from "@angular/material/tooltip";
import {MatTableModule} from '@angular/material/table';
import {provideHttpClient} from "@angular/common/http";
import {RouterModule} from '@angular/router';
import {AppComponent} from "./app/app.component";
import {appConfig} from "./app/app.config";
import {UsersComponent} from "./app/users/users.component";
import {CarsComponent} from "./app/cars/cars.component";
import {UserDetailComponent} from "./app/user-detail/user-detail.component";
import {CarDetailComponent} from "./app/car-detail/car-detail.component";
import { routes } from './app/app.routes';
import {MatSortModule} from "@angular/material/sort";

@NgModule({
  exports: [
    CdkTableModule,
    MatAutocompleteModule,
    MatButtonModule,
    MatButtonToggleModule,
    MatCardModule,
    MatCheckboxModule,
    MatChipsModule,
    MatStepperModule,
    MatDatepickerModule,
    MatDialogModule,
    MatDividerModule,
    MatExpansionModule,
    MatGridListModule,
    MatIconModule,
    MatInputModule,
    MatListModule,
    MatMenuModule,
    MatNativeDateModule,
    MatPaginatorModule,
    MatProgressBarModule,
    MatProgressSpinnerModule,
    MatRadioModule,
    MatRippleModule,
    MatSelectModule,
    MatSidenavModule,
    MatSliderModule,
    MatSlideToggleModule,
    MatSnackBarModule,
    MatTableModule,
    MatTabsModule,
    MatToolbarModule,
    MatTooltipModule,
    MatDialogModule,
    MatGridListModule,
    MatSortModule
  ]
})
export class DemoMaterialModule {}

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    DemoMaterialModule,
    MatNativeDateModule,
    ReactiveFormsModule,
    RouterModule.forRoot(routes)
  ],
  declarations: [UsersComponent, UserDetailComponent, CarsComponent, CarDetailComponent],
  bootstrap: [],
  providers: [provideHttpClient()]
})
export class AppModule {}

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

user.service.ts

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { User } from '../models/user.model';
import {map} from "rxjs/operators";

@Injectable({
  providedIn: 'root'
})
export class UserService {

  private users: User[] = [
    {
      "id": 1,
      "name": "Tim Johnson",
      "cars": [
        {
          "id": 1,
          "make": "BMW",
          "model": "3 Series",
          "numberplate": "AB12 CDE"
        },
        {
          "id": 2,
          "make": "Kia",
          "model": "Sorento",
          "numberplate": "DE34 FGH"
        }
      ]
    },
    {
      "id": 2,
      "name": "Patricia Reed",
      "cars": [
        {
          "id": 3,
          "make": "Peugeot",
          "model": "308",
          "numberplate": "HI56 JKL"
        },
        {
          "id": 4,
          "make": "Kia",
          "model": "Sorento",
          "numberplate": "MN78 PQR"
        }
      ]
    },
    {
      "id": 3,
      "name": "Matthew Hall",
      "cars": [
        {
          "id": 5,
          "make": "Skoda",
          "model": "Octavia",
          "numberplate": "ST90 UVW"
        },
        {
          "id": 6,
          "make": "Audi",
          "model": "A6",
          "numberplate": "XY12 ZAB"
        }
      ]
    },
    {
      "id": 4,
      "name": "Karen Smith",
      "cars": [
        {
          "id": 7,
          "make": "Renault",
          "model": "Clio",
          "numberplate": "CD34 EFG"
        },
        {
          "id": 8,
          "make": "Audi",
          "model": "A6",
          "numberplate": "GH56 IJK"
        }
      ]
    },
    {
      "id": 5,
      "name": "Tom Cruise",
      "cars": [
        {
          "id": 9,
          "make": "Volvo",
          "model": "XC60",
          "numberplate": "LM78 NOP"
        }
      ]
    }
  ];


  constructor() { }

  getUsers(): Observable<User[]> {
    return of(this.users).pipe(
      map(users => this.addUserReferenceToCars(users))
    );
  }

  private addUserReferenceToCars(users: User[]): User[] {
    // Create a map to store user references by id
    const userMap = new Map<number, User>();

    // Iterate over users and add them to the map
    users.forEach(user => {
      userMap.set(user.id, user);
      user.cars.forEach(car => {
        car.user = user;
      });
    });

    return users;
  }

}

问题原因与修复方案

核心问题

  1. MatSort绑定时机错误:在ngOnInit中订阅数据时,@ViewChild(MatSort)获取的组件还未完成初始化,此时赋值this.dataSource.sort = this.sort会导致排序组件与数据源无法正确关联。
  2. 模板属性冗余:name列的mat-sort-header="name"属于冗余配置,Angular会自动通过matColumnDef关联排序字段,手动指定虽不报错但无必要。

修复步骤

1. 调整MatSort绑定时机

将this.dataSource.sort = this.sort移至ngAfterViewInit生命周期钩子中,确保视图完全初始化后再绑定排序组件:

修改users.component.ts:

import {Component, OnInit, ViewChild, Input, AfterViewInit} from '@angular/core';
// 其他导入保持不变

export class UsersComponent implements OnInit, AfterViewInit {
  // 现有属性保持不变

  ngOnInit() {
    this.userService.getUsers().subscribe((data: User[]) => {
      this.users = data;
      this.dataSource.data = this.users;
      // 移除此处的 this.dataSource.sort = this.sort
    });
    this.dataSource.filterPredicate = (data, filter: string): boolean => {
      return data.name.toLowerCase().includes(filter) || data.id.toString() === filter;
    };
  }

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

  // 其他方法保持不变
}

2. 简化模板配置

修改users.component.html中name列的mat-header-cell,移除冗余属性:

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

3. 验证模块导入(可选)

确认MatSortModule已正确导入到DemoMaterialModule的exports数组及AppModule的imports中(你的代码已正确配置,此步可跳过)。

修复原理

  • ngAfterViewInit是Angular视图初始化完成后的钩子,此时@ViewChild(MatSort)能获取到已实例化的排序组件,绑定后数据源才能正确响应排序事件。
  • 移除冗余的mat-sort-header属性,让Angular自动关联列定义与排序字段,避免潜在的字段匹配问题。

完成以上修改后,点击列头即可正常触发排序功能。

内容的提问来源于stack exchange,提问作者J.Olufsen

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:07:03