Skip to content

Commit

Permalink
docs(data-table): add DataTableReactive example (unovue#744)
Browse files Browse the repository at this point in the history
* docs(table): add `DataTableReactive` example and tanstack docs

* chore: build:registry

* docs: refrase

* docs(data-table): update `defineModel`

* fix(data-table): change `reactive` data to `shallowRef`

* docs(data-table): add info about `ref` and `shallowRef`
  • Loading branch information
hrynevychroman authored Aug 31, 2024
1 parent cc84ac1 commit 481bebf
Show file tree
Hide file tree
Showing 4 changed files with 574 additions and 0 deletions.
14 changes: 14 additions & 0 deletions apps/www/__registry__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/DataTableDemoColumn.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DataTableDemoColumn.vue"],
},
"DataTableReactiveDemo": {
name: "DataTableReactiveDemo",
type: "components:example",
registryDependencies: ["button","checkbox","dropdown-menu","input","table","utils"],
component: () => import("../src/lib/registry/default/example/DataTableReactiveDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/DataTableReactiveDemo.vue"],
},
"DatePickerDemo": {
name: "DatePickerDemo",
type: "components:example",
Expand Down Expand Up @@ -2013,6 +2020,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/DataTableDemoColumn.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DataTableDemoColumn.vue"],
},
"DataTableReactiveDemo": {
name: "DataTableReactiveDemo",
type: "components:example",
registryDependencies: ["button","checkbox","dropdown-menu","input","table","utils"],
component: () => import("../src/lib/registry/new-york/example/DataTableReactiveDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/DataTableReactiveDemo.vue"],
},
"DatePickerDemo": {
name: "DatePickerDemo",
type: "components:example",
Expand Down
14 changes: 14 additions & 0 deletions apps/www/src/content/docs/components/data-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ npm install @tanstack/vue-table

<ComponentPreview name="DataTableColumnPinningDemo" />

### Reactive Table

A reactive table was added in `v8.20.0` of the TanStack Table. You can see the [docs](https://tanstack.com/table/latest/docs/framework/vue/guide/table-state#using-reactive-data) for more information. We added an example where we are randomizing `status` column. One main point is that you need to mutate **full** data, as it is a `shallowRef` object.

> __*⚠️ `shallowRef` is used under the hood for performance reasons, meaning that the data is not deeply reactive, only the `.value` is. To update the data you have to mutate the data directly.*__
Relative PR: [Tanstack/table #5687](https://github.com/TanStack/table/pull/5687#issuecomment-2281067245)

If you want to mutate `props.data`, you should use [`defineModel`](https://vuejs.org/api/sfc-script-setup.html#definemodel).

There is no difference between using `ref` or `shallowRef` for your data object; it will be automatically mutated by the TanStack Table to `shallowRef`.

<ComponentPreview name="DataTableReactiveDemo" />

## Prerequisites

We are going to build a table to show recent payments. Here's what our data looks like:
Expand Down
273 changes: 273 additions & 0 deletions apps/www/src/lib/registry/default/example/DataTableReactiveDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<script setup lang="ts">
import type {
ColumnDef,
ColumnFiltersState,
ExpandedState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref, shallowRef } from 'vue'
import DropdownAction from './DataTableDemoColumn.vue'
import { Button } from '@/lib/registry/default/ui/button'
import { Checkbox } from '@/lib/registry/default/ui/checkbox'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
import { Input } from '@/lib/registry/default/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/lib/registry/default/ui/table'
import { valueUpdater } from '@/lib/utils'
export interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
const data = shallowRef<Payment[]>([
{
id: 'm5gr84i9',
amount: 316,
status: 'success',
email: '[email protected]',
},
{
id: '3u1reuv4',
amount: 242,
status: 'success',
email: '[email protected]',
},
{
id: 'derv1ws0',
amount: 837,
status: 'processing',
email: '[email protected]',
},
{
id: '5kma53ae',
amount: 874,
status: 'success',
email: '[email protected]',
},
{
id: 'bhqecj4p',
amount: 721,
status: 'failed',
email: '[email protected]',
},
])
const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'checked': row.getIsSelected(),
'onUpdate:checked': value => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => h('div', { class: 'capitalize' }, row.getValue('status')),
},
{
accessorKey: 'email',
header: ({ column }) => {
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
// Format the amount as a dollar amount
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
},
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
onExpand: row.toggleExpanded,
}))
},
},
]
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
const statuses: Payment['status'][] = ['pending', 'processing', 'success', 'failed']
function randomize() {
data.value = data.value.map(item => ({
...item,
status: statuses[Math.floor(Math.random() * statuses.length)],
}))
}
</script>

<template>
<div class="w-full">
<div class="flex gap-2 items-center py-4">
<Input
class="max-w-52"
placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)"
/>
<Button @click="randomize">
Randomize
</Button>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns <ChevronDown class="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())"
:key="column.id"
class="capitalize"
:checked="column.getIsVisible()"
@update:checked="(value) => {
column.toggleVisibility(!!value)
}"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() && 'selected'">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>

<TableRow v-else>
<TableCell
:colspan="columns.length"
class="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>

<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</Button>
<Button
variant="outline"
size="sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</Button>
</div>
</div>
</div>
</template>
Loading

0 comments on commit 481bebf

Please sign in to comment.