Skip to content

Tables

Reshape's data table wraps TanStack Table with pagination, sorting, and search state that lives in the URL — so a table's page, sort order, and filters survive a refresh or a shared link, without any component-local state to manage.

The customers data table

DataTable

The core component, web/src/components/ui/data-table/DataTable.vue:

DataTable : columns, data, title?, totalRecords?, paginated?, sortable?, searchable?, searchablePlaceholder?, controlled?, loading?

v-models: page, perPage, sort, search. Slots: #actions (header controls), #empty, #footer.

controlled is the key switch: without it, the table sorts/paginates/filters client-side over the full data array (fine for small, fully-loaded datasets). With it, manualSorting/manualPagination/manualFiltering are enabled and the table trusts you to fetch the right page from the server — which is what every real, server-paginated table in Reshape does.

Columns

Columns are a standard TanStack ColumnDef<T>[], using h() to render Vue components for headers and cells. Two ready-made pieces cover most cases:

  • DataTableColumnHeader — a sortable column header with a direction indicator.
  • DateCell / BooleanCell (components/ui/data-table/*) — ready-made cell renderers for dates and booleans.

A trailing actions column with enableSorting: false, enableHiding: false — rendering a per-row dropdown menu — is the standard place for edit/delete actions. There's no built-in bulk/row-selection system, so multi-row actions aren't something you get for free.

URL-driven state

useRouteTableSearch<TSearch>({ from, debounceMillis? }) : { searchParams, page, perPage, search, sort, filter }

Connects a table's v-models to the route's search params, so state lives in the URL instead of component state — converts a sort string ("-created_at,name") to/from TanStack Table's SortingState, debounces search input, and resets to page 1 whenever search/filter/per-page changes. The route declares the shape of searchParams with a Zod schema in its route.ts (validateSearch), so invalid URL params are rejected before they ever reach the query. See the Customers index page for a full working example.

Filters

Beyond the built-in search box, components/resource-filter/* provides a composable popover filter system that binds directly to the same URL-backed filter model:

  • ResourceFilter — the popover container.
  • SelectFilter, CountryFilter, DateFilter — filter-type building blocks.

Non-table pagination

For lists that don't need the full table apparatus, app-paginator.vue and components/ui/pagination/* provide standalone pagination controls.