---
title: "Таблица данных"
description: "Таблица Nuxt UI на 2 000 задач, виртуализированная и с поиском, которая сортируется перетаскиванием строк за их ручки."
canonical_url: "https://zizigy.github.io/vue-dnd-kit/ru/docs/examples/data-table"
---

> Index of every page on this site: [llms.txt](https://zizigy.github.io/vue-dnd-kit/llms.txt)

<example height="34rem" hint="Перетащите задачу за ручку; найдите что-нибудь, затем перетащите: скрытые задачи остаются на своих местах" name="ui-table" title="Таблица данных">



</example>

`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-атрибуты, а остальная строка остаётся свободной
для кликов и выделения текста.

```ts [RowGrip.vue]
const grip = useTemplateRef<HTMLElement>('grip');
makeSortableItem(() => grip.value?.closest('tr') ?? null, {
  id: () => props.task.id,
  handle: grip,
  label: () => props.task.title,
});
```

```vue [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`).

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

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

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

```vue [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` (см. [виртуальные списки](https://zizigy.github.io/docs/guides/virtual-lists#tanstack-virtual)).
Таблица раскладывает строки между строками-распорками, где строка, отрендеренная далеко от
остальных, раздвинула бы их, поэтому эта её отпускает.

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

```ts [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` содержит весь массив.

<tip>

**Другие 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`) и
передайте таблице черновик списка как её данные.
- **Её собственный порядок — выключен.** Не включайте у таблицы ни собственную перестановку
строк, ни сортировку по колонкам: порядок задаёт список.
- **Виртуальные таблицы** размещают строки между распорками или в абсолютно позиционированных
блоках: работает и то и другое, а драг переживает размонтирование своей строки.

</tip>

<note>

Кнопка StackBlitz открывает файлы в Vite-проекте, настроенном под Nuxt UI так, как это описано в
его документации для Vue: плагин `@nuxt/ui/vite`, Tailwind CSS и `UApp` вокруг примера. В
приложении Nuxt с модулем `@nuxt/ui` скопируйте папку и отрендерите `index.vue` на странице.

</note>

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

Перейдите табом к ручке задачи и нажмите <kbd value="Space">



</kbd>

, чтобы взять строку.
<kbd value="ArrowUp">



</kbd>

 и <kbd value="ArrowDown">



</kbd>

 двигают её, <kbd value="Space">



</kbd>

 бросает,
<kbd value="Escape">



</kbd>

 возвращает на место. У строк есть `label`, и у списка тоже: скринридеры
слышат «Fix the login form dropped at position 3 of 2000 in Tasks.», а не ключи.

Подробнее: [виртуальные списки](https://zizigy.github.io/docs/guides/virtual-lists) ·
[показать часть элементов](https://zizigy.github.io/docs/guides/sortable-lists#show-some-of-the-items) ·
[стилизация: Tailwind и UI-киты](https://zizigy.github.io/docs/guides/styling#tailwind-css) ·
[доступность](https://zizigy.github.io/docs/guides/accessibility)

## Sitemap

See the full [sitemap](https://zizigy.github.io/vue-dnd-kit/sitemap.md) for all pages.
