Документация/Обзор

Таблица данных

Таблица Nuxt UI на 2 000 задач, виртуализированная и с поиском, которая сортируется перетаскиванием строк за их ручки.

Редактировать страницу

Страница переведена ИИ и может содержать неточности. Читать английский оригинал

Live

Загружаем демо…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import TaskTable from './TaskTable.vue';
</script>

<template>
  <!-- The row in hand: a table row can't have a shadow, so its overlay content has it. -->
  <DnDProvider overlay-class="overflow-hidden rounded-md bg-default shadow-xl ring ring-accented">
    <TaskTable />
  </DnDProvider>
</template>

UTable из Nuxt UI рендерит таблицу, её строки и её виртуализатор: в DOM есть только строки, которые на экране. Vue DnD Kit их сортирует. Таблица — такая, какой её даёт библиотека, со своими слотами и классами ui; ничего в ней не переписано.

На что обратить внимание

  • TaskTable.vue: сортируемый список — это <tbody> таблицы, найденный через tableRef, который открывает UTable. Таблица получает черновик списка как свои data, поэтому строки двигаются, пока вы тащите, а get-row-id задаёт им ключи по задаче, как и список. axis: 'y' держит драг в своей колонке, а restrictToFirstScrollableAncestor — внутри таблицы: её скролл-контейнер — первый прокручиваемый элемент вокруг строк. Разверните демо: таблица займёт весь экран.
  • RowGrip.vue: строки рендерит UTable, поэтому ни один ваш компонент не является строкой. Ручка в первой ячейке (слот #grip-cell) делает строку вокруг себя элементом списка и сама служит её handle.
  • Поиск — это filter списка: таблица показывает подходящие задачи, а бросок записывает обратно весь массив, скрытые задачи — там, где они были.
  • tasks.ts: 2 000 задач в shallowRef: библиотека никогда не заглядывает внутрь них.

Строка, которую рендерит таблица

Ручка вызывает makeSortableItem с геттером строки: <tr> вокруг неё, как только тот отрендерен. Как ручка, она получает фокус и ARIA-атрибуты, а остальная строка остаётся свободной для кликов и выделения текста.

RowGrip.vue
const grip = useTemplateRef<HTMLElement>('grip');
makeSortableItem(() => grip.value?.closest('tr') ?? null, {
  id: () => props.task.id,
  handle: grip,
  label: () => props.task.title,
});
TaskTable.vue
<UTable ref="table" :data="items" :get-row-id="(task: Task) => task.id" …>
  <template #grip-cell="{ row }">
    <RowGrip :task="row.original" />
  </template>
</UTable>

Оверлей — это копия строки, сделанная библиотекой, в собственной таблице с ширинами ячеек. Строка таблицы не рисует тень, поэтому тень содержимому оверлея даёт overlay-class провайдера, вместе с фоном таблицы и обводкой. Классы ui строки подкрашивают её место, пока она в пути (data-[dnd-placeholder]:bg-primary/5).

index.vue
<DnDProvider overlay-class="overflow-hidden rounded-md bg-default shadow-xl ring ring-accented">
  <TaskTable />
</DnDProvider>

Виртуализированные строки

virtualize даёт таблице TanStack Virtual: рендерятся строки на экране и ещё 8 вокруг них (overscan), между двумя строками-распорками.

TaskTable.vue
<UTable
  :data="items"
  :watch-options="{ deep: false }"
  :virtualize="{ estimateSize: 45, overscan: 8, onChange: () => captureLayout() }"
  class="min-h-0 flex-1"
/>
  • estimateSize — первая прикидка: виртуализатор измеряет каждую строку, когда она рендерится. Измерение сдвигает строки ниже неё, и onChange вызывает captureLayout() списка, чтобы во время драга библиотека анимировала этот сдвиг, а не давала ему прыгнуть.
  • Таблица — это скролл-контейнер, и строка в руке остаётся внутри неё: у её верхнего или нижнего края драг прокручивает её, а новые строки рендерятся, когда появляются в поле зрения.
  • После каждого перемещения черновик — новый массив, поэтому таблице не нужно следить за тем, что у него внутри (deep: false).
  • Прокрутите далеко от строки, которую держите, и она уйдёт из отрендеренного диапазона и размонтируется. Драг продолжается: оверлей всё ещё её показывает, и она приземлится туда, куда её ставит черновик. Ваш собственный виртуальный список может держать её смонтированной через rangeExtractor (см. виртуальные списки). Таблица раскладывает строки между строками-распорками, где строка, отрендеренная далеко от остальных, раздвинула бы их, поэтому эта её отпускает.

Найти, затем перетащить

TaskTable.vue
const { items, captureLayout } = makeSortableList(() => table.value?.tableRef?.tBodies[0] ?? null, {
  items: tasks,
  filter: (task) => {
    const text = query.value.trim().toLowerCase();
    return !text || `${task.id} ${task.title} ${task.assignee}`.toLowerCase().includes(text);
  },
  axis: 'y',
  modifiers: [restrictToFirstScrollableAncestor],
  label: 'Tasks',
});

Найдите «Mia», переместите одну из её задач выше другой, затем очистите поиск: её задачи стоят в новом порядке, а все остальные задачи — там, где были. e.to.index броска считает показанные строки; e.result содержит весь массив.

Другие UI-библиотеки. Nuxt UI рендерит обычные элементы и передаёт ваши классы и атрибуты дальше, как и примитивы Reka UI, на которых он построен: нужны лишь ref, слот и класс. В других библиотеках таблица тоже сама рендерит свои строки, и работает тот же рецепт:
  • Строка. Слот, который рендерит строку целиком (#item у Vuetify, #body у Quasar), даёт вам её элемент: сделайте его элементом списка. Где вы получаете только ячейки (#body у Column в PrimeVue, слот по умолчанию колонки Element Plus, render в Naive UI), поставьте в одну из них ручку и сделайте элементом списка её closest('tr'), как здесь.
  • Список. <tbody> внутри корневого элемента таблицы: template ref на таблице даёт вам компонент, а его $el — элемент.
  • Ключи и данные. Задайте строкам ключи по вашему id (item-value, row-key, dataKey) и передайте таблице черновик списка как её данные.
  • Её собственный порядок — выключен. Не включайте у таблицы ни собственную перестановку строк, ни сортировку по колонкам: порядок задаёт список.
  • Виртуальные таблицы размещают строки между распорками или в абсолютно позиционированных блоках: работает и то и другое, а драг переживает размонтирование своей строки.
Кнопка StackBlitz открывает файлы в Vite-проекте, настроенном под Nuxt UI так, как это описано в его документации для Vue: плагин @nuxt/ui/vite, Tailwind CSS и UApp вокруг примера. В приложении Nuxt с модулем @nuxt/ui скопируйте папку и отрендерите index.vue на странице.

Клавиатура и скринридеры

Перейдите табом к ручке задачи и нажмите Space, чтобы взять строку. ArrowUp и ArrowDown двигают её, Space бросает, Escape возвращает на место. У строк есть label, и у списка тоже: скринридеры слышат «Fix the login form dropped at position 3 of 2000 in Tasks.», а не ключи.

Подробнее: виртуальные списки · показать часть элементов · стилизация: Tailwind и UI-киты · доступность