diff --git a/docs/en-US/component/cascader.md b/docs/en-US/component/cascader.md index e0d5b0b2d5..3a5159a138 100644 --- a/docs/en-US/component/cascader.md +++ b/docs/en-US/component/cascader.md @@ -301,23 +301,23 @@ cascader/custom-header-footer ## CascaderProps -| Attribute | Description | Type | Default | -| -------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------- | -| expandTrigger | trigger mode of expanding options | ^[enum]`'click' \| 'hover'` | click | -| multiple | whether multiple selection is enabled | ^[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 | -| 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` | — | -| 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 | -| 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 | -| 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 | -| 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 | -| showPrefix ^(2.10.5) | whether to show the radio or checkbox prefix | ^[boolean] | true | +| Attribute | Description | Type | Default | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------- | +| expandTrigger | trigger mode of expanding options | ^[enum]`'click' \| 'hover'` | click | +| multiple | whether multiple selection is enabled | ^[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 | +| 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. 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 | +| 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 | +| 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 | +| 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 | +| 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 | ## Type Declarations @@ -336,7 +336,7 @@ type Resolve = (data: any) => void 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 diff --git a/packages/components/cascader-panel/__tests__/cascader-panel.test.tsx b/packages/components/cascader-panel/__tests__/cascader-panel.test.tsx index e92e16a940..5a765ba557 100644 --- a/packages/components/cascader-panel/__tests__/cascader-panel.test.tsx +++ b/packages/components/cascader-panel/__tests__/cascader-panel.test.tsx @@ -598,6 +598,128 @@ describe('CascaderPanel.vue', () => { 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(() => ( + + )) + + 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(() => ( + + )) + + 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(() => ( + + )) + + 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 () => { vi.useFakeTimers() const props = { diff --git a/packages/components/cascader-panel/src/index.vue b/packages/components/cascader-panel/src/index.vue index f41c3461e8..2aa379b1eb 100644 --- a/packages/components/cascader-panel/src/index.vue +++ b/packages/components/cascader-panel/src/index.vue @@ -83,6 +83,7 @@ const slots = useSlots() let store: Store const initialLoaded = ref(true) +const initialLoadedOnce = ref(false) const menuList = ref([]) const checkedValue = ref() const menus = ref([]) @@ -128,9 +129,20 @@ const lazyLoad: ElCascaderPanelContext['lazyLoad'] = (node, cb) => { _node.childrenData = _node.childrenData || [] dataList && store?.appendNodes(dataList, parent as Node) 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) => { @@ -376,6 +388,11 @@ watch( } ) +const loadLazyRootNodes = () => { + if (initialLoadedOnce.value) return + initStore() +} + onBeforeUpdate(() => (menuList.value = [])) onMounted(() => !isEmpty(props.modelValue) && syncCheckedValue()) @@ -397,5 +414,6 @@ defineExpose({ clearCheckedNodes, calculateCheckedValue, scrollToExpandingNode, + loadLazyRootNodes, }) diff --git a/packages/components/cascader-panel/src/types.ts b/packages/components/cascader-panel/src/types.ts index 4fcdb47e8e..0f504083ab 100644 --- a/packages/components/cascader-panel/src/types.ts +++ b/packages/components/cascader-panel/src/types.ts @@ -14,7 +14,11 @@ export type ExpandTrigger = 'click' | 'hover' export type isDisabled = (data: CascaderOption, node: CascaderNode) => boolean export type isLeaf = (data: CascaderOption, node: CascaderNode) => boolean 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 { node: CascaderNode data: CascaderOption diff --git a/packages/components/cascader/src/cascader.vue b/packages/components/cascader/src/cascader.vue index 283af3b2b9..702dac8906 100644 --- a/packages/components/cascader/src/cascader.vue +++ b/packages/components/cascader/src/cascader.vue @@ -749,6 +749,15 @@ watch(realSize, async () => { watch(presentText, syncPresentTextValue, { immediate: true }) +watch( + () => popperVisible.value, + (val) => { + if (val && props.props.lazy && props.props.lazyLoad) { + cascaderPanelRef.value?.loadLazyRootNodes() + } + } +) + onMounted(() => { const inputInner = inputRef.value!.input!