docs: add sub components example
This commit is contained in:
parent
8444039cb3
commit
8291ef452d
|
|
@ -1368,3 +1368,41 @@ const columns = computed(() => props.table.getAllColumns()
|
|||
```vue
|
||||
<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>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
<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 DataTable from './components/DataTable.vue'
|
||||
import UserNav from './components/UserNav.vue'
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -31,6 +40,6 @@ import { columns } from './components/columns'
|
|||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
<DataTable :data="tasks" :columns="columns" />
|
||||
<DataTable :data="tasks" :columns="columns" :render-sub-component="renderSubComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="TData, TValue">
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
ExpandedState,
|
||||
Row,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
} from '@tanstack/vue-table'
|
||||
|
|
@ -17,7 +19,7 @@ import {
|
|||
} from '@tanstack/vue-table'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import type { Task } from '../data/schema'
|
||||
import type { VNode } from 'vue'
|
||||
import DataTablePagination from './DataTablePagination.vue'
|
||||
import DataTableToolbar from './DataTableToolbar.vue'
|
||||
import { valueUpdater } from '@/lib/utils'
|
||||
|
|
@ -31,8 +33,9 @@ import {
|
|||
} from '@/lib/registry/new-york/ui/table'
|
||||
|
||||
interface DataTableProps {
|
||||
columns: ColumnDef<Task, any>[]
|
||||
data: Task[]
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
renderSubComponent: (row: Row<TData>) => VNode
|
||||
}
|
||||
const props = defineProps<DataTableProps>()
|
||||
|
||||
|
|
@ -40,6 +43,7 @@ const sorting = ref<SortingState>([])
|
|||
const columnFilters = ref<ColumnFiltersState>([])
|
||||
const columnVisibility = ref<VisibilityState>({})
|
||||
const rowSelection = ref({})
|
||||
const expanded = ref<ExpandedState>({})
|
||||
|
||||
const table = useVueTable({
|
||||
get data() { return props.data },
|
||||
|
|
@ -49,12 +53,14 @@ const table = useVueTable({
|
|||
get columnFilters() { return columnFilters.value },
|
||||
get columnVisibility() { return columnVisibility.value },
|
||||
get rowSelection() { return rowSelection.value },
|
||||
get expanded() { return expanded.value },
|
||||
},
|
||||
enableRowSelection: true,
|
||||
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),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
|
|
@ -78,15 +84,18 @@ const table = useVueTable({
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
<template v-if="table.getRowModel().rows?.length">
|
||||
<TableRow
|
||||
v-for="row in table.getRowModel().rows"
|
||||
:key="row.id"
|
||||
: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>
|
||||
<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()" class="shadow-inner">
|
||||
<TableCell :colspan="row.getAllCells().length">
|
||||
<FlexRender :render="renderSubComponent(row)" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<TableRow v-else>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="TData">
|
||||
import type { Column } from '@tanstack/vue-table'
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import type { Task } from '../data/schema'
|
||||
import PlusCircledIcon from '~icons/radix-icons/plus-circled'
|
||||
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'
|
||||
|
||||
interface DataTableFacetedFilter {
|
||||
column?: Column<Task, any>
|
||||
column?: Column<TData, any>
|
||||
title?: string
|
||||
options: {
|
||||
label: string
|
||||
|
|
@ -32,6 +31,22 @@ const props = defineProps<DataTableFacetedFilter>()
|
|||
|
||||
const facets = computed(() => props.column?.getFacetedUniqueValues())
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -74,7 +89,7 @@ const selectedValues = computed(() => new Set(props.column?.getFilterValue() as
|
|||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command
|
||||
:filter-function="(list: DataTableFacetedFilter['options'], term) => list.filter(i => i.label.toLowerCase()?.includes(term)) "
|
||||
:filter-function="filterFunction"
|
||||
>
|
||||
<CommandInput :placeholder="title" />
|
||||
<CommandList>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="TData">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
import type { Task } from '../data/schema'
|
||||
import ChevronLeftIcon from '~icons/radix-icons/chevron-left'
|
||||
import ChevronRightIcon from '~icons/radix-icons/chevron-right'
|
||||
import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left'
|
||||
|
|
@ -16,7 +15,7 @@ import {
|
|||
} from '@/lib/registry/new-york/ui/select'
|
||||
|
||||
interface DataTablePaginationProps {
|
||||
table: Table<Task>
|
||||
table: Table<TData>
|
||||
}
|
||||
defineProps<DataTablePaginationProps>()
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -24,8 +24,13 @@ import {
|
|||
interface DataTableRowActionsProps {
|
||||
row: Row<Task>
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableRowActionsProps>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'expand'): void
|
||||
}>()
|
||||
|
||||
const task = computed(() => taskSchema.parse(props.row.original))
|
||||
</script>
|
||||
|
||||
|
|
@ -56,6 +61,9 @@ const task = computed(() => taskSchema.parse(props.row.original))
|
|||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="$emit('expand')">
|
||||
Expand
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Delete
|
||||
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="TData">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
import { computed } from 'vue'
|
||||
import type { Task } from '../data/schema'
|
||||
|
||||
import { priorities, statuses } from '../data/data'
|
||||
import DataTableFacetedFilter from './DataTableFacetedFilter.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'
|
||||
|
||||
interface DataTableToolbarProps {
|
||||
table: Table<Task>
|
||||
table: Table<TData>
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableToolbarProps>()
|
||||
|
|
|
|||
43
apps/www/src/examples/tasks/components/DataTableUser.vue
Normal file
43
apps/www/src/examples/tasks/components/DataTableUser.vue
Normal 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>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="TData">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
import { computed } from 'vue'
|
||||
import type { Task } from '../data/schema'
|
||||
import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
|
||||
|
||||
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||
|
|
@ -15,7 +14,7 @@ import {
|
|||
} from '@/lib/registry/new-york/ui/dropdown-menu'
|
||||
|
||||
interface DataTableViewOptionsProps {
|
||||
table: Table<Task>
|
||||
table: Table<TData>
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableViewOptionsProps>()
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ export const columns: ColumnDef<Task>[] = [
|
|||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => h(DataTableRowActions, { row }),
|
||||
cell: ({ row }) => {
|
||||
return h(DataTableRowActions, { row, onExpand: row.toggleExpanded })
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ export const taskSchema = z.object({
|
|||
status: z.string(),
|
||||
label: 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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user