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

如何解耦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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 17:20:34