如何解耦Angular排序指令并使其适配不同数据模型?
通用Angular表格排序指令改造方案
问题背景
现有Angular表格排序指令与Student模型强耦合(SortColumn依赖keyof Student),无法直接复用在其他对象的表格中,需要改造为通用型并实现高效代码复用。
现有耦合代码:
import { Directive, EventEmitter, Input, Output } from '@angular/core'; import { Student } from 'src/app/models/student.model'; export type SortColumn = keyof Student | ''; export type SortDirection = 'asc' | 'desc' | ''; const rotate: { [key: string]: SortDirection } = { asc: 'desc', desc: '', '': 'asc' }; export interface SortEvent { column: SortColumn; direction: SortDirection; } @Directive({ selector: 'th[sortable]', // standalone: true, host: { '[class.asc]': 'direction === "asc"', '[class.desc]': 'direction === "desc"', '(click)': 'rotate()', }, }) export class NgbdSortableHeader { @Input() sortable: SortColumn = ''; @Input() direction: SortDirection = ''; @Output() sort = new EventEmitter<SortEvent>(); rotate() { this.direction = rotate[this.direction]; this.sort.emit({ column: this.sortable, direction: this.direction }); } }
改造为通用指令的实现方式
1. 将指令改为泛型结构
通过泛型支持任意模型类型,解除与特定模型的绑定:
import { Directive, EventEmitter, Input, Output } from '@angular/core'; export type SortDirection = 'asc' | 'desc' | ''; const rotate: { [key: string]: SortDirection } = { asc: 'desc', desc: '', '': 'asc' }; // 泛型排序事件,适配任意数据类型 export interface SortEvent<T> { column: keyof T | ''; direction: SortDirection; } // 泛型指令,通过<T>接收父组件传入的模型类型 @Directive({ selector: 'th[sortable]', // standalone: true, host: { '[class.asc]': 'direction === "asc"', '[class.desc]': 'direction === "desc"', '(click)': 'rotate()', }, }) export class NgbdSortableHeader<T> { @Input() sortable: keyof T | '' = ''; @Input() direction: SortDirection = ''; @Output() sort = new EventEmitter<SortEvent<T>>(); rotate() { this.direction = rotate[this.direction]; this.sort.emit({ column: this.sortable, direction: this.direction }); } }
2. 父组件中指定模型类型
在使用指令的父组件中,通过泛型参数传入目标模型(如Student):
import { Component, ViewChildren, QueryList } from '@angular/core'; import { Student } from 'src/app/models/student.model'; import { NgbdSortableHeader, SortEvent } from './ngbd-sortable-header.directive'; @Component({ selector: 'app-student-table', templateUrl: './student-table.component.html' }) export class StudentTableComponent { students: Student[] = []; // 明确指定指令的泛型类型为Student @ViewChildren(NgbdSortableHeader<Student>) headers!: QueryList<NgbdSortableHeader<Student>>; onSort(event: SortEvent<Student>) { const { column, direction } = event; if (direction === '') { this.students = [...this.students]; } else { // 针对Student类型的排序逻辑 this.students = [...this.students].sort((a, b) => { const valA = a[column] as any; const valB = b[column] as any; return direction === 'asc' ? (valA > valB ? 1 : -1) : (valA < valB ? 1 : -1); }); } } }
3. 模板使用方式
模板无需额外配置,直接绑定指令即可:
<table class="table"> <thead> <tr> <th sortable="name" [direction]="sortDir.name" (sort)="onSort($event)">姓名</th> <th sortable="age" [direction]="sortDir.age" (sort)="onSort($event)">年龄</th> </tr> </thead> <tbody> <tr *ngFor="let student of students"> <td>{{ student.name }}</td> <td>{{ student.age }}</td> </tr> </tbody> </table>
更优的代码复用方案(DRY原则)
1. 封装通用排序服务
将排序逻辑抽离为服务,避免在多个组件中重复编写:
import { Injectable } from '@angular/core'; import { SortDirection } from './ngbd-sortable-header.directive'; @Injectable({ providedIn: 'root' }) export class SortService { sort<T>(data: T[], column: keyof T, direction: SortDirection): T[] { if (direction === '') return [...data]; return [...data].sort((a, b) => { const valA = a[column] as any; const valB = b[column] as any; // 针对字符串类型做本地化排序 if (typeof valA === 'string' && typeof valB === 'string') { return direction === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA); } return direction === 'asc' ? (valA > valB ? 1 : -1) : (valA < valB ? 1 : -1); }); } }
父组件中注入服务简化逻辑:
constructor(private sortService: SortService) {} onSort(event: SortEvent<Student>) { this.students = this.sortService.sort(this.students, event.column, event.direction); }
2. 构建通用表格组件
如果多个表格结构相似,可进一步封装为通用表格组件,实现全场景复用:
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { SortEvent, SortDirection } from './ngbd-sortable-header.directive'; @Component({ selector: 'app-generic-table', template: ` <table class="table"> <thead> <tr> <th *ngFor="let col of columns" sortable="{{col.key}}" [direction]="sortDir[col.key]" (sort)="onSort.emit($event)"> {{ col.label }} </th> </tr> </thead> <tbody> <tr *ngFor="let item of data"> <td *ngFor="let col of columns">{{ item[col.key] }}</td> </tr> </tbody> </table> ` }) export class GenericTableComponent<T> { @Input() data: T[] = []; @Input() columns: { key: keyof T; label: string }[] = []; @Input() sortDir: Record<keyof T, SortDirection> = {} as Record<keyof T, SortDirection>; @Output() onSort = new EventEmitter<SortEvent<T>>(); }
使用时只需传入数据和列配置:
// 学生表格组件 columns = [ { key: 'name', label: '姓名' }, { key: 'age', label: '年龄' } ]; sortDir: Record<keyof Student, SortDirection> = { name: '', age: '' }; onSort(event: SortEvent<Student>) { this.sortDir[event.column as keyof Student] = event.direction; this.students = this.sortService.sort(this.students, event.column, event.direction); }
模板调用:
<app-generic-table [data]="students" [columns]="columns" [sortDir]="sortDir" (onSort)="onSort($event)"> </app-generic-table>
内容的提问来源于stack exchange,提问作者Rick Dawson
相关产品推荐
相关产品推荐

