Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement new Tasks BigData example for useVirtualList usage #334

Draft
wants to merge 9 commits into
base: dev
Choose a base branch
from
5 changes: 5 additions & 0 deletions apps/www/.vitepress/theme/components/ExamplesNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions apps/www/.vitepress/theme/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,16 @@ export const docsConfig: DocsConfig = {
},
],
},
{
title: 'Guides',
items: [
{
title: 'Render Large Data',
href: '/docs/guides/rendering-large-data',
items: [],
},
],
},
],
}

Expand Down
37 changes: 37 additions & 0 deletions apps/www/src/content/docs/guides/rendering-large-data.md
Original file line number Diff line number Diff line change
@@ -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<T[]>` 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.

<script setup>
import BigDataExample from "@/examples/big-data/Example.vue"
</script>

<Suspense>
<BigDataExample />
</Suspense>
7 changes: 7 additions & 0 deletions apps/www/src/content/examples/big-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup>
import BigDataExample from "@/examples/big-data/Example.vue"
</script>

<Suspense>
<BigDataExample />
</Suspense>
11 changes: 11 additions & 0 deletions apps/www/src/examples/big-data/Example.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import DataTable from './components/DataTable.vue'
import { columns } from './components/columns'

const response = await fetch('https://api.json-generator.com/templates/NttPDOzDwSsS/data?access_token=xi78iyv3ez1qwmm9pgou7r5mbqxfxrfmdzcps0hm')
const tasks = await response.json()
</script>

<template>
<DataTable :data="tasks" :columns="columns" />
</template>
126 changes: 126 additions & 0 deletions apps/www/src/examples/big-data/components/DataTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<script setup lang="ts">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'

import { useVirtualList } from '@vueuse/core'

import { computed, ref } from 'vue'
import type { Task } from '../data/schema'
import DataTablePagination from './DataTablePagination.vue'
import DataTableToolbar from './DataTableToolbar.vue'
import { valueUpdater } from '@/lib/utils'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/lib/registry/new-york/ui/table'

interface DataTableProps {
columns: ColumnDef<Task, any>[]
data: Task[]
}
const props = defineProps<DataTableProps>()

const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})

const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
},
enableRowSelection: true,
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
initialState: {
pagination: {
pageSize: 500,
},
},
})

// #region tableRows
const tableRows = computed(() => table.getRowModel().rows)
// #endregion tableRows

// #region useVirtualList
const { list, containerProps, wrapperProps } = useVirtualList(tableRows, {
itemHeight: 49,
overscan: 15,
})
// #endregion useVirtualList
</script>

<!-- #region template -->
<template>
<div class="space-y-4">
<DataTableToolbar :table="table" />
<div class="rounded-md border max-h-[75dvh]" v-bind="containerProps">
<Table v-bind="wrapperProps">
<TableHeader style="height: 49px">
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id" class="sticky top-0 bg-black z-10 border-b">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="tableRows?.length">
<TableRow
v-for="row in list"
:key="row.data.id"
:data-state="row.data.getIsSelected() && 'selected'"
>
<TableCell v-for="cell in row.data.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
</template>

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

<DataTablePagination :table="table" />
</div>
</template>
<!-- #endregion template -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import ArrowDownIcon from '~icons/radix-icons/arrow-down'
import ArrowUpIcon from '~icons/radix-icons/arrow-up'
import CaretSortIcon from '~icons/radix-icons/caret-sort'
import EyeNoneIcon from '~icons/radix-icons/eye-none'
import ResetIcon from '~icons/radix-icons/reset'

import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'

interface DataTableColumnHeaderProps {
column: Column<Task, any>
title: string
}

defineProps<DataTableColumnHeaderProps>()
</script>

<script lang="ts">
export default {
inheritAttrs: false,
}
</script>

<template>
<div v-if="column.getCanSort()" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{{ title }}</span>
<ArrowDownIcon v-if="column.getIsSorted() === 'desc'" class="ml-2 h-4 w-4" />
<ArrowUpIcon v-else-if=" column.getIsSorted() === 'asc'" class="ml-2 h-4 w-4" />
<CaretSortIcon v-else class="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem @click="column.toggleSorting(false)">
<ArrowUpIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem @click="column.toggleSorting(true)">
<ArrowDownIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="column.toggleVisibility(false)">
<EyeNoneIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
<DropdownMenuItem v-if="column.getIsSorted()" @click="column.clearSorting()">
<ResetIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Reset
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

<div v-else :class="$attrs.class">
{{ title }}
</div>
</template>
Loading