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:
daoyi.tian
2024-08-05 12:10:17 +08:00
committed by GitHub
parent ef54dfbb35
commit 48dfe3a69a
8 changed files with 269 additions and 103 deletions

View File

@@ -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) | — |

View 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>

View File

@@ -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: {

View File

@@ -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>) {

View File

@@ -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,
},
}
}

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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
}