feat(components): [cascader] lazyLoad support reject (#22283)

* feat(components): [cascader] `lazyLoad` support reject

* feat: update

* fix: update

* feat: remove unnecessary change

* test: add new test

* fix: update

* feat: update

* docs: update

* docs: update
This commit is contained in:
btea
2025-09-29 18:27:16 +08:00
committed by GitHub
parent 896815b986
commit 046815e33d
5 changed files with 173 additions and 20 deletions

View File

@@ -301,23 +301,23 @@ cascader/custom-header-footer
## CascaderProps ## CascaderProps
| Attribute | Description | Type | Default | | Attribute | Description | Type | Default |
| -------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------- | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------- |
| expandTrigger | trigger mode of expanding options | ^[enum]`'click' \| 'hover'` | click | | expandTrigger | trigger mode of expanding options | ^[enum]`'click' \| 'hover'` | click |
| multiple | whether multiple selection is enabled | ^[boolean] | false | | multiple | whether multiple selection is enabled | ^[boolean] | false |
| checkStrictly | whether checked state of a node not affects its parent and child nodes | ^[boolean] | false | | checkStrictly | whether checked state of a node not affects its parent and child nodes | ^[boolean] | false |
| emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | ^[boolean] | true | | emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | ^[boolean] | true |
| lazy | whether to dynamic load child nodes, use with `lazyload` attribute | ^[boolean] | false | | lazy | whether to dynamic load child nodes, use with `lazyload` attribute | ^[boolean] | false |
| lazyLoad | method for loading child nodes data, only works when `lazy` is true | ^[Function]`(node: Node, resolve: Resolve) => void` | — | | lazyLoad | method for loading child nodes data, only works when `lazy` is true. The reject parameter is supported after version ^(2.11.5). | ^[Function]`(node: Node, resolve: Resolve, reject: () => void) => void` | — |
| value | specify which key of node object is used as the node's value | ^[string] | value | | value | specify which key of node object is used as the node's value | ^[string] | value |
| label | specify which key of node object is used as the node's label | ^[string] | label | | label | specify which key of node object is used as the node's label | ^[string] | label |
| children | specify which key of node object is used as the node's children | ^[string] | children | | children | specify which key of node object is used as the node's children | ^[string] | children |
| disabled | specify which key of node object is used as the node's disabled | ^[string] | disabled | | disabled | specify which key of node object is used as the node's disabled | ^[string] | disabled |
| leaf | specify which key of node object is used as the node's leaf field | ^[string] | leaf | | leaf | specify which key of node object is used as the node's leaf field | ^[string] | leaf |
| hoverThreshold | hover threshold of expanding options | ^[number] | 500 | | hoverThreshold | hover threshold of expanding options | ^[number] | 500 |
| checkOnClickNode ^(2.10.5) | whether to check or uncheck node when clicking on the node | ^[boolean] | false | | checkOnClickNode ^(2.10.5) | whether to check or uncheck node when clicking on the node | ^[boolean] | false |
| checkOnClickLeaf ^(2.10.5) | whether to check or uncheck node when clicking on leaf node (last children). | ^[boolean] | true | | checkOnClickLeaf ^(2.10.5) | whether to check or uncheck node when clicking on leaf node (last children). | ^[boolean] | true |
| showPrefix ^(2.10.5) | whether to show the radio or checkbox prefix | ^[boolean] | true | | showPrefix ^(2.10.5) | whether to show the radio or checkbox prefix | ^[boolean] | true |
## Type Declarations ## Type Declarations
@@ -336,7 +336,7 @@ type Resolve = (data: any) => void
type ExpandTrigger = 'click' | 'hover' type ExpandTrigger = 'click' | 'hover'
type LazyLoad = (node: Node, resolve: Resolve) => void type LazyLoad = (node: Node, resolve: Resolve, reject: () => void) => void
type isDisabled = (data: CascaderOption, node: Node) => boolean type isDisabled = (data: CascaderOption, node: Node) => boolean

View File

@@ -598,6 +598,128 @@ describe('CascaderPanel.vue', () => {
vi.useRealTimers() vi.useRealTimers()
}) })
test('lazy load with loaded fails', async () => {
vi.useFakeTimers()
const value = ref([])
const props: CascaderProps = {
lazy: true,
lazyLoad(node, resolve, reject) {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 }).map(() => ({
value: ++id,
label: `option${id}`,
leaf: level >= 2,
}))
if (level === 1) {
// Simulate loading failure for the second level nodes
reject()
return
}
resolve(nodes)
}, 1000)
},
}
const wrapper = mount(() => (
<CascaderPanel v-model={value.value} props={props} />
))
vi.runAllTimers()
await nextTick()
const firstOption = wrapper.find(NODE)
expect(firstOption.exists()).toBe(true)
await firstOption.trigger('click')
expect(firstOption.findComponent(Loading).exists()).toBe(true)
vi.runAllTimers()
await nextTick()
expect(firstOption.findComponent(Loading).exists()).toBe(false)
expect(wrapper.findAll(MENU).length).toBe(1)
vi.useRealTimers()
})
test('lazy load with first level loaded fails', async () => {
vi.useFakeTimers()
const value = ref([])
const props: CascaderProps = {
lazy: true,
lazyLoad(node, resolve, reject) {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 }).map(() => ({
value: ++id,
label: `option${id}`,
leaf: level >= 2,
}))
if (level === 0) {
// Simulate loading failure for the first level nodes
reject()
return
}
resolve(nodes)
}, 1000)
},
}
const wrapper = mount(() => (
<CascaderPanel v-model={value.value} props={props} />
))
vi.runAllTimers()
await nextTick()
const firstOption = wrapper.find(NODE)
expect(firstOption.exists()).toBe(false)
expect(wrapper.findAll(MENU).length).toBe(1)
expect(wrapper.findComponent(Loading).exists()).toBe(false)
expect(wrapper.find('.is-empty').exists()).toBe(true)
vi.useRealTimers()
})
test('lazy load with first and second level loaded success and third level loaded fails', async () => {
vi.useFakeTimers()
const value = ref([])
const props: CascaderProps = {
lazy: true,
lazyLoad(node, resolve, reject) {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 }).map(() => ({
value: ++id,
label: `option${id}`,
leaf: level >= 2,
}))
if (level === 2) {
// Simulate loading failure for the second level nodes
reject()
return
}
resolve(nodes)
}, 1000)
},
}
const wrapper = mount(() => (
<CascaderPanel v-model={value.value} props={props} />
))
vi.runAllTimers()
await nextTick()
const firstOption = wrapper.find(NODE)
expect(firstOption.exists()).toBe(true)
await firstOption.trigger('click')
expect(firstOption.findComponent(Loading).exists()).toBe(true)
vi.runAllTimers()
await nextTick()
expect(firstOption.findComponent(Loading).exists()).toBe(false)
expect(wrapper.findAll(MENU).length).toBe(2)
const secondMenu = wrapper.findAll(MENU)[1]
const secondOption = secondMenu.find(NODE)
await secondOption.trigger('click')
expect(secondOption.findComponent(Loading).exists()).toBe(true)
vi.runAllTimers()
await nextTick()
expect(secondOption.findComponent(Loading).exists()).toBe(false)
expect(wrapper.findAll(MENU).length).toBe(2)
vi.useRealTimers()
})
test('lazy load with default primitive value', async () => { test('lazy load with default primitive value', async () => {
vi.useFakeTimers() vi.useFakeTimers()
const props = { const props = {

View File

@@ -83,6 +83,7 @@ const slots = useSlots()
let store: Store let store: Store
const initialLoaded = ref(true) const initialLoaded = ref(true)
const initialLoadedOnce = ref(false)
const menuList = ref<CascaderMenuInstance[]>([]) const menuList = ref<CascaderMenuInstance[]>([])
const checkedValue = ref<CascaderValue>() const checkedValue = ref<CascaderValue>()
const menus = ref<CascaderNode[][]>([]) const menus = ref<CascaderNode[][]>([])
@@ -128,9 +129,20 @@ const lazyLoad: ElCascaderPanelContext['lazyLoad'] = (node, cb) => {
_node.childrenData = _node.childrenData || [] _node.childrenData = _node.childrenData || []
dataList && store?.appendNodes(dataList, parent as Node) dataList && store?.appendNodes(dataList, parent as Node)
dataList && cb?.(dataList) dataList && cb?.(dataList)
if (node.level === 0) {
initialLoadedOnce.value = true
}
} }
cfg.lazyLoad(node, resolve) const reject = () => {
node!.loading = false
node!.loaded = false
if (node!.level === 0) {
initialLoaded.value = true
}
}
cfg.lazyLoad(node, resolve, reject)
} }
const expandNode: ElCascaderPanelContext['expandNode'] = (node, silent) => { const expandNode: ElCascaderPanelContext['expandNode'] = (node, silent) => {
@@ -376,6 +388,11 @@ watch(
} }
) )
const loadLazyRootNodes = () => {
if (initialLoadedOnce.value) return
initStore()
}
onBeforeUpdate(() => (menuList.value = [])) onBeforeUpdate(() => (menuList.value = []))
onMounted(() => !isEmpty(props.modelValue) && syncCheckedValue()) onMounted(() => !isEmpty(props.modelValue) && syncCheckedValue())
@@ -397,5 +414,6 @@ defineExpose({
clearCheckedNodes, clearCheckedNodes,
calculateCheckedValue, calculateCheckedValue,
scrollToExpandingNode, scrollToExpandingNode,
loadLazyRootNodes,
}) })
</script> </script>

View File

@@ -14,7 +14,11 @@ export type ExpandTrigger = 'click' | 'hover'
export type isDisabled = (data: CascaderOption, node: CascaderNode) => boolean export type isDisabled = (data: CascaderOption, node: CascaderNode) => boolean
export type isLeaf = (data: CascaderOption, node: CascaderNode) => boolean export type isLeaf = (data: CascaderOption, node: CascaderNode) => boolean
export type Resolve = (dataList?: CascaderOption[]) => void export type Resolve = (dataList?: CascaderOption[]) => void
export type LazyLoad = (node: CascaderNode, resolve: Resolve) => void export type LazyLoad = (
node: CascaderNode,
resolve: Resolve,
reject: () => void
) => void
export interface RenderLabelProps { export interface RenderLabelProps {
node: CascaderNode node: CascaderNode
data: CascaderOption data: CascaderOption

View File

@@ -749,6 +749,15 @@ watch(realSize, async () => {
watch(presentText, syncPresentTextValue, { immediate: true }) watch(presentText, syncPresentTextValue, { immediate: true })
watch(
() => popperVisible.value,
(val) => {
if (val && props.props.lazy && props.props.lazyLoad) {
cascaderPanelRef.value?.loadLazyRootNodes()
}
}
)
onMounted(() => { onMounted(() => {
const inputInner = inputRef.value!.input! const inputInner = inputRef.value!.input!