feat(Table): add select event (#2822)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Christian López C
2025-02-28 06:05:48 -05:00
committed by GitHub
parent 99bdbdeec1
commit 0668a399dc
6 changed files with 470 additions and 294 deletions

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
}, {
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
}, {
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
}, {
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
}, {
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}])
const columns: TableColumn<Payment>[] = [{
id: 'select',
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'ariaLabel': 'Select row'
})
}, {
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
}, {
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = ({
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
})[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
}
}, {
accessorKey: 'email',
header: 'Email'
}, {
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}]
const table = useTemplateRef('table')
const rowSelection = ref<Record<string, boolean>>({ })
function onSelect(row: TableRow<Payment>, e?: Event) {
/* If you decide to also select the column you can do this */
row.toggleSelected(!row.getIsSelected())
console.log(e)
}
</script>
<template>
<div class=" flex w-full flex-1 gap-1">
<div class="flex-1">
<UTable
ref="table"
v-model:row-selection="rowSelection"
:data="data"
:columns="columns"
@select="onSelect"
/>
<div class="px-4 py-3.5 border-t border-[var(--ui-border-accented)] text-sm text-[var(--ui-text-muted)]">
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
</div>
</div>
</div>
</template>

View File

@@ -264,7 +264,7 @@ collapse: true
name: 'table-row-selection-example' name: 'table-row-selection-example'
highlights: highlights:
- 55 - 55
- 70 - 72
class: '!p-0' class: '!p-0'
--- ---
:: ::
@@ -273,6 +273,26 @@ class: '!p-0'
You can use the `row-selection` prop to control the selection state of the rows (can be binded with `v-model`). You can use the `row-selection` prop to control the selection state of the rows (can be binded with `v-model`).
:: ::
### With `@select` event
You can add a `@select` listener to make rows clickable. The handler function receives the `TableRow` instance as the first argument and an optional `Event` as the second argument.
::note
You can use this to navigate to a page, open a modal or even to select the row manually.
::
::component-example
---
prettier: true
collapse: true
name: 'table-row-selection-event-example'
highlights:
- 123
- 130
class: '!p-0'
---
::
### With column sorting ### With column sorting
You can update a column `header` to render a [Button](/components/button) component inside the `header` to toggle the sorting state using the TanStack Table [Sorting APIs](https://tanstack.com/table/latest/docs/api/features/sorting). You can update a column `header` to render a [Button](/components/button) component inside the `header` to toggle the sorting state using the TanStack Table [Sorting APIs](https://tanstack.com/table/latest/docs/api/features/sorting).

View File

@@ -56,8 +56,8 @@ const table = tv({ extend: tv(theme), ...(appConfigTable.ui?.table || {}) })
type TableVariants = VariantProps<typeof table> type TableVariants = VariantProps<typeof table>
export type TableRow<T> = Row<T>
export type TableData = RowData export type TableData = RowData
export type TableColumn<T extends TableData, D = unknown> = ColumnDef<T, D> export type TableColumn<T extends TableData, D = unknown> = ColumnDef<T, D>
export interface TableOptions<T extends TableData> extends Omit<CoreOptions<T>, 'data' | 'columns' | 'getCoreRowModel' | 'state' | 'onStateChange' | 'renderFallbackValue'> { export interface TableOptions<T extends TableData> extends Omit<CoreOptions<T>, 'data' | 'columns' | 'getCoreRowModel' | 'state' | 'onStateChange' | 'renderFallbackValue'> {
@@ -144,6 +144,7 @@ export interface TableProps<T extends TableData> extends TableOptions<T> {
* @link [Guide](https://tanstack.com/table/v8/docs/guide/column-faceting) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-faceting)
*/ */
facetedOptions?: FacetedOptions<T> facetedOptions?: FacetedOptions<T>
onSelect?: (row: TableRow<T>, e?: Event) => void
class?: any class?: any
ui?: Partial<typeof table.slots> ui?: Partial<typeof table.slots>
} }
@@ -276,6 +277,22 @@ function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue
} }
function handleRowSelect(row: TableRow<T>, e: Event) {
if (!props.onSelect) {
return
}
const target = e.target as HTMLElement
const isInteractive = target.closest('button')
if (isInteractive) {
return
}
e.preventDefault()
e.stopPropagation()
props.onSelect(row, e)
}
defineExpose({ defineExpose({
tableApi tableApi
}) })
@@ -308,7 +325,15 @@ defineExpose({
<tbody :class="ui.tbody({ class: [props.ui?.tbody] })"> <tbody :class="ui.tbody({ class: [props.ui?.tbody] })">
<template v-if="tableApi.getRowModel().rows?.length"> <template v-if="tableApi.getRowModel().rows?.length">
<template v-for="row in tableApi.getRowModel().rows" :key="row.id"> <template v-for="row in tableApi.getRowModel().rows" :key="row.id">
<tr :data-selected="row.getIsSelected()" :data-expanded="row.getIsExpanded()" :class="ui.tr({ class: [props.ui?.tr] })"> <tr
:data-selected="row.getIsSelected()"
:data-selectable="!!props.onSelect"
:data-expanded="row.getIsExpanded()"
:role="props.onSelect ? 'button' : undefined"
:tabindex="props.onSelect ? 0 : undefined"
:class="ui.tr({ class: [props.ui?.tr] })"
@click="handleRowSelect(row, $event)"
>
<td <td
v-for="cell in row.getVisibleCells()" v-for="cell in row.getVisibleCells()"
:key="cell.id" :key="cell.id"

View File

@@ -6,7 +6,7 @@ export default (options: Required<ModuleOptions>) => ({
base: 'min-w-full overflow-clip', base: 'min-w-full overflow-clip',
caption: 'sr-only', caption: 'sr-only',
thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)', thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)',
tbody: 'divide-y divide-(--ui-border)', tbody: 'divide-y divide-(--ui-border) [&>tr]:data-[selectable=true]:hover:bg-(--ui-bg-elevated)/50 [&>tr]:data-[selectable=true]:focus-visible:outline-(--ui-primary)',
tr: 'data-[selected=true]:bg-(--ui-bg-elevated)/50', tr: 'data-[selected=true]:bg-(--ui-bg-elevated)/50',
th: 'px-4 py-3.5 text-sm text-(--ui-text-highlighted) text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0', th: 'px-4 py-3.5 text-sm text-(--ui-text-highlighted) text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
td: 'p-4 text-sm text-(--ui-text-muted) whitespace-nowrap [&:has([role=checkbox])]:pe-0', td: 'p-4 text-sm text-(--ui-text-muted) whitespace-nowrap [&:has([role=checkbox])]:pe-0',

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff