mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
feat(components): [table] tree children add check strictly (#13519)
* feat(components): [table] tree children add check strictly * fix(components): [table] remove utils console * fix(components): [table] error of selector state * docs: update * fix: remove unnecessary changes * fix: the toggleRowStatus has not been processed to selectable * docs: update * fix: the rowIndex error --------- Co-authored-by: qiang <qw13131wang@gmail.com>
This commit is contained in:
@@ -199,6 +199,14 @@ table/tree-and-lazy
|
||||
|
||||
:::
|
||||
|
||||
## Selectable tree ^(2.8.0)
|
||||
|
||||
:::demo When `treeProps.checkStrictly` is true, the selection state of parent and child nodes is no longer associated, that is, when the parent node is selected, its child nodes will not be selected; when `treeProps.checkStrictly` is false, the selection state of parent and child nodes will be associated with the selection state of child nodes, that is, when the parent node is selected, all its child nodes will be selected.
|
||||
|
||||
table/check-strictly
|
||||
|
||||
:::
|
||||
|
||||
## Summary row
|
||||
|
||||
For table of numbers, you can add an extra row at the table footer displaying each column's sum.
|
||||
@@ -278,7 +286,7 @@ table/table-layout
|
||||
| indent | horizontal indentation of tree data | ^[number] | 16 |
|
||||
| lazy | whether to lazy loading data | ^[boolean] | false |
|
||||
| load | method for loading child row data, only works when `lazy` is true | ^[Function]`(row: any, treeNode: TreeNode, resolve: (data: any[]) => void) => void` | — |
|
||||
| tree-props | configuration for rendering nested data | ^[object]`{ hasChildren?: string, children?: string }` | ^[object]`{ hasChildren: 'hasChildren', children: 'children' }` |
|
||||
| tree-props | configuration for rendering nested data | ^[object]`{ hasChildren?: string, children?: string, checkStrictly?: boolean }` | ^[object]`{ hasChildren: 'hasChildren', children: 'children', checkStrictly: false }` |
|
||||
| table-layout | sets the algorithm used to lay out table cells, rows, and columns | ^[enum]`'fixed' \| 'auto'` | fixed |
|
||||
| scrollbar-always-on | always show scrollbar | ^[boolean] | false |
|
||||
| show-overflow-tooltip | whether to hide extra content and show them in a tooltip when hovering on the cell.It will affect all the table columns, refer to table [tooltip-options](#table-attributes) | ^[boolean] / [`object`](#table-attributes) ^(2.3.7) | — |
|
||||
|
||||
77
docs/examples/table/check-strictly.vue
Normal file
77
docs/examples/table/check-strictly.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<el-radio-group v-model="treeProps.checkStrictly">
|
||||
<el-radio-button :value="true" label="true" />
|
||||
<el-radio-button :value="false" label="false" />
|
||||
</el-radio-group>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:tree-props="treeProps"
|
||||
row-key="id"
|
||||
default-expand-all
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="selectable" />
|
||||
<el-table-column prop="date" label="Date" />
|
||||
<el-table-column prop="name" label="Name" />
|
||||
<el-table-column prop="address" label="Address" />
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
date: string
|
||||
name: string
|
||||
address: string
|
||||
hasChildren?: boolean
|
||||
children?: User[]
|
||||
}
|
||||
|
||||
const treeProps = reactive({
|
||||
checkStrictly: false,
|
||||
})
|
||||
|
||||
const selectable = (row: User) => ![1, 31].includes(row.id)
|
||||
|
||||
const tableData: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
date: '2016-05-02',
|
||||
name: 'wangxiaohu',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '2016-05-04',
|
||||
name: 'wangxiaohu',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: '2016-05-01',
|
||||
name: 'wangxiaohu',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
children: [
|
||||
{
|
||||
id: 31,
|
||||
date: '2016-05-01',
|
||||
name: 'wangxiaohu',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
date: '2016-05-01',
|
||||
name: 'wangxiaohu',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
date: '2016-05-03',
|
||||
name: 'wangxiaohu',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ElCheckbox from '@element-plus/components/checkbox'
|
||||
import triggerEvent from '@element-plus/test-utils/trigger-event'
|
||||
@@ -1784,6 +1784,69 @@ describe('Table.vue', () => {
|
||||
const firstCellSpanAfterHide = wrapper.find('.el-table__body tr td span')
|
||||
expect(firstCellSpanAfterHide.classes().includes('release')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('selectable tree', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
},
|
||||
template: `
|
||||
<el-table :data="testData" :tree-props="treeProps" @selection-change="change">
|
||||
<el-table-column type="selection" />
|
||||
<el-table-column prop="name" label="name" />
|
||||
<el-table-column prop="release" label="release" />
|
||||
<el-table-column prop="director" label="director" />
|
||||
<el-table-column prop="runtime" label="runtime" />
|
||||
</el-table>
|
||||
`,
|
||||
data() {
|
||||
const treeProps = ref({
|
||||
children: 'childrenTest',
|
||||
checkStrictly: false,
|
||||
})
|
||||
const testData = getTestData() as any
|
||||
testData[1].childrenTest = [
|
||||
{
|
||||
name: "A Bug's Life copy 1",
|
||||
release: '1998-11-25-1',
|
||||
director: 'John Lasseter',
|
||||
runtime: 95,
|
||||
},
|
||||
{
|
||||
name: "A Bug's Life copy 2",
|
||||
release: '1998-11-25-2',
|
||||
director: 'John Lasseter',
|
||||
runtime: 95,
|
||||
},
|
||||
]
|
||||
return {
|
||||
treeProps,
|
||||
testData,
|
||||
selected: [],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(rows) {
|
||||
this.selected = rows
|
||||
},
|
||||
},
|
||||
})
|
||||
await doubleWait()
|
||||
wrapper.findAll('.el-checkbox')[2].trigger('click')
|
||||
await doubleWait()
|
||||
expect(wrapper.vm.selected.length).toEqual(3)
|
||||
wrapper.findAll('.el-checkbox')[2].trigger('click')
|
||||
await doubleWait()
|
||||
expect(wrapper.vm.selected.length).toEqual(0)
|
||||
|
||||
await (wrapper.vm.treeProps.checkStrictly = true)
|
||||
await doubleWait()
|
||||
wrapper.findAll('.el-checkbox')[2].trigger('click')
|
||||
await doubleWait()
|
||||
expect(wrapper.vm.selected.length).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('when tableLayout is auto', async () => {
|
||||
@@ -1837,54 +1900,7 @@ describe('Table.vue', () => {
|
||||
'min-width: 0'
|
||||
)
|
||||
})
|
||||
it('selectable tree', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
},
|
||||
template: `
|
||||
<el-table :data="testData" @selection-change="change">
|
||||
<el-table-column type="selection" />
|
||||
<el-table-column prop="name" label="name" />
|
||||
<el-table-column prop="release" label="release" />
|
||||
<el-table-column prop="director" label="director" />
|
||||
<el-table-column prop="runtime" label="runtime" />
|
||||
</el-table>
|
||||
`,
|
||||
data() {
|
||||
const testData = getTestData() as any
|
||||
testData[1].children = [
|
||||
{
|
||||
name: "A Bug's Life copy 1",
|
||||
release: '1998-11-25-1',
|
||||
director: 'John Lasseter',
|
||||
runtime: 95,
|
||||
},
|
||||
{
|
||||
name: "A Bug's Life copy 2",
|
||||
release: '1998-11-25-2',
|
||||
director: 'John Lasseter',
|
||||
runtime: 95,
|
||||
},
|
||||
]
|
||||
return {
|
||||
testData,
|
||||
selected: [],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(rows) {
|
||||
this.selected = rows
|
||||
},
|
||||
},
|
||||
})
|
||||
await doubleWait()
|
||||
wrapper.findAll('.el-checkbox')[2].trigger('click')
|
||||
await doubleWait()
|
||||
expect(wrapper.vm.selected.length).toEqual(3)
|
||||
})
|
||||
it('change columns order when use v-for & key to render table', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
|
||||
@@ -21,6 +21,10 @@ const InitialStateMap = {
|
||||
key: 'childrenColumnName',
|
||||
default: 'children',
|
||||
},
|
||||
['treeProps.checkStrictly']: {
|
||||
key: 'checkStrictly',
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
|
||||
export function createStore<T>(table: Table<T>, props: TableProps<T>) {
|
||||
|
||||
@@ -13,6 +13,7 @@ function useTree<T>(watcherData: WatcherPropsData<T>) {
|
||||
const lazyTreeNodeMap = ref({})
|
||||
const lazyColumnIdentifier = ref('hasChildren')
|
||||
const childrenColumnName = ref('children')
|
||||
const checkStrictly = ref(false)
|
||||
const instance = getCurrentInstance() as Table<T>
|
||||
const normalizedData = computed(() => {
|
||||
if (!watcherData.rowKey.value) return {}
|
||||
@@ -223,6 +224,7 @@ function useTree<T>(watcherData: WatcherPropsData<T>) {
|
||||
lazyTreeNodeMap,
|
||||
lazyColumnIdentifier,
|
||||
childrenColumnName,
|
||||
checkStrictly,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import useTree from './tree'
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
import type { TableColumnCtx } from '../table-column/defaults'
|
||||
import type { Table, TableRefs } from '../table/defaults'
|
||||
import type { DefaultRow, Table, TableRefs } from '../table/defaults'
|
||||
import type { StoreFilter } from '.'
|
||||
|
||||
const sortData = (data, states) => {
|
||||
@@ -193,7 +193,17 @@ function useWatcher<T>() {
|
||||
selected?: boolean,
|
||||
emitChange = true
|
||||
) => {
|
||||
const changed = toggleRowStatus(selection.value, row, selected)
|
||||
const treeProps = {
|
||||
children: instance?.store?.states?.childrenColumnName.value,
|
||||
checkStrictly: instance?.store?.states?.checkStrictly.value,
|
||||
}
|
||||
const changed = toggleRowStatus(
|
||||
selection.value,
|
||||
row,
|
||||
selected,
|
||||
treeProps,
|
||||
selectable.value
|
||||
)
|
||||
if (changed) {
|
||||
const newSelection = (selection.value || []).slice()
|
||||
// 调用 API 修改选中值,不触发 select 事件
|
||||
@@ -215,19 +225,25 @@ function useWatcher<T>() {
|
||||
let selectionChanged = false
|
||||
let childrenCount = 0
|
||||
const rowKey = instance?.store?.states?.rowKey.value
|
||||
const { childrenColumnName } = instance.store.states
|
||||
const treeProps = {
|
||||
children: childrenColumnName.value,
|
||||
checkStrictly: false, // Disable checkStrictly when selecting all
|
||||
}
|
||||
|
||||
data.value.forEach((row, index) => {
|
||||
const rowIndex = index + childrenCount
|
||||
if (selectable.value) {
|
||||
if (
|
||||
selectable.value.call(null, row, rowIndex) &&
|
||||
toggleRowStatus(selection.value, row, value)
|
||||
) {
|
||||
selectionChanged = true
|
||||
}
|
||||
} else {
|
||||
if (toggleRowStatus(selection.value, row, value)) {
|
||||
selectionChanged = true
|
||||
}
|
||||
if (
|
||||
toggleRowStatus(
|
||||
selection.value,
|
||||
row,
|
||||
value,
|
||||
treeProps,
|
||||
selectable.value,
|
||||
rowIndex
|
||||
)
|
||||
) {
|
||||
selectionChanged = true
|
||||
}
|
||||
childrenCount += getChildrenCount(getRowIdentity(row, rowKey))
|
||||
})
|
||||
@@ -259,42 +275,49 @@ function useWatcher<T>() {
|
||||
return
|
||||
}
|
||||
|
||||
let selectedMap
|
||||
if (rowKey.value) {
|
||||
selectedMap = getKeysMap(selection.value, rowKey.value)
|
||||
}
|
||||
const isSelected = function (row) {
|
||||
const { childrenColumnName } = instance.store.states
|
||||
const selectedMap = rowKey.value
|
||||
? getKeysMap(selection.value, rowKey.value)
|
||||
: undefined
|
||||
|
||||
let rowIndex = 0
|
||||
let selectedCount = 0
|
||||
|
||||
const isSelected = (row: DefaultRow) => {
|
||||
if (selectedMap) {
|
||||
return !!selectedMap[getRowIdentity(row, rowKey.value)]
|
||||
} else {
|
||||
return selection.value.includes(row)
|
||||
}
|
||||
}
|
||||
let isAllSelected_ = true
|
||||
let selectedCount = 0
|
||||
let childrenCount = 0
|
||||
for (let i = 0, j = (data.value || []).length; i < j; i++) {
|
||||
const keyProp = instance?.store?.states?.rowKey.value
|
||||
const rowIndex = i + childrenCount
|
||||
const item = data.value[i]
|
||||
const isRowSelectable =
|
||||
selectable.value && selectable.value.call(null, item, rowIndex)
|
||||
if (!isSelected(item)) {
|
||||
if (!selectable.value || isRowSelectable) {
|
||||
isAllSelected_ = false
|
||||
break
|
||||
const checkSelectedStatus = (data: DefaultRow[]) => {
|
||||
for (const row of data) {
|
||||
const isRowSelectable =
|
||||
selectable.value && selectable.value.call(null, row, rowIndex)
|
||||
|
||||
if (!isSelected(row)) {
|
||||
if (!selectable.value || isRowSelectable) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
selectedCount++
|
||||
}
|
||||
rowIndex++
|
||||
|
||||
if (
|
||||
row[childrenColumnName.value]?.length &&
|
||||
!checkSelectedStatus(row[childrenColumnName.value])
|
||||
) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
selectedCount++
|
||||
}
|
||||
childrenCount += getChildrenCount(getRowIdentity(item, keyProp))
|
||||
return true
|
||||
}
|
||||
|
||||
if (selectedCount === 0) isAllSelected_ = false
|
||||
isAllSelected.value = isAllSelected_
|
||||
const isAllSelected_ = checkSelectedStatus(data.value || [])
|
||||
isAllSelected.value = selectedCount === 0 ? false : isAllSelected_
|
||||
}
|
||||
|
||||
// gets the number of all child nodes by rowKey
|
||||
const getChildrenCount = (rowKey: string) => {
|
||||
if (!instance || !instance.store) return 0
|
||||
const { treeData } = instance.store.states
|
||||
|
||||
@@ -37,6 +37,12 @@ interface TableState {
|
||||
debouncedUpdateLayout: () => void
|
||||
}
|
||||
|
||||
interface TreeProps {
|
||||
hasChildren?: string
|
||||
children?: string
|
||||
checkStrictly?: boolean
|
||||
}
|
||||
|
||||
type HoverState<T> = Nullable<{
|
||||
cell: HTMLElement
|
||||
column: TableColumnCtx<T>
|
||||
@@ -134,10 +140,7 @@ interface TableProps<T> {
|
||||
| undefined
|
||||
selectOnIndeterminate?: boolean
|
||||
indent?: number
|
||||
treeProps?: {
|
||||
hasChildren?: string
|
||||
children?: string
|
||||
}
|
||||
treeProps?: TreeProps
|
||||
lazy?: boolean
|
||||
load?: (row: T, treeNode: TreeNode, resolve: (data: T[]) => void) => void
|
||||
className?: string
|
||||
@@ -344,6 +347,7 @@ export default {
|
||||
return {
|
||||
hasChildren: 'hasChildren',
|
||||
children: 'children',
|
||||
checkStrictly: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -399,4 +403,5 @@ export type {
|
||||
Sort,
|
||||
Filter,
|
||||
TableColumnCtx,
|
||||
TreeProps,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import ElTooltip, {
|
||||
type ElTooltipProps,
|
||||
} from '@element-plus/components/tooltip'
|
||||
import type { Table } from './table/defaults'
|
||||
import type { Table, TreeProps } from './table/defaults'
|
||||
import type { TableColumnCtx } from './table-column/defaults'
|
||||
|
||||
export type TableOverflowTooltipOptions = Partial<
|
||||
@@ -263,11 +263,16 @@ export function compose(...funcs) {
|
||||
export function toggleRowStatus<T>(
|
||||
statusArr: T[],
|
||||
row: T,
|
||||
newVal?: boolean
|
||||
newVal?: boolean,
|
||||
tableTreeProps?: TreeProps,
|
||||
selectable?: (row: T, index?: number) => boolean,
|
||||
rowIndex?: number
|
||||
): boolean {
|
||||
let _rowIndex = rowIndex ?? 0
|
||||
let changed = false
|
||||
const index = statusArr.indexOf(row)
|
||||
const included = index !== -1
|
||||
const isRowSelectable = selectable?.call(null, row, rowIndex)
|
||||
|
||||
const toggleStatus = (type: 'add' | 'remove') => {
|
||||
if (type === 'add') {
|
||||
@@ -276,21 +281,47 @@ export function toggleRowStatus<T>(
|
||||
statusArr.splice(index, 1)
|
||||
}
|
||||
changed = true
|
||||
if (isArray(row.children)) {
|
||||
row.children.forEach((item) => {
|
||||
toggleRowStatus(statusArr, item, newVal ?? !included)
|
||||
}
|
||||
const getChildrenCount = (row: T) => {
|
||||
let count = 0
|
||||
const children = tableTreeProps?.children && row[tableTreeProps.children]
|
||||
if (children && isArray(children)) {
|
||||
count += children.length
|
||||
children.forEach((item) => {
|
||||
count += getChildrenCount(item)
|
||||
})
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
if (!selectable || isRowSelectable) {
|
||||
if (isBoolean(newVal)) {
|
||||
if (newVal && !included) {
|
||||
toggleStatus('add')
|
||||
} else if (!newVal && included) {
|
||||
toggleStatus('remove')
|
||||
}
|
||||
} else {
|
||||
included ? toggleStatus('remove') : toggleStatus('add')
|
||||
}
|
||||
}
|
||||
|
||||
if (isBoolean(newVal)) {
|
||||
if (newVal && !included) {
|
||||
toggleStatus('add')
|
||||
} else if (!newVal && included) {
|
||||
toggleStatus('remove')
|
||||
}
|
||||
} else {
|
||||
included ? toggleStatus('remove') : toggleStatus('add')
|
||||
if (
|
||||
!tableTreeProps?.checkStrictly &&
|
||||
tableTreeProps?.children &&
|
||||
isArray(row[tableTreeProps.children])
|
||||
) {
|
||||
row[tableTreeProps.children].forEach((item) => {
|
||||
toggleRowStatus(
|
||||
statusArr,
|
||||
item,
|
||||
newVal ?? !included,
|
||||
tableTreeProps,
|
||||
selectable,
|
||||
_rowIndex + 1
|
||||
)
|
||||
_rowIndex += getChildrenCount(item) + 1
|
||||
})
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user