Skip to content

Commit a60fd06

Browse files
committed
feat(data-table): add selection column support with checkboxes
- Implemented a selection column in the DataTable component allowing users to select multiple rows. - Added `checkedRowKeys` prop to manage selected rows. - Updated rendering logic to include checkboxes for selection. - Enhanced tests to cover selection functionality. - Modified documentation to include examples for selection usage.
1 parent c803978 commit a60fd06

9 files changed

Lines changed: 431 additions & 197 deletions

File tree

packages/varlet-ui/src/data-table/DataTable.vue

Lines changed: 178 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,31 @@
1414
<var-loading :loading="loading">
1515
<div :class="n('main')">
1616
<table :class="n('table')">
17+
<colgroup>
18+
<col
19+
v-for="(column, index) in columns"
20+
:key="column.key ?? column.type ?? index"
21+
:style="getColStyle(column)"
22+
/>
23+
</colgroup>
24+
1725
<thead v-if="columns.length">
1826
<tr :class="n('header-row')">
1927
<th
2028
v-for="column in columns"
21-
:key="column.key"
22-
:class="classes(n('cell'), n('header-cell'))"
29+
:key="column.key ?? column.type"
30+
:class="classes(n('cell'), n('header-cell'), [isSelectionColumn(column), n('selection-cell')])"
2331
:style="getHeaderCellStyle(column)"
2432
>
25-
{{ column.title }}
33+
<var-checkbox
34+
v-if="isSelectionColumn(column)"
35+
:model-value="allCurrentRowsSelected"
36+
:indeterminate="someCurrentRowsSelected"
37+
:disabled="!currentSelectableRows.length"
38+
tabindex="-1"
39+
@update:model-value="toggleAllCurrentRows"
40+
/>
41+
<template v-else>{{ column.title }}</template>
2642
</th>
2743
</tr>
2844
</thead>
@@ -31,24 +47,33 @@
3147
<tr
3248
v-for="(row, pageRowIndex) in currentData"
3349
:key="getRowKey(row, getAbsoluteRowIndex(pageRowIndex))"
34-
:class="classes(n('row'), [striped, n('row--striped')])"
50+
:class="n('row')"
3551
v-bind="getRowProps(row, pageRowIndex)"
3652
>
3753
<td
3854
v-for="column in columns"
39-
:key="column.key"
40-
:class="classes(n('cell'), n('body-cell'))"
55+
:key="column.key ?? column.type"
56+
:class="classes(n('cell'), n('body-cell'), [isSelectionColumn(column), n('selection-cell')])"
4157
:style="getBodyCellStyle(column)"
4258
v-bind="getCellProps(row, column, pageRowIndex)"
4359
>
44-
<MaybeVNode :is="renderCell(row, column, pageRowIndex)" tag="div" />
60+
<var-checkbox
61+
v-if="isSelectionColumn(column)"
62+
:model-value="isRowSelected(row, pageRowIndex)"
63+
:disabled="!isRowSelectable(row, pageRowIndex, column)"
64+
tabindex="-1"
65+
@update:model-value="toggleRowSelection(row, pageRowIndex, $event)"
66+
/>
67+
<MaybeVNode v-else :is="renderCell(row, column, pageRowIndex)" tag="div" />
4568
</td>
4669
</tr>
4770
</tbody>
4871
</table>
4972

5073
<div v-if="!currentData.length" :class="n('empty')">
51-
{{ resolvedEmptyText }}
74+
<slot name="empty">
75+
{{ resolvedEmptyText }}
76+
</slot>
5277
</div>
5378
</div>
5479

@@ -78,14 +103,23 @@
78103
</template>
79104

