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!