Модуль 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' },
];
Компаратор
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.
Якщо сортування багатоколонкове, у шапці має бути видно порядковий номер кожної колонки, а не лише стрілку. Інакше користувач бачить дві стрілки й не розуміє, яка з них головна. Маленька цифра поруч зі стрілкою — саме та дрібниця, яку помічають на демо.