80105
<script lang="ts">
81-
import { call, isFunction, toNumber } from '@varlet/shared'
82-
import { computed, defineComponent, watch } from 'vue'
106+
import { call, callOrReturn, clamp, isFunction, toNumber } from '@varlet/shared'
107+
import { computed, defineComponent, ref, watch } from 'vue'
108+
import VarCheckbox from '../checkbox'
83109
import VarLoading from '../loading'
84110
import { t } from '../locale'
85111
import { injectLocaleProvider } from '../locale-provider/provide'
86112
import VarPagination from '../pagination'
87113
import { createNamespace, formatElevation, MaybeVNode } from '../utils/components'
88-
import { props, type DataTableAlign, type DataTableColumn, type DataTablePagination } from './props'
114+
import { toSizeUnit } from '../utils/elements'
115+
import {
116+
props,
117+
type DataTableAlign,
118+
type DataTableColumn,
119+
type DataTableKey,
120+
type DataTablePagination,
121+
type DataTableSelectionColumn,
122+
} from './props'
89123
90124
const { name, n, classes } = createNamespace('data-table')
91125
@@ -100,13 +134,15 @@ type NormalizedPaginationOptions = Required<
100134
export default defineComponent({
101135
name,
102136
components: {
137+
VarCheckbox,
103138
VarLoading,
104139
VarPagination,
105140
MaybeVNode,
106141
},
107142
props,
108143
setup(props) {
109144
const { t: pt } = injectLocaleProvider()
145+
const checkedRowKeys = ref<DataTableKey[]>([...props.checkedRowKeys])
110146
const defaultPaginationOptions: NormalizedPaginationOptions = {
111147
simple: false,
112148
disabled: false,
@@ -134,16 +170,16 @@ export default defineComponent({
134170
}
135171
})
136172
137-
const currentPage = computed(() => Math.max(1, toNumber(props.page) || 1))
138-
const currentPageSize = computed(() => Math.max(1, toNumber(props.pageSize) || 10))
173+
const currentPage = computed(() => clamp(toNumber(props.page) || 1, 1, Number.MAX_SAFE_INTEGER))
174+
const currentPageSize = computed(() => clamp(toNumber(props.pageSize) || 10, 1, Number.MAX_SAFE_INTEGER))
139175
const localTotal = computed(() => props.data.length)
140176
const paginationTotal = computed(() => {
141177
if (!paginationEnabled.value) {
142178
return props.data.length
143179
}
144180
145181
if (props.remote) {
146-
return Math.max(0, toNumber(props.total) || 0)
182+
return clamp(toNumber(props.total) || 0, 0, Number.MAX_SAFE_INTEGER)
147183
}
148184
149185
return localTotal.value
@@ -164,7 +200,7 @@ export default defineComponent({
164200
return 1
165201
}
166202
167-
return Math.min(currentPage.value, pageCount.value)
203+
return clamp(currentPage.value, 1, pageCount.value)
168204
})
169205
170206
const currentData = computed(() => {
@@ -178,7 +214,42 @@ export default defineComponent({
178214
return props.data.slice(start, end)
179215
})
180216
181-
const resolvedEmptyText = computed(() => props.emptyText || (pt ? pt : t)('selectEmptyText'))
217+
const resolvedEmptyText = computed(() => (pt ? pt : t)('selectEmptyText'))
218+
const selectionColumn = computed(() => props.columns.find(isSelectionColumn))
219+
const checkedRowKeySet = computed(() => new Set(checkedRowKeys.value))
220+
const currentSelectableRows = computed(() => {
221+
if (!selectionColumn.value) {
222+
return []
223+
}
224+
225+
return currentData.value
226+
.map((row, pageRowIndex) => ({
227+
row,
228+
pageRowIndex,
229+
rowIndex: getAbsoluteRowIndex(pageRowIndex),
230+
key: getRowKey(row, getAbsoluteRowIndex(pageRowIndex)),
231+
}))
232+
.filter(({ row, rowIndex, pageRowIndex }) =>
233+
isRowSelectable(row, pageRowIndex, selectionColumn.value!, rowIndex),
234+
)
235+
})
236+
const allCurrentRowsSelected = computed(
237+
() =>
238+
currentSelectableRows.value.length > 0 &&
239+
currentSelectableRows.value.every(({ key }) => checkedRowKeySet.value.has(key)),
240+
)
241+
const someCurrentRowsSelected = computed(
242+
() =>
243+
currentSelectableRows.value.some(({ key }) => checkedRowKeySet.value.has(key)) && !allCurrentRowsSelected.value,
244+
)
245+
246+
watch(
247+
() => props.checkedRowKeys,
248+
(value) => {
249+
checkedRowKeys.value = [...value]
250+
},
251+
{ deep: true },
252+
)
182253
183254
watch(
184255
[hasPagination, () => props.remote, currentPage, normalizedPage],
@@ -208,9 +279,62 @@ export default defineComponent({
208279
return row[props.rowKey] ?? rowIndex
209280
}
210281
282+
function isSelectionColumn(column: DataTableColumn): column is DataTableSelectionColumn {
283+
return column.type === 'selection'
284+
}
285+
286+
function isRowSelectable(
287+
row: Record<string, any>,
288+
pageRowIndex: number,
289+
column?: DataTableSelectionColumn,
290+
rowIndex = getAbsoluteRowIndex(pageRowIndex),
291+
) {
292+
if (!column?.selectable) {
293+
return true
294+
}
295+
296+
return column.selectable({
297+
row,
298+
rowIndex,
299+
pageRowIndex,
300+
})
301+
}
302+
303+
function updateCheckedRowKeys(value: DataTableKey[]) {
304+
checkedRowKeys.value = value
305+
call(props['onUpdate:checkedRowKeys'], value)
306+
}
307+
308+
function isRowSelected(row: Record<string, any>, pageRowIndex: number) {
309+
return checkedRowKeySet.value.has(getRowKey(row, getAbsoluteRowIndex(pageRowIndex)))
310+
}
311+
312+
function toggleRowSelection(row: Record<string, any>, pageRowIndex: number, selected: boolean) {
313+
const key = getRowKey(row, getAbsoluteRowIndex(pageRowIndex))
314+
const nextKeys = new Set(checkedRowKeys.value)
315+
316+
selected ? nextKeys.add(key) : nextKeys.delete(key)
317+
318+
updateCheckedRowKeys([...nextKeys])
319+
}
320+
321+
function toggleAllCurrentRows(selected: boolean) {
322+
const nextKeys = new Set(checkedRowKeys.value)
323+
324+
currentSelectableRows.value.forEach(({ key }) => {
325+
selected ? nextKeys.add(key) : nextKeys.delete(key)
326+
})
327+
328+
updateCheckedRowKeys([...nextKeys])
329+
}
330+
211331
const renderCell = (row: Record<string, any>, column: DataTableColumn, pageRowIndex: number) => {
212332
const rowIndex = getAbsoluteRowIndex(pageRowIndex)
213333
334+
if (isSelectionColumn(column)) {
335+
return undefined
336+
}
337+
214338
if (column.render) {
215339
return column.render({
216340
row,
@@ -230,15 +354,11 @@ export default defineComponent({
230354
231355
const rowIndex = getAbsoluteRowIndex(pageRowIndex)
232356
233-
if (isFunction(props.rowProps)) {
234-
return props.rowProps({
235-
row,
236-
rowIndex,
237-
pageRowIndex,
238-
})
239-
}
240-
241-
return props.rowProps
357+
return callOrReturn(props.rowProps, {
358+
row,
359+
rowIndex,
360+
pageRowIndex,
361+
})
242362
}
243363
244364
const getCellProps = (row: Record<string, any>, column: DataTableColumn, pageRowIndex: number) => {
@@ -248,29 +368,26 @@ export default defineComponent({
248368
249369
const rowIndex = getAbsoluteRowIndex(pageRowIndex)
250370
251-
if (isFunction(column.cellProps)) {
252-
return column.cellProps({
253-
row,
254-
rowIndex,
255-
pageRowIndex,
256-
column,
257-
})
258-
}
259-
260-
return column.cellProps
371+
return callOrReturn(column.cellProps, {
372+
row,
373+
rowIndex,
374+
pageRowIndex,
375+
column,
376+
})
261377
}
262378
263379
const getAlign = (align?: DataTableAlign) => align ?? 'left'
264380
381+
const getColStyle = (column: DataTableColumn) => ({
382+
width: column.width != null ? toSizeUnit(column.width) : isSelectionColumn(column) ? '52px' : undefined,
383+
minWidth: column.minWidth != null ? toSizeUnit(column.minWidth) : isSelectionColumn(column) ? '52px' : undefined,
384+
})
385+
265386
const getHeaderCellStyle = (column: DataTableColumn) => ({
266-
width: column.width != null ? toSizeUnit(column.width) : undefined,
267-
minWidth: column.minWidth != null ? toSizeUnit(column.minWidth) : undefined,
268387
textAlign: getAlign(column.titleAlign ?? column.align),
269388
})
270389
271390
const getBodyCellStyle = (column: DataTableColumn) => ({
272-
width: column.width != null ? toSizeUnit(column.width) : undefined,
273-
minWidth: column.minWidth != null ? toSizeUnit(column.minWidth) : undefined,
274391
textAlign: getAlign(column.align),
275392
})
276393
@@ -287,11 +404,20 @@ export default defineComponent({
287404
paginationTotal,
288405
resolvedEmptyText,
289406
hasPagination,
407+
allCurrentRowsSelected,
408+
someCurrentRowsSelected,
290409
getAbsoluteRowIndex,
291410
getRowKey,
292411
getRowProps,
293412
getCellProps,
413+
isSelectionColumn,
414+
isRowSelectable,
415+
isRowSelected,
416+
toggleAllCurrentRows,
417+
toggleRowSelection,
418+
currentSelectableRows,
294419
renderCell,
420+
getColStyle,
295421
getHeaderCellStyle,
296422
getBodyCellStyle,
297423
handlePaginationChange,
@@ -306,5 +432,18 @@ export default defineComponent({
306432
<style lang="less">
307433
@import '../styles/common';
308434
@import '../styles/elevation';
435+
@import '../ripple/ripple';
436+
@import '../form-details/formDetails';
437+
@import '../icon/icon';
438+
@import '../hover-overlay/hoverOverlay';
439+
@import '../checkbox/checkbox';
440+
@import '../loading/loading';
441+
@import '../menu/menu';
442+
@import '../menu-select/menuSelect';
443+
@import '../menu-option/menuOption';
444+
@import '../cell/cell';
445+
@import '../field-decorator/fieldDecorator';
446+
@import '../input/input';
447+
@import '../pagination/pagination';
309448
@import './dataTable';
310449
</style>

0 commit comments

Comments
 (0)