Skip to content

Commit d6daf46

Browse files
rdjanuarbenjamincanac
andauthoredNov 8, 2024··
feat(Table): allow dynamically render checkbox (#2549)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
1 parent 6e66990 commit d6daf46

File tree

4 files changed

+224
-36
lines changed

4 files changed

+224
-36
lines changed
 

‎docs/components/content/examples/TableExampleAdvanced.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script lang="ts" setup>
22
// Columns
33
const columns = [{
4+
key: 'select',
5+
class: 'w-2'
6+
}, {
47
key: 'id',
58
label: '#',
69
sortable: true
@@ -20,6 +23,7 @@ const columns = [{
2023
2124
const selectedColumns = ref(columns)
2225
const columnsTable = computed(() => columns.filter(column => selectedColumns.value.includes(column)))
26+
const excludeSelectColumn = computed(() => columns.filter(v => v.key !== 'select'))
2327
2428
// Selected Rows
2529
const selectedRows = ref([])
@@ -153,7 +157,7 @@ const { data: todos, status } = await useLazyAsyncData<{
153157
</UButton>
154158
</UDropdown>
155159

156-
<USelectMenu v-model="selectedColumns" :options="columns" multiple>
160+
<USelectMenu v-model="selectedColumns" :options="excludeSelectColumn" multiple>
157161
<UButton
158162
icon="i-heroicons-view-columns"
159163
color="gray"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script setup lang="ts">
2+
const people = [{
3+
id: 1,
4+
name: 'Lindsay Walton',
5+
title: 'Front-end Developer',
6+
email: 'lindsay.walton@example.com',
7+
role: 'Member'
8+
}, {
9+
id: 2,
10+
name: 'Courtney Henry',
11+
title: 'Designer',
12+
email: 'courtney.henry@example.com',
13+
role: 'Admin'
14+
}, {
15+
id: 3,
16+
name: 'Tom Cook',
17+
title: 'Director of Product',
18+
email: 'tom.cook@example.com',
19+
role: 'Member'
20+
}, {
21+
id: 4,
22+
name: 'Whitney Francis',
23+
title: 'Copywriter',
24+
email: 'whitney.francis@example.com',
25+
role: 'Admin'
26+
}, {
27+
id: 5,
28+
name: 'Leonard Krasner',
29+
title: 'Senior Designer',
30+
email: 'leonard.krasner@example.com',
31+
role: 'Owner'
32+
}, {
33+
id: 6,
34+
name: 'Floyd Miles',
35+
title: 'Principal Designer',
36+
email: 'floyd.miles@example.com',
37+
role: 'Member'
38+
}]
39+
40+
const selected = ref([people[1]])
41+
42+
const columns = [{
43+
key: 'id',
44+
label: 'ID'
45+
}, {
46+
key: 'name',
47+
label: 'User name'
48+
}, {
49+
key: 'title',
50+
label: 'Job position'
51+
}, {
52+
key: 'email',
53+
label: 'Email'
54+
}, {
55+
key: 'role'
56+
}, {
57+
key: 'select',
58+
class: 'w-2'
59+
}]
60+
</script>
61+
62+
<template>
63+
<UTable v-model="selected" :rows="people" :columns="columns" />
64+
</template>

‎docs/content/2.components/table.md

+99-1
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,29 @@ componentProps:
285285
---
286286
::
287287

288+
#### Single Select Mode
289+
Control how the select function allows only one row to be selected at a time.
290+
291+
```vue
292+
<template>
293+
<!-- Allow only one row to be selectable at a time -->
294+
<UTable :single-select="true" />
295+
</template>
296+
```
297+
298+
#### Checkbox Placement
299+
You can customize the checkbox column position by using the `select` key in the `columns` configuration.
300+
301+
::component-example{class="grid"}
302+
---
303+
extraClass: 'overflow-hidden'
304+
padding: false
305+
component: 'table-example-dynamically-render-selectable'
306+
componentProps:
307+
class: 'flex-1'
308+
---
309+
::
310+
288311
### Contextmenu
289312

290313
Use the `contextmenu` listener on your Table to make the rows righ-clickable. The function will receive the original event as the first argument and the row as the second argument.
@@ -393,7 +416,6 @@ Controls whether multiple rows can be expanded simultaneously in the table.
393416
<!-- Or simply -->
394417
<UTable />
395418
</template>
396-
397419
```
398420

399421
#### Disable Row Expansion
@@ -534,6 +556,82 @@ componentProps:
534556
---
535557
::
536558

559+
### `select-header`
560+
This slot allows you to customize the checkbox appearance in the table header for selecting all rows at once while using feature [Selectable](#selectable).
561+
562+
#### Usage
563+
```vue
564+
<template>
565+
<UTable v-model="selectable">
566+
<template #select-header="{ checked, change, indeterminate }">
567+
<!-- Place your custom component here -->
568+
</template>
569+
</UTable>
570+
</template>
571+
```
572+
573+
#### Props
574+
575+
| Prop | Type | Description |
576+
|------|------|-------------|
577+
| `checked` | `Boolean` | Indicates if all rows are selected |
578+
| `change` | `Function` | Function to handle selection state changes. Must receive a boolean value (true/false) |
579+
| `indeterminate` | `Boolean` | Indicates partial selection (when some rows are selected) |
580+
581+
#### Example
582+
```vue
583+
<template>
584+
<UTable>
585+
<!-- Header checkbox customization -->
586+
<template #select-header="{ indeterminate, checked, change }">
587+
<input
588+
type="checkbox"
589+
:indeterminate="indeterminate"
590+
:checked="checked"
591+
@change="e => change(e.target.checked)"
592+
/>
593+
</template>
594+
</UTable>
595+
</template>
596+
```
597+
598+
### `select-data`
599+
This slot allows you to customize the checkbox appearance for each row in the table while using feature [Selectable](#selectable).
600+
601+
#### Usage
602+
```vue
603+
<template>
604+
<UTable v-model="selectable">
605+
<template #select-data="{ checked, change }">
606+
<!-- Place your custom component here -->
607+
</template>
608+
</UTable>
609+
</template>
610+
```
611+
612+
#### Props
613+
614+
| Prop | Type | Description |
615+
|------|------|-------------|
616+
| `checked` | `Boolean` | Indicates if the current row is selected |
617+
| `change` | `Function` | Function to handle selection state changes. Must receive a boolean value (true/false) |
618+
619+
#### Example
620+
```vue
621+
<template>
622+
<UTable>
623+
<!-- Row checkbox customization -->
624+
<template #select-data="{ checked, change }">
625+
<input
626+
type="checkbox"
627+
:checked="checked"
628+
@change="e => change(e.target.checked)"
629+
/>
630+
</template>
631+
</UTable>
632+
</template>
633+
```
634+
537635
### `expand-action`
538636

539637
The `#expand-action` slot allows you to customize the expansion control interface for expandable table rows. This feature provides a flexible way to implement custom expand/collapse functionality while maintaining access to essential row data and state.

‎src/runtime/components/data/Table.vue

+56-34
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,27 @@
88
</slot>
99
<thead :class="ui.thead">
1010
<tr :class="ui.tr.base">
11-
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding">
12-
<UCheckbox
13-
:model-value="isAllRowChecked"
14-
:indeterminate="indeterminate"
15-
v-bind="ui.default.checkbox"
16-
aria-label="Select all"
17-
@change="onChange"
18-
/>
19-
</th>
20-
2111
<th v-if="expand" scope="col" :class="ui.tr.base">
2212
<span class="sr-only">Expand</span>
2313
</th>
24-
2514
<th
2615
v-for="(column, index) in columns"
2716
:key="index"
2817
scope="col"
29-
:class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.class]"
18+
:class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.key === 'select' && ui.checkbox.padding, column.class]"
3019
:aria-sort="getAriaSort(column)"
3120
>
32-
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
21+
<slot v-if="!singleSelect && modelValue && (column.key === 'select' || shouldRenderColumnInFirstPlace(index, 'select'))" name="select-header" :indeterminate="indeterminate" :checked="isAllRowChecked" :change="onChange">
22+
<UCheckbox
23+
:model-value="isAllRowChecked"
24+
:indeterminate="indeterminate"
25+
v-bind="ui.default.checkbox"
26+
aria-label="Select all"
27+
@change="onChange"
28+
/>
29+
</slot>
30+
31+
<slot v-else :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
3332
<UButton
3433
v-if="column.sortable"
3534
v-bind="{ ...(ui.default.sortButton || {}), ...sortButton }"
@@ -77,16 +76,7 @@
7776

7877
<template v-else>
7978
<template v-for="(row, index) in rows" :key="index">
80-
<tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, isExpanded(row) && ui.tr.expanded, ($attrs.onSelect || $attrs.onContextmenu) && ui.tr.active, row?.class]" @click="() => onSelect(row)" @contextmenu="(event) => onContextmenu(event, row)">
81-
<td v-if="modelValue" :class="ui.checkbox.padding">
82-
<UCheckbox
83-
:model-value="isSelected(row)"
84-
v-bind="ui.default.checkbox"
85-
aria-label="Select row"
86-
@change="onChangeCheckbox($event, row)"
87-
@click.capture.stop="() => onSelect(row)"
88-
/>
89-
</td>
79+
<tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, isExpanded(row) && ui.tr.expanded, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)" @contextmenu="(event) => onContextmenu(event, row)">
9080
<td
9181
v-if="expand"
9282
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
@@ -102,8 +92,25 @@
10292
@click.capture.stop="toggleOpened(row)"
10393
/>
10494
</td>
105-
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class]">
106-
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
95+
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class, column.key === 'select' && ui.checkbox.padding]">
96+
<slot v-if="modelValue && (column.key === 'select' || shouldRenderColumnInFirstPlace(subIndex, 'select')) " name="select-data" :checked="isSelected(row)" :change="(ev: boolean) => onChangeCheckbox(ev, row)">
97+
<UCheckbox
98+
:model-value="isSelected(row)"
99+
v-bind="ui.default.checkbox"
100+
aria-label="Select row"
101+
@change="onChangeCheckbox($event, row)"
102+
@click.capture.stop="() => onSelect(row)"
103+
/>
104+
</slot>
105+
106+
<slot
107+
v-else
108+
:name="`${column.key}-data`"
109+
:column="column"
110+
:row="row"
111+
:index="index"
112+
:get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)"
113+
>
107114
{{ getRowData(row, column.key) }}
108115
</slot>
109116
</td>
@@ -130,6 +137,7 @@ import type { PropType, AriaAttributes } from 'vue'
130137
import { upperFirst } from 'scule'
131138
import { defu } from 'defu'
132139
import { useVModel } from '@vueuse/core'
140+
import { isEqual } from 'ohash'
133141
import UIcon from '../elements/Icon.vue'
134142
import UButton from '../elements/Button.vue'
135143
import UProgress from '../elements/Progress.vue'
@@ -144,7 +152,7 @@ import { table } from '#ui/ui.config'
144152
const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table)
145153
146154
function defaultComparator<T>(a: T, z: T): boolean {
147-
return JSON.stringify(a) === JSON.stringify(z)
155+
return isEqual(a, z)
148156
}
149157
150158
function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
@@ -159,6 +167,14 @@ function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
159167
}
160168
}
161169
170+
function getStringifiedSet(arr: TableRow[]) {
171+
return new Set(arr.map(item => JSON.stringify(item)))
172+
}
173+
174+
function accessor<T extends Record<string, any>>(key: string) {
175+
return (obj: T) => get(obj, key)
176+
}
177+
162178
export default defineComponent({
163179
components: {
164180
UIcon,
@@ -247,6 +263,10 @@ export default defineComponent({
247263
multipleExpand: {
248264
type: Boolean,
249265
default: true
266+
},
267+
singleSelect: {
268+
type: Boolean,
269+
default: false
250270
}
251271
},
252272
emits: ['update:modelValue', 'update:sort', 'update:expand'],
@@ -292,8 +312,6 @@ export default defineComponent({
292312
}
293313
})
294314
295-
const getStringifiedSet = (arr: TableRow[]) => new Set(arr.map(item => JSON.stringify(item)))
296-
297315
const totalRows = computed(() => props.rows.length)
298316
299317
const countCheckedRow = computed(() => {
@@ -328,10 +346,6 @@ export default defineComponent({
328346
return props.by(a, z)
329347
}
330348
331-
function accessor<T extends Record<string, any>>(key: string) {
332-
return (obj: T) => get(obj, key)
333-
}
334-
335349
function isSelected(row: TableRow) {
336350
if (!props.modelValue) {
337351
return false
@@ -397,7 +411,7 @@ export default defineComponent({
397411
398412
function onChangeCheckbox(checked: boolean, row: TableRow) {
399413
if (checked) {
400-
selected.value.push(row)
414+
selected.value = props.singleSelect ? [row] : [...selected.value, row]
401415
} else {
402416
const index = selected.value.findIndex(item => compare(item, row))
403417
selected.value.splice(index, 1)
@@ -412,6 +426,13 @@ export default defineComponent({
412426
return expand.value?.openedRows ? expand.value.openedRows.some(openedRow => compare(openedRow, row)) : false
413427
}
414428
429+
function shouldRenderColumnInFirstPlace(index: number, key: string) {
430+
if (!props.columns) {
431+
return index === 0
432+
}
433+
return index === 0 && !props.columns.find(col => col.key === key)
434+
}
435+
415436
function toggleOpened(row: TableRow) {
416437
expand.value = {
417438
openedRows: isExpanded(row) ? expand.value.openedRows.filter(v => !compare(v, row)) : props.multipleExpand ? [...expand.value.openedRows, row] : [row],
@@ -465,7 +486,8 @@ export default defineComponent({
465486
getRowData,
466487
toggleOpened,
467488
getAriaSort,
468-
isExpanded
489+
isExpanded,
490+
shouldRenderColumnInFirstPlace
469491
}
470492
}
471493
})

0 commit comments

Comments
 (0)
Please sign in to comment.