Перейти до вмісту
tatetUI

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`.
keyRowstring"id"Поле-ідентифікатор рядка для `:key`.
sortTableSortnullПоточне сортування. Використовуйте через `v-model:sort`.
skeletonRowsnumber5Скільки рядків-заглушок показати під час ПЕРШОГО завантаження.
emptyTextstring"Даних немає"Текст, коли рядків немає. Складніший стан — слот `empty`.
densitysmmd"md"Щільність рядків. `sm` для довгих таблиць, де важливіше бачити більше рядків.
rowClass(item: T) => stringКлас на рядок — для підсвітки виділених, помилкових тощо. З `tableId` рядок отримує власне тло `bg-card`, тож заливка звідси має бути утилітою, яка в CSS іде після `bg-card` — усі семантичні токени (`bg-warning-bg`, `bg-danger-bg`, `bg-primary-50`) підходять.
maxHeightstringНапр. `"24rem"`. Без нього `stickyHeader` не має де закріплюватись.
tableIdstringВмикає меню налаштувань і збереження розкладки в localStorage під ключем `table_settings_${tableId}`. Без нього таблиця некерована користувачем і нічого не запам'ятовує.
settingsVersionnumber0Версія ВАШИХ дефолтів. Змінили ширини чи видимість у `headers` — підніміть число, і збережений вибір користувача скинеться.
selectedSelectionKey[][]Ключі обраних рядків. Використовуйте через `v-model:selected`.
selectableRow(item: T) => booleanЯкі рядки взагалі можна обрати. Незбиральний рядок показує вимкнений прапорець і не потрапляє ні в «обрати всі», ні в діапазон Shift.
serverSortbooleanСортувати на сервері: компонент лише повідомляє про намір через `update:sort`, але порядок рядків не чіпає.
loadingbooleanПоказує скелетони замість рядків, зберігаючи висоту таблиці.
densityTogglebooleantrueПоказати перемикач щільності в меню налаштувань.
rowClickablebooleanРобить рядки клікабельними: додає роль, фокус і обробку Enter/Space.
mobileCardsbooleanНижче `md` таблиця ховається, а замість неї рендериться список карток — із ТИХ САМИХ слотів `cell-*`. Одне API, дві верстки.
stickyHeaderbooleanЗакріпити шапку. Вимагає `maxHeight`, інакше не діє.
selectablebooleanВмикає колонку прапорців ліворуч. Ключем виділення служить той самий `keyRow`, що вже дає рядкам `:key`.
selectionBarbooleantrueПанель «Вибрано 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 вікна.

Чого тут немає

Віртуального скролу. Для списків у тисячі рядків беріть пагінацію або серверну фільтрацію.