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; } }
问题原因与修复方案
核心问题
MatSort绑定时机错误:在ngOnInit中订阅数据时,@ViewChild(MatSort)获取的组件还未完成初始化,此时赋值this.dataSource.sort = this.sort会导致排序组件与数据源无法正确关联。- 模板属性冗余:
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
相关产品推荐
相关产品推荐

