Angular IDPStrong Junior
Модулі / Грид / Багатоколонкове сортування
🔥 0

Модуль 5 · урок 5 з 10

Багатоколонкове сортування

«Multi-column sorting» — перший пункт цілі, і єдиний, якого Material не дає з коробки. Тут доведеться писати логіку самому, і це добре: компаратор — саме той тип коду, за яким видно, чи розуміє людина, що робить.

Стан сортування

export interface SortState {
  key: string;
  dir: 'asc' | 'desc';
}

// пріоритет задає порядок у масиві:
// спершу за статусом, у межах однакового статусу — за терміном
const sort: SortState[] = [
  { key: 'status', dir: 'asc' },
  { key: 'dueDate', dir: 'desc' },
];
ключ 1: status порівняли різні повертаємо результат однакові ключ 2: dueDate порівнюємо далі повертаємо результат усі ключі однакові → 0, і порядок лишається як був
Компаратор — це послідовність спроб: перший ключ вирішує; якщо нічия — другий; якщо і там нічия — наступний.

Компаратор

export function compareBy<T>(sort: readonly SortState[]) {
  return (a: T, b: T): number => {
    for (const { key, dir } of sort) {
      const av = a[key as keyof T];
      const bv = b[key as keyof T];

      // 1. порожні — ПОЗА напрямком, інакше при desc вони стрибнуть угору
      const emptyDiff = rankEmpty(av) - rankEmpty(bv);
      if (emptyDiff !== 0) return emptyDiff;
      if (av == null) continue;        // обидва порожні — цей ключ нічого не вирішує

      // 2. звичайне порівняння — ось тут напрямок уже застосовується
      const diff = compareValues(av, bv);
      if (diff !== 0) return dir === 'asc' ? diff : -diff;
    }
    return 0;   // повна рівність — лишаємо як було
  };
}

function rankEmpty(v: unknown): number {
  return v == null || v === '' ? 1 : 0;   // порожнє завжди «більше» → у кінець
}

function compareValues(a: unknown, b: unknown): number {
  if (typeof a === 'number' && typeof b === 'number') return a - b;
  if (typeof a === 'boolean' && typeof b === 'boolean') return Number(a) - Number(b);

  // рядки: з урахуванням української абетки й без різниці регістру
  return String(a).localeCompare(String(b), 'uk', { sensitivity: 'base', numeric: true });
}
Три деталі, які відрізняють робочий компаратор від наївного

localeCompare замість >. Звичайне порівняння рядків працює за кодами символів: «Яблуко» опиниться перед «яблуня», а «Ї» — узагалі поза абеткою. Опція numeric: true ще й правильно сортує KYIV-9 перед KYIV-10.

Порожні значення в кінець завжди. І тут головна пастка: обробку null треба робити до застосування напрямку. Якщо просто повернути з compareValues значення 1 для порожнього, інверсія при desc перетворить його на −1 — і порожні рядки опиняться вгорі. Саме тому в коді вище перевірка на порожнечу стоїть окремим кроком і повертає результат напряму, без множення на напрямок.

Повернути 0 при рівності. Сортування в JavaScript стабільне за специфікацією, тож рядки з однаковими ключами збережуть попередній порядок.

Перемикання в шапці

toggleSort(key: string, additive: boolean): void {
  const current = this.state.value.sort;
  const existing = current.find(s => s.key === key);

  let next: SortState[];

  if (!existing) {
    // нова колонка
    next = additive ? [...current, { key, dir: 'asc' }] : [{ key, dir: 'asc' }];
  } else if (existing.dir === 'asc') {
    // asc → desc
    next = current.map(s => s.key === key ? { ...s, dir: 'desc' as const } : s);
  } else {
    // desc → прибрати колонку із сортування
    next = current.filter(s => s.key !== key);
  }

  this.state.next({ ...this.state.value, sort: next, page: 1 });   // сторінка на першу
}
<th (click)="toggleSort('status', $event.shiftKey)">
  Статус
  @if (sortIndex('status'); as idx) {
    <span class="sort-badge">{{ sortDir('status') === 'asc' ? '↑' : '↓' }}{{ idx }}</span>
  }
</th>

Цикл із трьох станів — asc → desc → без сортування — це те, до чого користувачі звикли. А Shift+клік для додавання другої колонки — загальноприйнята домовленість, її ж використовує AG Grid.

Обовʼязково покажи пріоритет

Якщо сортування багатоколонкове, у шапці має бути видно порядковий номер кожної колонки, а не лише стрілку. Інакше користувач бачить дві стрілки й не розуміє, яка з них головна. Маленька цифра поруч зі стрілкою — саме та дрібниця, яку помічають на демо.