mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat(Table): add select event (#2822)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
committed by
GitHub
parent
99bdbdeec1
commit
0668a399dc
@@ -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>
|
||||||
@@ -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).
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user