docs: add sub components example

This commit is contained in:
Shl 2024-07-07 22:47:59 +07:00
parent 8444039cb3
commit 8291ef452d
12 changed files with 956 additions and 129 deletions

View File

@ -1368,3 +1368,41 @@ const columns = computed(() => props.table.getAllColumns()
```vue ```vue
<DataTableViewOptions :table="table" /> <DataTableViewOptions :table="table" />
``` ```
### Expanding
Let's add the display of arbitrary content in expandable rows
```vue
<script setup lang="ts" generic="TData, TValue">
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
renderSubComponent: (row: Row<TData>) => VNode
}>()
</script>
<template>
<!-- Table -->
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
<FlexRender :render="renderSubComponent(row)" />
</TableCell>
</TableRow>
<!-- Table -->
</template>
```
Now we are not bound to columns content, we can render any component with any content
```vue
<script setup lang="ts">
function renderSubComponent(row: Row<Task>): VNode {
return h(User, { row })
}
</script>
<template>
<DataTable :data="tasks" :columns="columns" :render-sub-component="renderSubComponent" />
</template>
```

View File

@ -1,8 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Row } from '@tanstack/vue-table'
import type { VNode } from 'vue'
import { h } from 'vue'
import tasks from './data/tasks.json' import tasks from './data/tasks.json'
import DataTable from './components/DataTable.vue' import DataTable from './components/DataTable.vue'
import UserNav from './components/UserNav.vue' import UserNav from './components/UserNav.vue'
import { columns } from './components/columns' import { columns } from './components/columns'
import type { Task } from './data/schema'
import User from './components/DataTableUser.vue'
function renderSubComponent(row: Row<Task>): VNode {
return h(User, { row })
}
</script> </script>
<template> <template>
@ -31,6 +40,6 @@ import { columns } from './components/columns'
<UserNav /> <UserNav />
</div> </div>
</div> </div>
<DataTable :data="tasks" :columns="columns" /> <DataTable :data="tasks" :columns="columns" :render-sub-component="renderSubComponent" />
</div> </div>
</template> </template>

View File

@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts" generic="TData, TValue">
import type { import type {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
ExpandedState,
Row,
SortingState, SortingState,
VisibilityState, VisibilityState,
} from '@tanstack/vue-table' } from '@tanstack/vue-table'
@ -17,7 +19,7 @@ import {
} from '@tanstack/vue-table' } from '@tanstack/vue-table'
import { ref } from 'vue' import { ref } from 'vue'
import type { Task } from '../data/schema' import type { VNode } from 'vue'
import DataTablePagination from './DataTablePagination.vue' import DataTablePagination from './DataTablePagination.vue'
import DataTableToolbar from './DataTableToolbar.vue' import DataTableToolbar from './DataTableToolbar.vue'
import { valueUpdater } from '@/lib/utils' import { valueUpdater } from '@/lib/utils'
@ -31,8 +33,9 @@ import {
} from '@/lib/registry/new-york/ui/table' } from '@/lib/registry/new-york/ui/table'
interface DataTableProps { interface DataTableProps {
columns: ColumnDef<Task, any>[] columns: ColumnDef<TData, TValue>[]
data: Task[] data: TData[]
renderSubComponent: (row: Row<TData>) => VNode
} }
const props = defineProps<DataTableProps>() const props = defineProps<DataTableProps>()
@ -40,6 +43,7 @@ const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([]) const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({}) const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({}) const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({ const table = useVueTable({
get data() { return props.data }, get data() { return props.data },
@ -49,12 +53,14 @@ const table = useVueTable({
get columnFilters() { return columnFilters.value }, get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value }, get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value }, get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
}, },
enableRowSelection: true, enableRowSelection: true,
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection), onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
@ -78,15 +84,18 @@ const table = useVueTable({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<template v-if="table.getRowModel().rows?.length"> <template v-if="table.getRowModel().rows?.length">
<TableRow <template v-for="row in table.getRowModel().rows" :key="row.id">
v-for="row in table.getRowModel().rows" <TableRow :data-state="row.getIsSelected() && 'selected'">
:key="row.id"
:data-state="row.getIsSelected() && 'selected'"
>
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id"> <TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow v-if="row.getIsExpanded()" class="shadow-inner">
<TableCell :colspan="row.getAllCells().length">
<FlexRender :render="renderSubComponent(row)" />
</TableCell>
</TableRow>
</template>
</template> </template>
<TableRow v-else> <TableRow v-else>

View File

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts" generic="TData">
import type { Column } from '@tanstack/vue-table' import type { Column } from '@tanstack/vue-table'
import type { Component } from 'vue' import type { Component } from 'vue'
import { computed } from 'vue' import { computed } from 'vue'
import type { Task } from '../data/schema'
import PlusCircledIcon from '~icons/radix-icons/plus-circled' import PlusCircledIcon from '~icons/radix-icons/plus-circled'
import CheckIcon from '~icons/radix-icons/check' import CheckIcon from '~icons/radix-icons/check'
@ -19,7 +18,7 @@ import { Separator } from '@/lib/registry/new-york/ui/separator'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
interface DataTableFacetedFilter { interface DataTableFacetedFilter {
column?: Column<Task, any> column?: Column<TData, any>
title?: string title?: string
options: { options: {
label: string label: string
@ -32,6 +31,22 @@ const props = defineProps<DataTableFacetedFilter>()
const facets = computed(() => props.column?.getFacetedUniqueValues()) const facets = computed(() => props.column?.getFacetedUniqueValues())
const selectedValues = computed(() => new Set(props.column?.getFilterValue() as string[])) const selectedValues = computed(() => new Set(props.column?.getFilterValue() as string[]))
type ReturnTypeFilterFunction = string[] | number[] | false[] | true[] | Record<string, any>[]
function isListOptions(list: ReturnTypeFilterFunction | DataTableFacetedFilter['options']): list is DataTableFacetedFilter['options'] {
return Array.isArray(list) && list.length > 0 && 'value' in (list[0] as DataTableFacetedFilter['options'])
}
function filterFunction(list: ReturnTypeFilterFunction | DataTableFacetedFilter['options'], term: string): DataTableFacetedFilter['options'] {
if (isListOptions(list)) {
return list.filter(i => i.label.toLowerCase()?.includes(term))
}
else {
console.error('List is not a DataTableFacetedFilter options')
return []
}
}
</script> </script>
<template> <template>
@ -74,7 +89,7 @@ const selectedValues = computed(() => new Set(props.column?.getFilterValue() as
</PopoverTrigger> </PopoverTrigger>
<PopoverContent class="w-[200px] p-0" align="start"> <PopoverContent class="w-[200px] p-0" align="start">
<Command <Command
:filter-function="(list: DataTableFacetedFilter['options'], term) => list.filter(i => i.label.toLowerCase()?.includes(term)) " :filter-function="filterFunction"
> >
<CommandInput :placeholder="title" /> <CommandInput :placeholder="title" />
<CommandList> <CommandList>

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts" generic="TData">
import type { Table } from '@tanstack/vue-table' import type { Table } from '@tanstack/vue-table'
import type { Task } from '../data/schema'
import ChevronLeftIcon from '~icons/radix-icons/chevron-left' import ChevronLeftIcon from '~icons/radix-icons/chevron-left'
import ChevronRightIcon from '~icons/radix-icons/chevron-right' import ChevronRightIcon from '~icons/radix-icons/chevron-right'
import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left' import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left'
@ -16,7 +15,7 @@ import {
} from '@/lib/registry/new-york/ui/select' } from '@/lib/registry/new-york/ui/select'
interface DataTablePaginationProps { interface DataTablePaginationProps {
table: Table<Task> table: Table<TData>
} }
defineProps<DataTablePaginationProps>() defineProps<DataTablePaginationProps>()
</script> </script>

View File

@ -24,8 +24,13 @@ import {
interface DataTableRowActionsProps { interface DataTableRowActionsProps {
row: Row<Task> row: Row<Task>
} }
const props = defineProps<DataTableRowActionsProps>() const props = defineProps<DataTableRowActionsProps>()
defineEmits<{
(e: 'expand'): void
}>()
const task = computed(() => taskSchema.parse(props.row.original)) const task = computed(() => taskSchema.parse(props.row.original))
</script> </script>
@ -56,6 +61,9 @@ const task = computed(() => taskSchema.parse(props.row.original))
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem @click="$emit('expand')">
Expand
</DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem>
Delete Delete
<DropdownMenuShortcut></DropdownMenuShortcut> <DropdownMenuShortcut></DropdownMenuShortcut>

View File

@ -1,8 +1,6 @@
<script setup lang="ts"> <script setup lang="ts" generic="TData">
import type { Table } from '@tanstack/vue-table' import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue' import { computed } from 'vue'
import type { Task } from '../data/schema'
import { priorities, statuses } from '../data/data' import { priorities, statuses } from '../data/data'
import DataTableFacetedFilter from './DataTableFacetedFilter.vue' import DataTableFacetedFilter from './DataTableFacetedFilter.vue'
import DataTableViewOptions from './DataTableViewOptions.vue' import DataTableViewOptions from './DataTableViewOptions.vue'
@ -11,7 +9,7 @@ import { Button } from '@/lib/registry/new-york/ui/button'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
interface DataTableToolbarProps { interface DataTableToolbarProps {
table: Table<Task> table: Table<TData>
} }
const props = defineProps<DataTableToolbarProps>() const props = defineProps<DataTableToolbarProps>()

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import type { Row } from '@tanstack/vue-table'
import { CheckIcon, Mail } from 'lucide-vue-next'
import { useClipboard } from '@vueuse/core'
import type { Task } from '../data/schema'
import { Avatar, AvatarFallback, AvatarImage } from '@/lib/registry/new-york/ui/avatar'
import { Badge } from '@/lib/registry/new-york/ui/badge'
import { Button } from '@/lib/registry/new-york/ui/button'
const props = defineProps<{
row: Row<Task>
}>()
const { id, firstName, lastName, email, avatar } = props.row.original.user
const { copy, copied } = useClipboard({ source: email })
</script>
<template>
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<Avatar class="h-9 w-9">
<AvatarImage :src="avatar" alt="User image" />
<AvatarFallback>{{ firstName.slice(0, 1) }}</AvatarFallback>
</Avatar>
<span>{{ `${firstName} ${lastName}` }}</span>
<Button
class="h-7 w-7 [&_svg]:size-3.5"
variant="outline"
size="icon"
@click="copy()"
>
<span class="sr-only">Copy</span>
<CheckIcon v-if="copied" />
<Mail v-else />
</Button>
</div>
<Badge class="mr-2" variant="outline">
#{{ id }}
</Badge>
</div>
</template>

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts" generic="TData">
import type { Table } from '@tanstack/vue-table' import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue' import { computed } from 'vue'
import type { Task } from '../data/schema'
import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal' import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
@ -15,7 +14,7 @@ import {
} from '@/lib/registry/new-york/ui/dropdown-menu' } from '@/lib/registry/new-york/ui/dropdown-menu'
interface DataTableViewOptionsProps { interface DataTableViewOptionsProps {
table: Table<Task> table: Table<TData>
} }
const props = defineProps<DataTableViewOptionsProps>() const props = defineProps<DataTableViewOptionsProps>()

View File

@ -84,6 +84,8 @@ export const columns: ColumnDef<Task>[] = [
}, },
{ {
id: 'actions', id: 'actions',
cell: ({ row }) => h(DataTableRowActions, { row }), cell: ({ row }) => {
return h(DataTableRowActions, { row, onExpand: row.toggleExpanded })
},
}, },
] ]

View File

@ -8,6 +8,13 @@ export const taskSchema = z.object({
status: z.string(), status: z.string(),
label: z.string(), label: z.string(),
priority: z.string(), priority: z.string(),
user: z.object({
id: z.number(),
firstName: z.string(),
lastName: z.string(),
email: z.string().email(),
avatar: z.string(),
}),
}) })
export type Task = z.infer<typeof taskSchema> export type Task = z.infer<typeof taskSchema>

File diff suppressed because it is too large Load Diff