diff --git a/docs/en-US/component/table.md b/docs/en-US/component/table.md
index d9d21124e5..3bc9352afc 100644
--- a/docs/en-US/component/table.md
+++ b/docs/en-US/component/table.md
@@ -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) | — |
diff --git a/docs/examples/table/check-strictly.vue b/docs/examples/table/check-strictly.vue
new file mode 100644
index 0000000000..542c611564
--- /dev/null
+++ b/docs/examples/table/check-strictly.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/components/table/__tests__/table.test.ts b/packages/components/table/__tests__/table.test.ts
index 2410ab5af3..4126dcf402 100644
--- a/packages/components/table/__tests__/table.test.ts
+++ b/packages/components/table/__tests__/table.test.ts
@@ -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: `
+
+
+
+
+
+
+
+ `,
+ 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: `
-
-
-
-
-
-
-
- `,
- 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: {
diff --git a/packages/components/table/src/store/helper.ts b/packages/components/table/src/store/helper.ts
index bb6022f8fa..025d836536 100644
--- a/packages/components/table/src/store/helper.ts
+++ b/packages/components/table/src/store/helper.ts
@@ -21,6 +21,10 @@ const InitialStateMap = {
key: 'childrenColumnName',
default: 'children',
},
+ ['treeProps.checkStrictly']: {
+ key: 'checkStrictly',
+ default: false,
+ },
}
export function createStore(table: Table, props: TableProps) {
diff --git a/packages/components/table/src/store/tree.ts b/packages/components/table/src/store/tree.ts
index b11e31a704..bb6cfc807d 100644
--- a/packages/components/table/src/store/tree.ts
+++ b/packages/components/table/src/store/tree.ts
@@ -13,6 +13,7 @@ function useTree(watcherData: WatcherPropsData) {
const lazyTreeNodeMap = ref({})
const lazyColumnIdentifier = ref('hasChildren')
const childrenColumnName = ref('children')
+ const checkStrictly = ref(false)
const instance = getCurrentInstance() as Table
const normalizedData = computed(() => {
if (!watcherData.rowKey.value) return {}
@@ -223,6 +224,7 @@ function useTree(watcherData: WatcherPropsData) {
lazyTreeNodeMap,
lazyColumnIdentifier,
childrenColumnName,
+ checkStrictly,
},
}
}
diff --git a/packages/components/table/src/store/watcher.ts b/packages/components/table/src/store/watcher.ts
index 6552b23006..47a527026c 100644
--- a/packages/components/table/src/store/watcher.ts
+++ b/packages/components/table/src/store/watcher.ts
@@ -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() {
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() {
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() {
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
diff --git a/packages/components/table/src/table/defaults.ts b/packages/components/table/src/table/defaults.ts
index f1e860ca02..ecbb0eee3f 100644
--- a/packages/components/table/src/table/defaults.ts
+++ b/packages/components/table/src/table/defaults.ts
@@ -37,6 +37,12 @@ interface TableState {
debouncedUpdateLayout: () => void
}
+interface TreeProps {
+ hasChildren?: string
+ children?: string
+ checkStrictly?: boolean
+}
+
type HoverState = Nullable<{
cell: HTMLElement
column: TableColumnCtx
@@ -134,10 +140,7 @@ interface TableProps {
| 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,
}
diff --git a/packages/components/table/src/util.ts b/packages/components/table/src/util.ts
index 8bf4ddba6f..b68e21f9f4 100644
--- a/packages/components/table/src/util.ts
+++ b/packages/components/table/src/util.ts
@@ -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(
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(
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
}