Table
Таблиця зі сортуванням, станами завантаження і мобільними картками з тих самих слотів.
Приклад
- Сторінка
- Розділ 9
- Перегляди
- 22 910
- Статус
- опубліковано
- Сторінка
- Розділ 2
- Перегляди
- 1 284
- Статус
- опубліковано
- Сторінка
- Вступ
- Перегляди
- 431
- Статус
- чернетка
- Сторінка
- Розділ 10
- Перегляди
- 87
- Статус
- чернетка
Сортування: views / desc — третій клік по заголовку скидає.
Налаштування колонок
Задайте tableId — і вмикається все одразу: ресайз межею заголовка, меню
видимості й порядку, перемикач щільності та збереження в localStorage.
- SKU
- AB-1
- Назва
- Кабель USB-C 2 м
- Постачальник
- Мережа Плюс
- Ціна
- 249 ₴
- Залишок
- 12
- Примітка
- Доставка з Києва протягом доби. Залишок оновлюється щогодини.
- SKU
- AB-10
- Назва
- Кабель USB-C 0.5 м
- Постачальник
- Мережа Плюс
- Ціна
- 149 ₴
- Залишок
- 3
- Примітка
- Мала партія.
- SKU
- AB-9
- Назва
- Перехідник HDMI
- Постачальник
- Технохаб
- Ціна
- 599 ₴
- Залишок
- 0
- Примітка
- Немає в наявності, очікується поповнення наступного тижня.
- SKU
- CD-2
- Назва
- Док-станція 7-в-1
- Постачальник
- Технохаб
- Ціна
- 2 490 ₴
- Залишок
- 5
- Примітка
- SKU
- CD-7
- Назва
- Хаб USB-C 4 порти
- Постачальник
- Мережа Плюс
- Ціна
- 890 ₴
- Залишок
- 27
- Примітка
- Хіт продажів сезону.
- SKU
- EF-3
- Назва
- Зарядний пристрій 65 Вт GaN
- Постачальник
- Технохаб
- Ціна
- 1 290 ₴
- Залишок
- 8
- Примітка
- Компактний блок із двома USB-C.
Потягніть межу заголовка, щоб змінити ширину; подвійний клік скидає. Кнопка над таблицею — видимість, порядок і щільність. Усе зберігається й переживає перезавантаження.
Стани
Вибір рядків
selectable додає колонку прапорців, v-model:selected тримає ключі
обраних рядків. Ключ береться з того самого поля, що вже дає рядкам :key
— окремого props для цього немає й не треба.
- Клієнт
- ТОВ «Сігма Трейд»
- Статус
- В роботі
- Сума
- 420 000 ₴
- Клієнт
- ФОП Коваленко О. П.
- Статус
- Виграно
- Сума
- 86 500 ₴
- Клієнт
- ПрАТ «Дніпро-Логістик»
- Статус
- В роботі
- Сума
- 1 240 000 ₴
- Клієнт
- ТОВ «Аграрій Плюс»
- Статус
- Втрачено
- Сума
- 54 000 ₴
- Клієнт
- ТОВ «Медтехніка»
- Статус
- В роботі
- Сума
- 315 000 ₴
Shift+клік виділяє діапазон. Втрачену угоду обрати не можна — selectableRow її виключає.
Три рішення, які тут неочевидні.
«Обрати всі» об'єднує, а не замінює. Таблиця не пагінує сама:
items — це вже сторінка, а UiPagination
живе окремо. Якби прапорець у шапці замінював набір, користувач, що вибрав
рядки, перейшов на другу сторінку й натиснув «обрати всі», мовчки втратив
би вибір із першої. Тому шапка додає ключі сторінки до набору й забирає їх
назад — ключі інших сторінок лишаються цілі.
Shift+клік ставить діапазону стан якоря, як у Finder і Gmail, а не перемикає кожен рядок окремо: почергове перемикання дає результат, який неможливо передбачити оком. Якір — останнє перемикання без Shift. Діапазон рахується в порядку показу, тож після сортування Shift виділяє те, що видно між двома рядками, а не те, що лежало між ними у вихідних даних.
Виділення не зберігається. Ширини й видимість колонок ідуть у
localStorage, виділення — ні: набір обраних записів живе рівно стільки,
скільки триває дія над ними.
Сортування виділенню не шкодить: воно ключується по keyRow, а не по
індексу, тож перевпорядкування рядків його не плутає.
API
Props
| Назва | Тип | Типово | Опис |
|---|---|---|---|
headers* | TableHeader[] | — | Опис колонок: ключ, підпис, вирівнювання, ширина, сортованість. |
items* | T[] | — | Рядки таблиці. Ключ рядка береться з поля, названого в `keyRow`. |
keyRow | string | "id" | Поле-ідентифікатор рядка для `:key`. |
sort | TableSort | null | Поточне сортування. Використовуйте через `v-model:sort`. |
skeletonRows | number | 5 | Скільки рядків-заглушок показати під час ПЕРШОГО завантаження. |
emptyText | string | "Даних немає" | Текст, коли рядків немає. Складніший стан — слот `empty`. |
density | smmd | "md" | Щільність рядків. `sm` для довгих таблиць, де важливіше бачити більше рядків. |
rowClass | (item: T) => string | — | Клас на рядок — для підсвітки виділених, помилкових тощо. З `tableId` рядок отримує власне тло `bg-card`, тож заливка звідси має бути утилітою, яка в CSS іде після `bg-card` — усі семантичні токени (`bg-warning-bg`, `bg-danger-bg`, `bg-primary-50`) підходять. |
maxHeight | string | — | Напр. `"24rem"`. Без нього `stickyHeader` не має де закріплюватись. |
tableId | string | — | Вмикає меню налаштувань і збереження розкладки в localStorage під ключем `table_settings_${tableId}`. Без нього таблиця некерована користувачем і нічого не запам'ятовує. |
settingsVersion | number | 0 | Версія ВАШИХ дефолтів. Змінили ширини чи видимість у `headers` — підніміть число, і збережений вибір користувача скинеться. |
selected | SelectionKey[] | [] | Ключі обраних рядків. Використовуйте через `v-model:selected`. |
selectableRow | (item: T) => boolean | — | Які рядки взагалі можна обрати. Незбиральний рядок показує вимкнений прапорець і не потрапляє ні в «обрати всі», ні в діапазон Shift. |
serverSort | boolean | — | Сортувати на сервері: компонент лише повідомляє про намір через `update:sort`, але порядок рядків не чіпає. |
loading | boolean | — | Показує скелетони замість рядків, зберігаючи висоту таблиці. |
densityToggle | boolean | true | Показати перемикач щільності в меню налаштувань. |
rowClickable | boolean | — | Робить рядки клікабельними: додає роль, фокус і обробку Enter/Space. |
mobileCards | boolean | — | Нижче `md` таблиця ховається, а замість неї рендериться список карток — із ТИХ САМИХ слотів `cell-*`. Одне API, дві верстки. |
stickyHeader | boolean | — | Закріпити шапку. Вимагає `maxHeight`, інакше не діє. |
selectable | boolean | — | Вмикає колонку прапорців ліворуч. Ключем виділення служить той самий `keyRow`, що вже дає рядкам `:key`. |
selectionBar | boolean | true | Панель «Вибрано N» над таблицею. Вимикайте, коли масові дії живуть у власному тулбарі споживача. |
Події
| Назва | Payload | Опис |
|---|---|---|
update:sort | [value: TableSort] | Нове сортування або `null`, якщо скинуто. Використовуйте через `v-model:sort`. |
update:headers | [value: TableHeader[]] | Розкладка колонок змінилася — видимість, порядок або ширина. |
update:selected | [value: SelectionKey[]] | Ключі обраних рядків. Використовуйте через `v-model:selected`. |
rowClick | [item: T] | Активація рядка кліком, Enter або Space. Лише коли задано `rowClickable`. |
Слоти
| Назва | Опис |
|---|---|
mobile-card | Вміст картки нижче `md`, якщо стандартний список пар не підходить. |
empty | Показується замість «Даних немає». |
selection-actions | Дії в панелі «Вибрано N». `clear` знімає виділення. |
Доступно через ref
| Назва | Тип | Опис |
|---|---|---|
clearSelection | () => void | Знімає виділення. Потрібне після успішної масової дії. |
selectAllOnPage | () => void | Обирає всі доступні рядки поточного `items`. |
Коли використовувати
Для однорідних даних, які порівнюють між собою по колонках.
mobileCards вмикайте завжди, коли колонок більше трьох: горизонтальна
прокрутка таблиці на телефоні — найгірший спосіб читання даних.
Коли НЕ використовувати
Не робіть таблицю з двох колонок «назва / значення» — це список описів
(<dl>), і на мобільному він виглядатиме краще без жодних зусиль.
Ширини живуть у colgroup
width — це число в пікселях, а не CSS-рядок. Ширини задаються один раз у
<colgroup> при table-layout: fixed, а не на кожній комірці: за фіксованої
розкладки враховується лише перший рядок, тож інлайновий width на кожному
<td> був би мертвим стилем, помноженим на кількість рядків.
Рівно одна колонка може мати flex: true — вона забирає весь залишок. Сума
фіксованих ширин має вміщатись у контейнер.
Число, а не рядок, ще й тому, що ширина бере участь в арифметиці: ресайз,
збереження, підрахунок переповнення. З "9rem" нічого з цього не порахуєш.
Одне API, дві верстки
Нижче md таблиця ховається, а картки рендеряться з тих самих слотів
cell-*. Розмітку комірки не треба писати двічі:
<UiTable :headers="headers" :items="items" mobile-cards>
<template #cell-status="{ item }">
<UiChip :tone="item.status === 'published' ? 'success' : 'neutral'">
{{ item.status }}
</UiChip>
</template>
</UiTable>
Сортування
Порівняння враховує числа й локаль. Наївні < і > ставлять «Розділ 10»
перед «Розділ 9», а кирилицю сортують за кодами символів — перевірте це на
прикладі вище.
Порожні значення завжди в кінці, незалежно від напрямку: рядок без даних не має витісняти заповнені з початку списку.
Третій клік по заголовку скидає сортування. Без цього повернутися до вихідного порядку можна було б лише перезавантаженням сторінки.
Для серверного сортування задайте serverSort: компонент повідомить про
намір через update:sort, але порядок рядків не чіпатиме.
Стани
Скелетон показується лише при першому завантаженні. Якщо дані вже є, замість нього накладається напівпрозорий оверлей — інакше таблиця блимала б на кожній зміні фільтра.
Ширина заглушок детермінована, а не випадкова: Math.random() міняв би її
на кожному рендері, і скелетон миготів би.
Доступність
Заголовки колонок мають scope="col" і aria-sort. Сортування вмикається
кнопкою всередині <th>, а не кліком по самому <th> — інакше воно
недоступне з клавіатури.
stickyHeader вимагає maxHeight. Без нього закріплювати шапку немає
відносно чого, і властивість мовчки нічого не робить.
Дві осі версій збереженого
Найтонше місце всієї таблиці. Збережена розкладка має два незалежні номери версій, бо в ній змішані дві різні речі.
settingsVersion — ваш. Підніміть його, коли змінили дефолтні ширини чи
видимість у headers: збережене більше не описує ту саму таблицю, і тримати
його означало б показувати користувачам чужу розкладку.
SETTINGS_SCHEMA — компонента. Коли міняється формат payload, порядок і
видимість (це вибір користувача) мігрують уперед, а ширини й щільність (це
дефолти компонента) скидаються.
Нова колонка вставляється після найближчого лівого сусіда, а не в кінець. Без цього колонка, додана через пів року, стрибала б у хвіст у кожного, хто колись міняв порядок.
tableId варто передавати динамічно там, де таблиця показує різні сутності
(group-${id}) — компонент стежить і за ним, інакше наступна сутність
відкрилася б із розкладкою попередньої.
Афорданси прокрутки
Переповнення рахується як ширина таблиці − ширина видимої області, а не через
scrollWidth. Так градієнт з'являється і після ресайзу колонки — від нього
розмір контейнера не міняється, тож самого ResizeObserver було б замало.
ResizeObserver при цьому теж потрібен: згортання сайдбара міняє ширину
контейнера без жодної події resize вікна.
Чого тут немає
Віртуального скролу. Для списків у тисячі рядків беріть пагінацію або серверну фільтрацію.