diff --git a/apps/www/.vitepress/theme/components/ExamplesNav.vue b/apps/www/.vitepress/theme/components/ExamplesNav.vue index 039c010f..c171755f 100644 --- a/apps/www/.vitepress/theme/components/ExamplesNav.vue +++ b/apps/www/.vitepress/theme/components/ExamplesNav.vue @@ -28,6 +28,11 @@ const examples = [ href: '/examples/tasks', code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/tasks', }, + { + name: 'Tasks Virtualized', + href: '/examples/big-data', + code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/big-data', + }, { name: 'Playground', href: '/examples/playground', diff --git a/apps/www/.vitepress/theme/config/docs.ts b/apps/www/.vitepress/theme/config/docs.ts index b18a57d6..ea1bd45d 100644 --- a/apps/www/.vitepress/theme/config/docs.ts +++ b/apps/www/.vitepress/theme/config/docs.ts @@ -365,6 +365,16 @@ export const docsConfig: DocsConfig = { }, ], }, + { + title: 'Guides', + items: [ + { + title: 'Render Large Data', + href: '/docs/guides/rendering-large-data', + items: [], + }, + ], + }, ], } diff --git a/apps/www/src/content/docs/guides/rendering-large-data.md b/apps/www/src/content/docs/guides/rendering-large-data.md new file mode 100644 index 00000000..86fbbe07 --- /dev/null +++ b/apps/www/src/content/docs/guides/rendering-large-data.md @@ -0,0 +1,37 @@ +--- +title: Rendering Large Data +description: In some cases, you may need to render a large amount of data. This can be done using the useVirtualList Component from VueUse library to render only the visible items. This can help to improve performance. +sidebar: false +--- + +## Introduction + +For Table logic we use [TanStack Table](https://tanstack.com/table/latest/) library. It's a powerful and flexible library for building tables in Vue, React and other frameworks. It's designed to be easy to use and flexible, while still providing the features you need. + +## How to render large data + +When you have a large amount of data to render, you can use the [useVirtualList](https://vueuse.org/core/useVirtualList/#usevirtuallist) Component from [VueUse](https://vueuse.org/) library to render only the visible items. This can help to improve performance. + +`useVirtualList` waiting for a `MaybeRef` so firstly you need to wrap your data with `ref` or `computed` function. So we will convert our `table.getRowModel().rows` to `computed` function. + +<<< @/examples/big-data/components/DataTable.vue#tableRows{ts} + +Next step is to create a `useVirtualList` instance and pass our `tableRows` to it. For better experience list elements must be same height, which you define inside `itemHeight` property. `overscan` property is used to define how many items should be rendered outside of the visible area. + +<<< @/examples/big-data/components/DataTable.vue#useVirtualList{2-3 ts} + +As you see `useVirtualList` function returns `list` object which contains `list` property with only visible items and `containerProps`, `wrapperProps` which should be passed to the container and wrapper element. + +<<< @/examples/big-data/components/DataTable.vue#template{4,5,14-24 vue:line-numbers} + +## Example + +Here is an example of how to use `useVirtualList` to render a large amount of data. For test purposes, we will use 2000 rows for our table, you can change statuses, priorities, and view to see speed difference. + + + + + + diff --git a/apps/www/src/content/examples/big-data.md b/apps/www/src/content/examples/big-data.md new file mode 100644 index 00000000..fac3c338 --- /dev/null +++ b/apps/www/src/content/examples/big-data.md @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/www/src/examples/big-data/Example.vue b/apps/www/src/examples/big-data/Example.vue new file mode 100644 index 00000000..b3f1ee0d --- /dev/null +++ b/apps/www/src/examples/big-data/Example.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/www/src/examples/big-data/components/DataTable.vue b/apps/www/src/examples/big-data/components/DataTable.vue new file mode 100644 index 00000000..255d35cd --- /dev/null +++ b/apps/www/src/examples/big-data/components/DataTable.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/apps/www/src/examples/big-data/components/DataTableColumnHeader.vue b/apps/www/src/examples/big-data/components/DataTableColumnHeader.vue new file mode 100644 index 00000000..22be3c48 --- /dev/null +++ b/apps/www/src/examples/big-data/components/DataTableColumnHeader.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/apps/www/src/examples/big-data/components/DataTableFacetedFilter.vue b/apps/www/src/examples/big-data/components/DataTableFacetedFilter.vue new file mode 100644 index 00000000..fc825ffb --- /dev/null +++ b/apps/www/src/examples/big-data/components/DataTableFacetedFilter.vue @@ -0,0 +1,149 @@ + + + diff --git a/apps/www/src/examples/big-data/components/DataTablePagination.vue b/apps/www/src/examples/big-data/components/DataTablePagination.vue new file mode 100644 index 00000000..15524f21 --- /dev/null +++ b/apps/www/src/examples/big-data/components/DataTablePagination.vue @@ -0,0 +1,93 @@ + + + diff --git a/apps/www/src/examples/big-data/components/DataTableRowActions.vue b/apps/www/src/examples/big-data/components/DataTableRowActions.vue new file mode 100644 index 00000000..039e87b6 --- /dev/null +++ b/apps/www/src/examples/big-data/components/DataTableRowActions.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/www/src/examples/big-data/components/DataTableToolbar.vue b/apps/www/src/examples/big-data/components/DataTableToolbar.vue new file mode 100644 index 00000000..d58b54bc --- /dev/null +++ b/apps/www/src/examples/big-data/components/DataTableToolbar.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/www/src/examples/big-data/components/DataTableViewOptions.vue b/apps/www/src/examples/big-data/components/DataTableViewOptions.vue new file mode 100644 index 00000000..21b0f98d --- /dev/null +++ b/apps/www/src/examples/big-data/components/DataTableViewOptions.vue @@ -0,0 +1,57 @@ + + + diff --git a/apps/www/src/examples/big-data/components/UserNav.vue b/apps/www/src/examples/big-data/components/UserNav.vue new file mode 100644 index 00000000..e5868674 --- /dev/null +++ b/apps/www/src/examples/big-data/components/UserNav.vue @@ -0,0 +1,64 @@ + + + diff --git a/apps/www/src/examples/big-data/components/columns.ts b/apps/www/src/examples/big-data/components/columns.ts new file mode 100644 index 00000000..9e7fc467 --- /dev/null +++ b/apps/www/src/examples/big-data/components/columns.ts @@ -0,0 +1,86 @@ +import type { ColumnDef } from '@tanstack/vue-table' +import { h } from 'vue' + +import { labels, priorities, statuses } from '../data/data' +import type { Task } from '../data/schema' +import DataTableColumnHeader from './DataTableColumnHeader.vue' +import DataTableRowActions from './DataTableRowActions.vue' +import { Checkbox } from '@/lib/registry/new-york/ui/checkbox' +import { Badge } from '@/lib/registry/new-york/ui/badge' + +export const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => h(Checkbox, + { 'checked': table.getIsAllPageRowsSelected(), 'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value), 'ariaLabel': 'Select all', 'class': 'translate-y-0.5' }), + cell: ({ row }) => h(Checkbox, + { 'checked': row.getIsSelected(), 'onUpdate:checked': value => row.toggleSelected(!!value), 'ariaLabel': 'Select row', 'class': 'translate-y-0.5' }), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'id', + header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Task' }), + cell: ({ row }) => h('div', { class: 'w-20' }, row.getValue('id')), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'title', + header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Title' }), + + cell: ({ row }) => { + const label = labels.find(label => label.value === row.original.label) + + return h('div', { class: 'flex space-x-2' }, [ + label && h(Badge, { variant: 'outline' }, label.label), + h('span', { class: 'max-w-[180px] truncate font-medium' }, row.getValue('title')), + ]) + }, + }, + { + accessorKey: 'status', + header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Status' }), + + cell: ({ row }) => { + const status = statuses.find( + status => status.value === row.getValue('status'), + ) + + if (!status) + return null + + return h('div', { class: 'flex w-[100px] items-center' }, [ + status.icon && h(status.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }), + h('span', status.label), + ]) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + { + accessorKey: 'priority', + header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Priority' }), + cell: ({ row }) => { + const priority = priorities.find( + priority => priority.value === row.getValue('priority'), + ) + + if (!priority) + return null + + return h('div', { class: 'flex items-center' }, [ + priority.icon && h(priority.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }), + h('span', priority.label), + ]) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + { + id: 'actions', + cell: ({ row }) => h(DataTableRowActions, { row }), + }, +] diff --git a/apps/www/src/examples/big-data/data/data.ts b/apps/www/src/examples/big-data/data/data.ts new file mode 100644 index 00000000..f2eb8651 --- /dev/null +++ b/apps/www/src/examples/big-data/data/data.ts @@ -0,0 +1,70 @@ +import { h } from 'vue' +import ArrowDownIcon from '~icons/radix-icons/arrow-down' +import ArrowRightIcon from '~icons/radix-icons/arrow-right' +import ArrowUpIcon from '~icons/radix-icons/arrow-up' +import CheckCircledIcon from '~icons/radix-icons/check-circled' +import CircleIcon from '~icons/radix-icons/circle' +import CrossCircledIcon from '~icons/radix-icons/cross-circled' +import QuestionMarkCircledIcon from '~icons/radix-icons/question-mark-circled' +import StopwatchIcon from '~icons/radix-icons/stopwatch' + +export const labels = [ + { + value: 'bug', + label: 'Bug', + }, + { + value: 'feature', + label: 'Feature', + }, + { + value: 'documentation', + label: 'Documentation', + }, +] + +export const statuses = [ + { + value: 'backlog', + label: 'Backlog', + icon: h(QuestionMarkCircledIcon), + }, + { + value: 'todo', + label: 'Todo', + icon: h(CircleIcon), + }, + { + value: 'in progress', + label: 'In Progress', + icon: h(StopwatchIcon), + }, + { + value: 'done', + label: 'Done', + icon: h(CheckCircledIcon), + }, + { + value: 'canceled', + label: 'Canceled', + icon: h(CrossCircledIcon), + }, +] + +export const priorities = [ + { + label: 'Low', + value: 'low', + icon: h(ArrowDownIcon), + }, + { + label: 'Medium', + value: 'medium', + icon: h(ArrowRightIcon), + }, + { + label: 'High', + value: 'high', + icon: h(ArrowUpIcon), + }, +] diff --git a/apps/www/src/examples/big-data/data/schema.ts b/apps/www/src/examples/big-data/data/schema.ts new file mode 100644 index 00000000..deacb53d --- /dev/null +++ b/apps/www/src/examples/big-data/data/schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +// We're keeping a simple non-relational schema here. +// IRL, you will have a schema for your data models. +export const taskSchema = z.object({ + id: z.string(), + title: z.string(), + status: z.string(), + label: z.string(), + priority: z.string(), +}) + +export type Task = z.infer diff --git a/apps/www/src/lib/registry/default/ui/table/Table.vue b/apps/www/src/lib/registry/default/ui/table/Table.vue index a4238918..1a0d6572 100644 --- a/apps/www/src/lib/registry/default/ui/table/Table.vue +++ b/apps/www/src/lib/registry/default/ui/table/Table.vue @@ -8,7 +8,7 @@ const props = defineProps<{