diff --git a/src/tree/__tests__/api.test.jsx b/src/tree/__tests__/api.test.jsx index 400a2dedfc..6688c6b18c 100644 --- a/src/tree/__tests__/api.test.jsx +++ b/src/tree/__tests__/api.test.jsx @@ -123,7 +123,7 @@ describe('Tree:api', () => { expect(wrapper.find('[data-value="t1"]').text()).toBe('节点1'); }); - it('可以设置节点属性 checked,触发视图更新', async () => { + it('可以设置节点属性 checked, 触发视图更新', async () => { const data = [ { value: 't1', @@ -142,13 +142,37 @@ describe('Tree:api', () => { ], }, ]; + let changeParams = null; + let changeCount = 0; + const onChange = (checked, context) => { + changeCount += 1; + changeParams = { + checked, + context, + }; + }; const wrapper = mount({ + data() { + return { + checked: [], + }; + }, render() { - return ; + return ( + + ); }, }); - await delay(10); + await delay(1); const { tree } = wrapper.vm.$refs; tree.setItem('t1', { checked: true, @@ -156,14 +180,24 @@ describe('Tree:api', () => { tree.setItem('t2', { checked: true, }); - await delay(10); + + expect(wrapper.vm.checked.length).toBe(2); + expect(wrapper.vm.checked[0]).toBe('t1.1'); + expect(wrapper.vm.checked[1]).toBe('t2.1'); + await delay(1); expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-checked')).toBe(true); expect(wrapper.find('[data-value="t1.1"] .t-checkbox').classes('t-is-checked')).toBe(true); expect(wrapper.find('[data-value="t2"] .t-checkbox').classes('t-is-checked')).toBe(true); expect(wrapper.find('[data-value="t2.1"] .t-checkbox').classes('t-is-checked')).toBe(true); + + expect(changeCount).toBe(2); + expect(changeParams.checked.length).toBe(2); + expect(changeParams.checked[0]).toBe('t1.1'); + expect(changeParams.checked[1]).toBe('t2.1'); + expect(changeParams.context.node.value).toEqual('t2'); }); - it('可以设置节点属性 expanded,触发视图更新', async () => { + it('可以设置节点属性 expanded, 触发视图更新', async () => { const data = [ { value: 't1', @@ -182,13 +216,23 @@ describe('Tree:api', () => { ], }, ]; + + let expandParams = null; + let expandCount = 0; + const onExpand = (expanded, context) => { + expandCount += 1; + expandParams = { + expanded, + context, + }; + }; const wrapper = mount({ render() { - return ; + return ; }, }); - await delay(10); + await delay(1); const { tree } = wrapper.vm.$refs; tree.setItem('t1', { expanded: true, @@ -196,13 +240,80 @@ describe('Tree:api', () => { tree.setItem('t2', { expanded: true, }); - await delay(10); + await delay(1); const t1d1 = wrapper.find('[data-value="t1.1"]'); expect(t1d1.exists()).toBe(true); expect(t1d1.classes('t-tree__item--visible')).toBe(true); const t2d1 = wrapper.find('[data-value="t2.1"]'); expect(t2d1.exists()).toBe(true); expect(t2d1.classes('t-tree__item--visible')).toBe(true); + + expect(expandCount).toBe(2); + expect(expandParams.expanded.length).toBe(2); + expect(expandParams.expanded[0]).toBe('t1'); + expect(expandParams.expanded[1]).toBe('t2'); + expect(expandParams.context.node.value).toEqual('t2'); + }); + + it('可以设置节点属性 actived, 触发视图更新', async () => { + const data = [ + { + value: 't1', + children: [ + { + value: 't1.1', + }, + ], + }, + { + value: 't2', + children: [ + { + value: 't2.1', + }, + ], + }, + ]; + + let activeParams = null; + let activeCount = 0; + const onActive = (actived, context) => { + activeCount += 1; + activeParams = { + actived, + context, + }; + }; + const wrapper = mount({ + render() { + return ; + }, + }); + + await delay(1); + const t1d1 = wrapper.find('[data-value="t1.1"]'); + const t2d1 = wrapper.find('[data-value="t2.1"]'); + + const { tree } = wrapper.vm.$refs; + tree.setItem('t1.1', { + actived: true, + }); + expect(activeCount).toBe(1); + expect(activeParams.actived.length).toBe(1); + expect(activeParams.actived[0]).toBe('t1.1'); + expect(activeParams.context.node.value).toEqual('t1.1'); + await delay(1); + expect(t1d1.classes('t-is-active')).toBe(true); + + tree.setItem('t2.1', { + actived: true, + }); + expect(activeCount).toBe(2); + expect(activeParams.actived.length).toBe(1); + expect(activeParams.actived[0]).toBe('t2.1'); + expect(activeParams.context.node.value).toEqual('t2.1'); + await delay(1); + expect(t2d1.classes('t-is-active')).toBe(true); }); }); diff --git a/src/tree/__tests__/event.test.jsx b/src/tree/__tests__/event.test.jsx index 38aacc158a..2f67deb5e5 100644 --- a/src/tree/__tests__/event.test.jsx +++ b/src/tree/__tests__/event.test.jsx @@ -54,6 +54,7 @@ describe('Tree:props:events', () => { expect(rsActived.length).toBe(1); expect(rsActived[0]).toBe('t2'); expect(rsContext.node.value).toBe('t2'); + expect(rsContext.node.actived).toBe(true); }, 300); it('active 事件可触发', async () => { @@ -98,6 +99,7 @@ describe('Tree:props:events', () => { expect(rsActived.length).toBe(1); expect(rsActived[0]).toBe('t2'); expect(rsContext.node.value).toBe('t2'); + expect(rsContext.node.actived).toBe(true); }, 300); }); @@ -161,6 +163,7 @@ describe('Tree:props:events', () => { expect(rsExpanded.length).toBe(1); expect(rsExpanded[0]).toBe('t2'); expect(rsContext.node.value).toBe('t2'); + expect(rsContext.node.expanded).toBe(true); }, 300); it('expand 事件可触发', async () => { @@ -222,6 +225,7 @@ describe('Tree:props:events', () => { expect(rsExpanded.length).toBe(1); expect(rsExpanded[0]).toBe('t2'); expect(rsContext.node.value).toBe('t2'); + expect(rsContext.node.expanded).toBe(true); }, 300); }); @@ -286,6 +290,7 @@ describe('Tree:props:events', () => { expect(rsValue.length).toBe(1); expect(rsValue[0]).toBe('t2.1'); expect(rsContext.node.value).toBe('t2'); + expect(rsContext.node.checked).toBe(true); }, 300); it('change 事件可触发', async () => { @@ -348,6 +353,7 @@ describe('Tree:props:events', () => { expect(rsValue.length).toBe(1); expect(rsValue[0]).toBe('t2.1'); expect(rsContext.node.value).toBe('t2'); + expect(rsContext.node.checked).toBe(true); }, 300); }); diff --git a/src/tree/_example/checkable.vue b/src/tree/_example/checkable.vue index a49aa17223..9fa383e7c9 100644 --- a/src/tree/_example/checkable.vue +++ b/src/tree/_example/checkable.vue @@ -40,7 +40,7 @@ export default { valueMode: 'onlyLeaf', checkable: true, checkStrictly: false, - allChecked: ['1.1.1.1'], + allChecked: [], valueOptions: [ { value: 'onlyLeaf', @@ -149,19 +149,14 @@ export default { }, methods: { onClick(context) { - console.info('onClick:', context); + console.info('onClick context:', context); const { node } = context; - console.info(node.value, 'checked:', node.checked); + console.info(node.value, 'onClick context.node.checked:', node.checked); }, onChange(checked, context) { - console.info('onChange:', checked, context); + console.info('onChange checked:', checked, 'context:', context); const { node } = context; - console.info(node.value, 'checked:', node.checked); - }, - propOnChange(checked, context) { - console.info('propOnChange:', checked, context); - const { node } = context; - console.info(node.value, 'checked:', node.checked); + console.info(node.value, 'onChange context.node.checked:', node.checked); }, selectInvert() { const { tree } = this.$refs; diff --git a/src/tree/_example/controlled.vue b/src/tree/_example/controlled.vue index 3383f49a93..19af6726a5 100644 --- a/src/tree/_example/controlled.vue +++ b/src/tree/_example/controlled.vue @@ -169,39 +169,60 @@ export default { this.expanded = ['1', '1.2']; }, onClick(context) { - console.info('onClick:', context); - const { node } = context; - console.info(node.value, 'checked:', node.checked, 'expanded:', node.expanded, 'actived:', node.actived); + console.info('onClick context:', context); }, onChange(vals, context) { - console.info('onChange:', vals, context); - const checked = vals.filter((val) => val !== '2.1'); - console.info('节点 2.1 不允许选中'); + console.info('onChange value:', vals, 'context:', context); + const { node } = context; + // onChange 事件发生时,context.node 状态预先发生变更,此时拿到预先变更的节点状态 + console.info(node.value, 'context.node.checked:', node.checked); if (this.syncProps) { + const checked = vals.filter((val) => { + if (val === '2.1') { + console.info('节点 2.1 不允许选中'); + return false; + } + return true; + }); + // 受控状态下, tree 的 props.value 可被修改为预期的值 + console.log('before set this.checked, expect checked:', checked); this.checked = checked; } - const { node } = context; - console.info(node.value, 'checked:', node.checked); + // 赋值变更后的选中态之后,nextTick 之后触发视图更新 + // node.checked 状态发生变更,符合 tree 的 props.value 的取值 + this.$nextTick(() => { + console.info(node.value, 'nextTick context.node.checked:', node.checked); + }); }, onActive(vals, context) { - console.info('onActive:', vals, context); - const actived = vals.filter((val) => val !== '2.2'); - console.info('节点 2.2 不允许激活', actived); + console.info('onActive actived:', vals, 'context:', context); + const { node } = context; + console.info(node.value, 'context.node.actived:', node.actived); + const actived = vals.filter((val) => { + if (val === '2.2') { + console.info('节点 2.2 不允许激活'); + return false; + } + return true; + }); if (this.syncProps) { this.actived = actived; } - const { node } = context; - console.info(node.value, 'actived:', node.actived); }, onExpand(vals, context) { - console.info('onExpand:', vals, context); - const expanded = vals.filter((val) => val !== '2.3'); - console.info('节点 2.3 不允许展开', expanded); + console.info('onExpand expanded:', vals, 'context:', context); + const { node } = context; + console.info(node.value, 'context.node.expanded:', node.expanded); + const expanded = vals.filter((val) => { + if (val === '2.3') { + console.info('节点 2.3 不允许展开'); + return false; + } + return true; + }); if (this.syncProps) { this.expanded = expanded; } - const { node } = context; - console.info(node.value, 'expanded:', node.expanded); }, }, }; diff --git a/src/tree/hooks/useTreeAction.ts b/src/tree/hooks/useTreeAction.ts index a0a2135423..f8be96cb9f 100644 --- a/src/tree/hooks/useTreeAction.ts +++ b/src/tree/hooks/useTreeAction.ts @@ -15,13 +15,12 @@ export default function useTreeAction(state: TypeTreeState) { const { store } = treeState; const componentName = usePrefixClass('tree').value; - const [, setTValue] = state.vmValue; - const [, setTActived] = state.vmActived; - const [, setTExpanded] = state.vmExpanded; + const [tValue, setTValue] = state.vmValue; + const [tActived, setTActived] = state.vmActived; + const [tExpanded, setTExpanded] = state.vmExpanded; const setExpanded = (item: TypeTargetNode, isExpanded: boolean): TreeNodeValue[] => { const node = getNode(store, item); - const expanded = node.setExpanded(isExpanded); const mouseEvent = treeState.mouseEvent as MouseEvent; const evtCtx: TypeExpandEventContext = { node: node.getModel(), @@ -36,7 +35,13 @@ export default function useTreeAction(state: TypeTreeState) { evtCtx.trigger = 'icon-click'; } } + const expanded = node.setExpanded(isExpanded, { + directly: true, + }); setTExpanded(expanded, evtCtx); + if (evtCtx.trigger !== 'setItem') { + store.replaceExpanded(tExpanded.value as TreeNodeValue[]); + } return expanded; }; @@ -47,7 +52,6 @@ export default function useTreeAction(state: TypeTreeState) { const setActived = (item: TypeTargetNode, isActived: boolean) => { const node = getNode(store, item); - const actived = node.setActived(isActived); const mouseEvent = treeState.mouseEvent as MouseEvent; const evtCtx: TypeActiveEventContext = { node: node.getModel(), @@ -57,7 +61,13 @@ export default function useTreeAction(state: TypeTreeState) { if (mouseEvent) { evtCtx.trigger = 'node-click'; } + const actived = node.setActived(isActived, { + directly: true, + }); setTActived(actived, evtCtx); + if (evtCtx.trigger !== 'setItem') { + store.replaceActived(tActived.value as TreeNodeValue[]); + } return actived; }; @@ -68,7 +78,6 @@ export default function useTreeAction(state: TypeTreeState) { const setChecked = (item: TypeTargetNode, isChecked: boolean, ctx: { e: Event }): TreeNodeValue[] => { const node = getNode(store, item); - const checked = node.setChecked(isChecked); const mouseEvent = ctx?.e as MouseEvent; const evtCtx: TypeChangeEventContext = { node: node.getModel(), @@ -78,7 +87,14 @@ export default function useTreeAction(state: TypeTreeState) { if (mouseEvent) { evtCtx.trigger = 'node-click'; } + const checked = node.setChecked(isChecked, { + directly: true, + }); setTValue(checked, evtCtx); + // 这是针对受控执行的操作,如果 props.value 未变更,则执行还原操作 + if (evtCtx.trigger !== 'setItem') { + store.replaceChecked(tValue.value as TreeNodeValue[]); + } return checked; }; diff --git a/src/tree/hooks/useTreeStore.ts b/src/tree/hooks/useTreeStore.ts index d0447a3df4..64eaa6ffa2 100644 --- a/src/tree/hooks/useTreeStore.ts +++ b/src/tree/hooks/useTreeStore.ts @@ -186,25 +186,15 @@ export default function useTreeStore(state: TypeTreeState) { state.setStore(store); // 配置属性监听 - watch(refProps.value, (nVal: TreeNodeValue[]) => { - const previousVal = store.getChecked(); - if (nVal.join() === previousVal?.join()) return; - store.replaceChecked(nVal); - }); - watch(refProps.expanded, (nVal: TreeNodeValue[]) => { - store.replaceExpanded(nVal); - }); - watch(refProps.actived, (nVal: TreeNodeValue[]) => { - const previousVal = store.getActived(); - if (nVal.join() === previousVal?.join()) return; - store.replaceActived(nVal); - }); + // tValue 就是 refProps.value watch(tValue, (nVal: TreeNodeValue[]) => { store.replaceChecked(nVal); }); + // tExpanded 就是 refProps.expanded watch(tExpanded, (nVal: TreeNodeValue[]) => { store.replaceExpanded(nVal); }); + // tActived 就是 refProps.actived watch(tActived, (nVal: TreeNodeValue[]) => { store.replaceActived(nVal); }); diff --git a/src/tree/tree.tsx b/src/tree/tree.tsx index 96af345f89..3ad0b7ca07 100644 --- a/src/tree/tree.tsx +++ b/src/tree/tree.tsx @@ -99,12 +99,9 @@ export default defineComponent({ const val = spec[name]; delete spec[name]; const methodName = `set${upperFirst(name)}`; - const setupMethod = node[methodName]; + const setupMethod = this[methodName]; if (isFunction(setupMethod)) { - setupMethod.call(node, val, { - directly: true, - isAction: false, - }); + setupMethod.call(this, node, val); } } }); diff --git a/test/unit/snap/__snapshots__/csr.test.js.snap b/test/unit/snap/__snapshots__/csr.test.js.snap index 521e976fa2..39e1efb4b7 100644 --- a/test/unit/snap/__snapshots__/csr.test.js.snap +++ b/test/unit/snap/__snapshots__/csr.test.js.snap @@ -180935,7 +180935,7 @@ exports[`csr snapshot test > csr test ./src/tree/_example/checkable.vue 1`] = `