diff --git a/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap b/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap index 6411be1560..5b15e9f923 100644 --- a/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap +++ b/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap @@ -2,6 +2,6 @@ exports[`Address: exist defaultIcon & selectIcon 1`] = `"
请选择地址
"`; -exports[`Address: show custom 1`] = `"
选择地址
请选择
浙江
湖南
福建
"`; +exports[`Address: show custom 1`] = `"
选择地址
请选择
浙江
湖南
福建
"`; exports[`Address: show exist 1`] = `"
选择地址
"`; diff --git a/src/packages/address/address.taro.tsx b/src/packages/address/address.taro.tsx index 62d3bfdaeb..139ee36c64 100644 --- a/src/packages/address/address.taro.tsx +++ b/src/packages/address/address.taro.tsx @@ -27,8 +27,8 @@ type AddressRef = { export interface AddressProps extends CascaderProps { visible: boolean defaultVisible: boolean - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue type: string options: CascaderOption[] optionKey: CascaderOptionKey diff --git a/src/packages/address/address.tsx b/src/packages/address/address.tsx index 901c982475..1fffc889ce 100644 --- a/src/packages/address/address.tsx +++ b/src/packages/address/address.tsx @@ -27,8 +27,8 @@ type AddressRef = { export interface AddressProps extends CascaderProps { visible: boolean defaultVisible: boolean - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue type: string options: CascaderOption[] optionKey: CascaderOptionKey diff --git a/src/packages/address/customRender.taro.tsx b/src/packages/address/customRender.taro.tsx index b72e290d04..fce3d818ec 100644 --- a/src/packages/address/customRender.taro.tsx +++ b/src/packages/address/customRender.taro.tsx @@ -11,8 +11,8 @@ export interface AddressProps extends CascaderProps { visible: boolean // popup visible type: string options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue optionKey: CascaderOptionKey format: Record height: string | number diff --git a/src/packages/address/customRender.tsx b/src/packages/address/customRender.tsx index b5e22b5cb3..b5576d6da6 100644 --- a/src/packages/address/customRender.tsx +++ b/src/packages/address/customRender.tsx @@ -11,8 +11,8 @@ export interface AddressProps extends CascaderProps { visible: boolean // popup visible type: string options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue optionKey: CascaderOptionKey format: Record height: string | number diff --git a/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap b/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap index db50b5b19b..21d7de3247 100644 --- a/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap +++ b/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap @@ -19,7 +19,7 @@ exports[`Cascader > visible true 1`] = ` style="z-index: 1000;" >
visible true 1`] = ` class="nut-tabs-titles nut-tabs-titles-line nut-tabs-titles-scrollable" >
- +
福建 - - +
- +
福州 - - +
- +
鼓楼区 - - +
visible true 1`] = `
- new Promise((r) => { - setTimeout(r, t) - }) const mockOptions = [ { value: '浙江', @@ -92,163 +85,6 @@ const mockConvertOptions = [ { value: '广州市', text: '广州市', nodeId: 21, nodePid: 2 }, ] -describe('helpers', () => { - test('formatTree', () => { - const fromatedTree = formatTree(mockKeyConfigOptions, null, { - children: 'items', - text: 'name', - value: 'name', - }) - - expect(fromatedTree).toMatchObject(mockOptions) - }) - - test('convertListToOptions', () => { - const convertList = convertListToOptions(mockConvertOptions, { - topId: 0, - idKey: 'nodeId', - pidKey: 'nodePid', - sortKey: '', - }) - expect(convertList).toMatchObject([ - { - nodePid: 0, - nodeId: 1, - text: '北京', - value: '北京', - sort: 2, - children: [ - { - nodePid: 1, - nodeId: 11, - text: '朝阳区', - value: '朝阳区', - children: [ - { - nodePid: 11, - nodeId: 111, - text: '亦庄', - value: '亦庄', - children: [], - }, - ], - }, - ], - }, - { - nodePid: 0, - nodeId: 2, - text: '广东省', - value: '广东省', - children: [ - { - nodePid: 2, - nodeId: 21, - text: '广州市', - value: '广州市', - }, - ], - }, - ]) - }) -}) - -describe('Tree', () => { - test('tree', () => { - const tree = new Tree([ - { - text: '浙江', - value: '浙江', - }, - { - text: '福建', - value: '福建', - }, - ]) - expect(tree.nodes).toMatchObject([ - { - text: '浙江', - value: '浙江', - }, - { - text: '福建', - value: '福建', - }, - ]) - }) - - test('tree with config', () => { - const tree = new Tree(mockKeyConfigOptions, { - value: 'name', - text: 'name', - children: 'items', - }) - expect(tree.nodes).toMatchObject(mockOptions) - }) - - const tree = new Tree(mockOptions) - test('getPathNodesByValue', () => { - const pathNodes = tree.getPathNodesByValue(['浙江', '杭州', '西湖区']) - const mappedPathNodes = pathNodes.map(({ text, value }) => ({ - text, - value, - })) - - expect(mappedPathNodes).toMatchObject([ - { text: '浙江', value: '浙江' }, - { text: '杭州', value: '杭州' }, - { text: '西湖区', value: '西湖区' }, - ]) - }) - - test('isLeaf', () => { - const node = tree.getNodeByValue('西湖区') - let isLeaf = tree.isLeaf(node as CascaderOption, false) - expect(isLeaf).toBeTruthy() - isLeaf = tree.isLeaf(node as CascaderOption, true) - expect(isLeaf).toBeFalsy() - }) - - test('hasChildren', () => { - let node = tree.getNodeByValue('西湖区') - - let hasChildren = tree.hasChildren(node as CascaderOption, false) - expect(hasChildren).toBeFalsy() - - hasChildren = tree.hasChildren(node as CascaderOption, true) - expect(hasChildren).toBeFalsy() - - node = tree.getNodeByValue('杭州') - - hasChildren = tree.hasChildren(node as CascaderOption, false) - expect(hasChildren).toBeTruthy() - - hasChildren = tree.hasChildren(node as CascaderOption, true) - expect(hasChildren).toBeTruthy() - }) - - test('updateChildren', () => { - let node = tree.getNodeByValue('福建') - expect(node).toBeTruthy() - - tree.updateChildren( - [{ text: '福州', value: '福州' }], - node as CascaderOption - ) - node = tree.getNodeByValue('福州') as CascaderOption - expect(node).toBeTruthy() - expect(node.value).toBe('福州') - - tree.updateChildren( - [{ text: '鼓楼区', value: '鼓楼区' }], - node as CascaderOption - ) - node = tree.getNodeByValue('鼓楼区') as CascaderOption - expect(node).toBeTruthy() - expect(node.value).toBe('鼓楼区') - }) -}) - describe('Cascader', () => { it('options', async () => { const { container } = render( @@ -330,11 +166,11 @@ describe('Cascader', () => { /> ) const element = container.querySelectorAll( - '.active.nut-tabpane .active .nut-cascader-item-title' + '.nut-tabs-titles-item-active .nut-tabs-titles-item-text' )[0] expect(element).toHaveTextContent('鼓楼区') }) - it('init Value with both valu and defaultValue', async () => { + it('init Value with both value and defaultValue', async () => { const { container } = render( { /> ) const element = container.querySelectorAll( - '.active.nut-tabpane .active .nut-cascader-item-title' + '.nut-tabs-titles-item-active .nut-tabs-titles-item-text' )[0] expect(element).toHaveTextContent('台江区') }) @@ -375,17 +211,19 @@ describe('Cascader', () => { const { container } = render( void) => { - setTimeout(() => { - lazyFunc() - resolve({}) - }, 50) + value={['test']} + onLoad={async (node: any) => { + return new Promise((resolve) => { + setTimeout(() => { + lazyFunc() + resolve([] as CascaderOption[]) + }, 50) + }) }} /> ) expect(lazyFunc).not.toBeCalled() - await act(async () => { - await later(100) + await waitFor(() => { expect(lazyFunc).toBeCalled() }) }) diff --git a/src/packages/cascader/cascader.taro.tsx b/src/packages/cascader/cascader.taro.tsx index fb84efdbeb..dbb902d868 100644 --- a/src/packages/cascader/cascader.taro.tsx +++ b/src/packages/cascader/cascader.taro.tsx @@ -1,509 +1,349 @@ import React, { - ForwardRefRenderFunction, - PropsWithChildren, + forwardRef, isValidElement, - useState, - useEffect, ReactNode, + useEffect, useImperativeHandle, + useMemo, + useRef, + useState, } from 'react' +import { Checklist, Loading } from '@nutui/icons-react-taro' import classNames from 'classnames' -import { Loading, Checklist } from '@nutui/icons-react-taro' -import { ScrollView } from '@tarojs/components' -import { Popup, PopupProps } from '@/packages/popup/popup.taro' -import { Tabs } from '@/packages/tabs/tabs.taro' -import { convertListToOptions } from './helper' +import Tabs from '@/packages/tabs/index.taro' +import Popup, { PopupProps } from '@/packages/popup/index.taro' +import { + normalizeListOptions, + normalizeOptions, +} from '@/packages/cascader/utils' import { - CascaderPane, CascaderOption, + CascaderActions, CascaderValue, CascaderOptionKey, - CascaderFormat, } from './types' -import Tree from './tree' import { ComponentDefaults } from '@/utils/typings' +import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' -import { useConfig } from '@/packages/configprovider/configprovider.taro' - -export interface CascaderProps - extends Pick< +import { isEmpty } from '@/utils/is-empty' +import { getRefValue, useRefState } from '@/utils/use-ref-state' +import { useConfig } from '@/packages/configprovider' + +export type CascaderPopupProps = Pick< + PopupProps, + | 'className' + | 'style' + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' +> +export type CascaderSupportPopupProps = Partial< + Omit< PopupProps, - | 'className' - | 'style' | 'closeIcon' | 'closeable' | 'title' | 'left' | 'closeIconPosition' | 'onClose' - > { - popup: boolean - popupProps: Partial< - Omit< - PopupProps, - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > > - visible: boolean // popup visible +> + +export interface CascaderProps extends CascaderPopupProps { + visible: boolean + value: CascaderValue activeColor: string - activeIcon: string + activeIcon: ReactNode + defaultValue: CascaderValue options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue optionKey: CascaderOptionKey format: Record closeable: boolean - closeIconPosition: string closeIcon: ReactNode + closeIconPosition: string + popup: boolean + popupProps: CascaderSupportPopupProps lazy: boolean - onLoad: (node: any, resolve: any) => void - onChange: (value: CascaderValue, params?: any) => void - onPathChange: (value: CascaderValue, params: any) => void -} - -export type CascaderActions = { - open: () => void - close: () => void + onLoad: ( + node: CascaderOption, + levelIndex: number + ) => Promise + onChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onPathChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onTabsChange: (index: number) => void + onClose: () => void } -const defaultProps = { +const defaultProps: CascaderProps = { ...ComponentDefaults, activeColor: '', activeIcon: 'checklist', popup: true, options: [], - optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, + optionKey: {}, format: {}, closeable: false, closeIconPosition: 'top-right', closeIcon: 'close', lazy: false, - onLoad: () => {}, onClose: () => {}, onChange: () => {}, onPathChange: () => {}, } as unknown as CascaderProps -const InternalCascader: ForwardRefRenderFunction< - unknown, - PropsWithChildren> -> = (props, ref) => { - const { locale } = useConfig() + +export const Cascader = forwardRef((props: Partial, ref) => { + const classPrefix = 'nut-cascader' + const classPane = `${classPrefix}-pane` const { - className, - style, activeColor, activeIcon, popup, popupProps = {}, - visible, - options, - value, - defaultValue, + visible: outerVisible, + options: outerOptions, + value: outerValue, + defaultValue: outerDefaultValue, optionKey, format, closeable, closeIconPosition, closeIcon, lazy, - title, - left, onLoad, - onClose, - onChange, - onPathChange, - } = { ...defaultProps, ...props } - - const [tabvalue, setTabvalue] = useState('c1') - const [optionsData, setOptionsData] = useState([]) - const isLazy = () => state.configs.lazy && Boolean(state.configs.onLoad) + } = mergeProps(defaultProps, props) + const { locale } = useConfig() - const [innerValue, setInnerValue] = usePropsValue({ - value, - defaultValue, - finalValue: defaultValue, + const [tabActiveIndex, setTabActiveIndex] = useState(0) + const [optionsRef, setInnerOptions] = useRefState(outerOptions) + const innerOptions = getRefValue(optionsRef) + const [loading, setLoading] = useState<{ [key: string]: any }>({}) + + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: [], + onChange: (value) => { + props.onChange?.(value, pathNodes.current) + // props.onPathChange?.(value, pathNodes.current) + }, }) - const [innerVisible, setInnerVisible] = usePropsValue({ - value: visible, + + const [innerValue, setInnerValue] = useState(value) + + const options = useMemo(() => { + if (!isEmpty(format)) { + return normalizeListOptions(innerOptions, format) + } + if (!isEmpty(optionKey)) { + return normalizeOptions(innerOptions, optionKey) + } + return innerOptions + }, [innerOptions, optionKey, format, innerValue]) + + const pathNodes = useRef([]) + + const levels: any[] = useMemo(() => { + const next = [] + let end = false + let currentOptions = options + for (const [index, val] of innerValue.entries()) { + const opt = currentOptions?.find((o: CascaderOption) => o.value === val) + next.push({ + selected: val, + pane: currentOptions, + }) + pathNodes.current[index] = opt + if (opt?.children) { + currentOptions = opt.children + } else { + end = true + break + } + } + if (!end) { + next.push({ + selected: null, + pane: currentOptions, + }) + } + return next + }, [innerValue, options, innerOptions]) + + const [visible, setVisible] = usePropsValue({ + value: outerVisible, defaultValue: undefined, - finalValue: false, + onChange: (value) => { + if (value === false) { + props.onClose?.() + } + }, }) const actions: CascaderActions = { open: () => { - setInnerVisible(true) + setVisible(true) }, close: () => { - setInnerVisible(false) + setVisible(false) }, } useImperativeHandle(ref, () => actions) - const [state] = useState({ - optionsData: [] as any, - panes: [ - { - nodes: [] as any, - selectedNode: [] as CascaderOption | null, - paneKey: '', - }, - ], - tree: new Tree([], {}), - tabsCursor: 0, // 选中的tab项 - initLoading: false, - currentProcessNode: [] as CascaderOption | null, - configs: { - lazy, - onLoad, - optionKey, - format, - }, - lazyLoadMap: new Map(), - }) - - const classPrefix = classNames(`nut-cascader`) - const classesPane = classNames({ - [`${classPrefix}-pane`]: true, - }) - - useEffect(() => { - initData() - }, [options, format]) - useEffect(() => { - syncValue() - }, [value]) - - const initData = async () => { - // 初始化开始处理数据 - state.lazyLoadMap.clear() - if (format && Object.keys(format).length > 0) { - state.optionsData = convertListToOptions( - options as CascaderOption[], - format as CascaderFormat - ) - } else { - state.optionsData = options + if (!visible) { + setInnerValue(value) } - state.tree = new Tree(state.optionsData as CascaderOption[], { - value: state.configs.optionKey.valueKey, - text: state.configs.optionKey.textKey, - children: state.configs.optionKey.childrenKey, - }) - if (isLazy() && !state.tree.nodes.length) { - await invokeLazyLoad({ - root: true, - loading: true, - text: '', - value: '', - }) - } - state.panes = [ - { - nodes: state.tree.nodes, - selectedNode: null, - paneKey: 'c1', - }, - ] - syncValue() - setOptionsData(state.panes) - } - // 处理有默认值时的数据 - const syncValue = async () => { - const currentValue = innerValue + }, [visible, value]) - if ( - currentValue === undefined || - ![defaultValue, value].includes(currentValue) || - !state.tree.nodes.length - ) { - return - } + useEffect(() => { + setInnerOptions(outerOptions) + }, [outerOptions]) - if (currentValue.length === 0) { - state.tabsCursor = 0 - return + useEffect(() => { + setTabActiveIndex(levels.length - 1) + }, [innerValue, innerOptions, outerOptions]) + useEffect(() => { + const max = levels.length - 1 + if (tabActiveIndex > max) { + setTabActiveIndex(max) } - - let needToSync = currentValue - - if (isLazy() && Array.isArray(currentValue) && currentValue.length) { - needToSync = [] - const parent: any = state.tree.nodes.find( - (node) => node.value === currentValue[0] - ) - - if (parent) { - needToSync = [parent.value] - state.initLoading = true - - const last = await currentValue - .slice(1) - .reduce(async (p: Promise, value) => { - const parent = await p - await invokeLazyLoad(parent) - const node: any = parent?.children?.find( - (item: any) => item.value === value - ) - if (node) { - needToSync.push(value) - } + }, [tabActiveIndex, levels, innerOptions, outerOptions]) + useEffect(() => { + const load = async () => { + const parent = { children: [] } + try { + await innerValue.reduce(async (promise: Promise, val, key) => { + const pane = await onLoad({ value: val }, key) + const parent = await promise + parent.children = pane + if (key === innerValue.length - 1) { + return Promise.resolve(parent) + } + if (pane) { + const node = pane.find((p) => p.value === val) return Promise.resolve(node) - }, Promise.resolve(parent)) - await invokeLazyLoad(last) - state.initLoading = false - } - } - - if (needToSync.length && [defaultValue, value].includes(currentValue)) { - const pathNodes = state.tree.getPathNodesByValue(needToSync) - pathNodes.forEach((node, index) => { - state.tabsCursor = index - // 当有默认值时,不触发 chooseItem 里的 emit 事件 - chooseItem(node, true) - }) - } - } - - const invokeLazyLoad = async (node?: CascaderOption | void) => { - if (!node) { - return - } - - if (!state.configs.onLoad) { - node.leaf = true - return - } - - if ( - state.tree.isLeaf(node, isLazy()) || - state.tree.hasChildren(node, isLazy()) - ) { - return - } - - node.loading = true - - const parent = node.root ? null : node - let lazyLoadPromise = state.lazyLoadMap.get(node) - - if (!lazyLoadPromise) { - lazyLoadPromise = new Promise((resolve) => { - // 外部必须resolve - state.configs.onLoad?.(node, resolve) - }) - state.lazyLoadMap.set(node, lazyLoadPromise) - } - - const nodes: CascaderOption[] | void = await lazyLoadPromise - - if (Array.isArray(nodes) && nodes.length > 0) { - state.tree.updateChildren(nodes, parent) - } else { - // 如果加载完成后没有提供子节点,作为叶子节点处理 - node.leaf = true - } - node.loading = false - state.lazyLoadMap.delete(node) - } - - const close = () => { - setInnerVisible(false) - onClose && onClose() - } - - const closePopup = () => { - close() - } + } + }, Promise.resolve(parent)) - /* type: 是否是静默模式,是的话不触发事件 - tabsCursor: tab的索引 */ - const chooseItem = async (node: CascaderOption, type: boolean) => { - if ((!type && node.disabled) || !state.panes[state.tabsCursor]) { - return - } - // 如果没有子节点 - if (state.tree.isLeaf(node, isLazy())) { - node.leaf = true - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, (node.level as number) + 1) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item.value) - onChange(optionParams, pathNodes) - onPathChange?.(optionParams, pathNodes) - setInnerValue(optionParams) + // 如果需要处理最终结果,可以在这里使用 last + setInnerOptions(parent.children) + } catch (error) { + console.error('Error loading data:', error) } - setOptionsData(state.panes) - close() - return } - // 如果有子节点,滑到下一个 - if (state.tree.hasChildren(node, isLazy())) { - const level = (node.level as number) + 1 - - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, level) - state.tabsCursor = level - state.panes.push({ - nodes: node.children || [], - selectedNode: null, - paneKey: `c${state.tabsCursor + 1}`, - }) - setOptionsData(state.panes) - setTabvalue(`c${state.tabsCursor + 1}`) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item?.value) - onPathChange?.(optionParams, pathNodes) - } - return + if (lazy) load() + }, [lazy]) + + const chooseItem = async (pane: CascaderOption, levelIndex: number) => { + if (pane.disabled) return + const nextValue = innerValue.slice(0, levelIndex) + const nextPathNodes = pathNodes.current.slice(0, levelIndex) + if (pane.value) { + setLoading(!!onLoad && { [levelIndex]: pane.value }) + nextValue[levelIndex] = pane.value + nextPathNodes[levelIndex] = pane + pathNodes.current = nextPathNodes + props?.onPathChange?.(nextValue, pathNodes.current) } - state.currentProcessNode = node - if (node.loading) { - return + if (onLoad) { + // 叶子节点不操作 + if (!pane.leaf) { + const asyncOptions = await onLoad(pane, levelIndex) + // 修改 options 触发渲染逻辑 + if (asyncOptions) pane.children = asyncOptions + } else { + setVisible(false) + setValue(nextValue) + } } - - await invokeLazyLoad(node) - if (state.currentProcessNode === node) { - state.panes[state.tabsCursor].selectedNode = node - chooseItem(node, type) + if (!pane.children && !onLoad) { + setVisible(false) + setValue(nextValue) } - setOptionsData(state.panes) + setInnerValue(nextValue) + setLoading({}) } - const renderItem = (pane: any, node: any, index: number) => { - const classPrefix2 = 'nut-cascader-item' - const checked = pane.selectedNode?.value === node.value - - const classes = classNames( - { - active: checked, - disabled: node.disabled, - }, - classPrefix2 - ) - - const classesTitle = classNames({ - [`${classPrefix2}-title`]: true, + const renderCascaderItem = (item: any, levelIndex: number) => { + return item.pane?.map((pane: CascaderOption, index: number) => { + const active = item.selected === pane.value + const classes = classNames( + { + active, + disabled: pane.disabled, + }, + 'nut-cascader-item' + ) + const showLoadingIcon = loading[levelIndex] === pane.value + return ( +
{ + chooseItem(pane, levelIndex) + }} + > +
{pane.text}
+ {showLoadingIcon && ( + + )} + {active && + (isValidElement(activeIcon) ? ( + activeIcon + ) : ( + + ))} +
+ ) }) - - const renderIcon = () => { - if (checked) { - if (isValidElement(activeIcon)) { - return activeIcon - } - return ( - - ) - } - return null - } - - return ( -
{ - chooseItem(node, false) - }} - > -
{node.text}
- {node.loading ? ( - - ) : ( - renderIcon() - )} -
- ) } - const renderTabs = () => { + const renderTab = () => { return ( -
+
{ - return optionsData.map((pane, index) => ( -
{ - setTabvalue(pane.paneKey) - state.tabsCursor = index - }} - className={`nut-tabs-titles-item ${ - tabvalue === pane.paneKey ? 'nut-tabs-titles-item-active' : '' - }`} - key={pane.paneKey} - > - - {!state.initLoading && - state.panes.length && - pane?.selectedNode?.text} - {!state.initLoading && - state.panes.length && - !pane?.selectedNode?.text && - `${locale.select}`} - {!(!state.initLoading && state.panes.length) && 'Loading...'} - - -
- )) + value={tabActiveIndex} + onChange={(index) => { + props.onTabsChange?.(Number(index)) + setTabActiveIndex(Number(index)) }} > - {!state.initLoading && state.panes.length ? ( - optionsData.map((pane) => ( - - - {pane.nodes?.map((node: any, index: number) => - renderItem(pane, node, index) - )} - - - )) - ) : ( - -
+ {levels.map((pane, index) => ( + +
{renderCascaderItem(pane, index)}
- )} + ))}
) } - return ( - <> - {popup ? ( - - {renderTabs()} - - ) : ( - renderTabs() - )} - + return popup ? ( + setVisible(false)} + onCloseIconClick={() => setVisible(false)} + > + {renderTab()} + + ) : ( + renderTab() ) -} - -export const Cascader = React.forwardRef(InternalCascader) +}) Cascader.displayName = 'NutCascader' diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index f6c4c2f647..6defd290a4 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -1,507 +1,349 @@ import React, { - ForwardRefRenderFunction, - PropsWithChildren, + forwardRef, isValidElement, - useState, - useEffect, ReactNode, + useEffect, useImperativeHandle, + useMemo, + useRef, + useState, } from 'react' +import { Checklist, Loading } from '@nutui/icons-react' import classNames from 'classnames' -import { Loading, Checklist } from '@nutui/icons-react' -import { Popup, PopupProps } from '@/packages/popup/popup' -import { Tabs } from '@/packages/tabs/tabs' -import { convertListToOptions } from './helper' +import Tabs from '@/packages/tabs' +import Popup, { PopupProps } from '@/packages/popup' +import { + normalizeListOptions, + normalizeOptions, +} from '@/packages/cascader/utils' import { - CascaderPane, CascaderOption, + CascaderActions, CascaderValue, CascaderOptionKey, - CascaderFormat, } from './types' -import Tree from './tree' import { ComponentDefaults } from '@/utils/typings' +import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' +import { isEmpty } from '@/utils/is-empty' +import { getRefValue, useRefState } from '@/utils/use-ref-state' import { useConfig } from '@/packages/configprovider' -export interface CascaderProps - extends Pick< +export type CascaderPopupProps = Pick< + PopupProps, + | 'className' + | 'style' + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' +> +export type CascaderSupportPopupProps = Partial< + Omit< PopupProps, - | 'className' - | 'style' | 'closeIcon' | 'closeable' | 'title' | 'left' | 'closeIconPosition' | 'onClose' - > { - popup: boolean - popupProps: Partial< - Omit< - PopupProps, - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > > - visible: boolean // popup visible +> + +export interface CascaderProps extends CascaderPopupProps { + visible: boolean + value: CascaderValue activeColor: string - activeIcon: string + activeIcon: ReactNode + defaultValue: CascaderValue options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue optionKey: CascaderOptionKey format: Record closeable: boolean - closeIconPosition: string closeIcon: ReactNode + closeIconPosition: string + popup: boolean + popupProps: CascaderSupportPopupProps lazy: boolean - onLoad: (node: any, resolve: any) => void - onChange: (value: CascaderValue, params?: any) => void - onPathChange: (value: CascaderValue, params: any) => void -} - -export type CascaderActions = { - open: () => void - close: () => void + onLoad: ( + node: CascaderOption, + levelIndex: number + ) => Promise + onChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onPathChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onTabsChange: (index: number) => void + onClose: () => void } -const defaultProps = { +const defaultProps: CascaderProps = { ...ComponentDefaults, activeColor: '', activeIcon: 'checklist', popup: true, options: [], - optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, + optionKey: {}, format: {}, closeable: false, closeIconPosition: 'top-right', closeIcon: 'close', lazy: false, - onLoad: () => {}, onClose: () => {}, onChange: () => {}, onPathChange: () => {}, } as unknown as CascaderProps -const InternalCascader: ForwardRefRenderFunction< - unknown, - PropsWithChildren> -> = (props, ref) => { - const { locale } = useConfig() + +export const Cascader = forwardRef((props: Partial, ref) => { + const classPrefix = 'nut-cascader' + const classPane = `${classPrefix}-pane` const { - className, - style, activeColor, activeIcon, popup, popupProps = {}, - visible, - options, - value, - defaultValue, + visible: outerVisible, + options: outerOptions, + value: outerValue, + defaultValue: outerDefaultValue, optionKey, format, closeable, closeIconPosition, closeIcon, lazy, - title, - left, onLoad, - onClose, - onChange, - onPathChange, - } = { ...defaultProps, ...props } - - const [tabvalue, setTabvalue] = useState('c1') - const [optionsData, setOptionsData] = useState([]) - const isLazy = () => state.configs.lazy && Boolean(state.configs.onLoad) + } = mergeProps(defaultProps, props) + const { locale } = useConfig() - const [innerValue, setInnerValue] = usePropsValue({ - value, - defaultValue, - finalValue: defaultValue, + const [tabActiveIndex, setTabActiveIndex] = useState(0) + const [optionsRef, setInnerOptions] = useRefState(outerOptions) + const innerOptions = getRefValue(optionsRef) + const [loading, setLoading] = useState<{ [key: string]: any }>({}) + + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: [], + onChange: (value) => { + props.onChange?.(value, pathNodes.current) + // props.onPathChange?.(value, pathNodes.current) + }, }) - const [innerVisible, setInnerVisible] = usePropsValue({ - value: visible, + + const [innerValue, setInnerValue] = useState(value) + + const options = useMemo(() => { + if (!isEmpty(format)) { + return normalizeListOptions(innerOptions, format) + } + if (!isEmpty(optionKey)) { + return normalizeOptions(innerOptions, optionKey) + } + return innerOptions + }, [innerOptions, optionKey, format, innerValue]) + + const pathNodes = useRef([]) + + const levels: any[] = useMemo(() => { + const next = [] + let end = false + let currentOptions = options + for (const [index, val] of innerValue.entries()) { + const opt = currentOptions?.find((o: CascaderOption) => o.value === val) + next.push({ + selected: val, + pane: currentOptions, + }) + pathNodes.current[index] = opt + if (opt?.children) { + currentOptions = opt.children + } else { + end = true + break + } + } + if (!end) { + next.push({ + selected: null, + pane: currentOptions, + }) + } + return next + }, [innerValue, options, innerOptions]) + + const [visible, setVisible] = usePropsValue({ + value: outerVisible, defaultValue: undefined, - finalValue: false, + onChange: (value) => { + if (value === false) { + props.onClose?.() + } + }, }) const actions: CascaderActions = { open: () => { - setInnerVisible(true) + setVisible(true) }, close: () => { - setInnerVisible(false) + setVisible(false) }, } useImperativeHandle(ref, () => actions) - const [state] = useState({ - optionsData: [] as any, - panes: [ - { - nodes: [] as any, - selectedNode: [] as CascaderOption | null, - paneKey: '', - }, - ], - tree: new Tree([], {}), - tabsCursor: 0, // 选中的tab项 - initLoading: false, - currentProcessNode: [] as CascaderOption | null, - configs: { - lazy, - onLoad, - optionKey, - format, - }, - lazyLoadMap: new Map(), - }) - - const classPrefix = classNames(`nut-cascader`) - const classesPane = classNames({ - [`${classPrefix}-pane`]: true, - }) - - useEffect(() => { - initData() - }, [options, format]) - useEffect(() => { - syncValue() - }, [value]) - - const initData = async () => { - // 初始化开始处理数据 - state.lazyLoadMap.clear() - if (format && Object.keys(format).length > 0) { - state.optionsData = convertListToOptions( - options as CascaderOption[], - format as CascaderFormat - ) - } else { - state.optionsData = options + if (!visible) { + setInnerValue(value) } - state.tree = new Tree(state.optionsData as CascaderOption[], { - value: state.configs.optionKey.valueKey, - text: state.configs.optionKey.textKey, - children: state.configs.optionKey.childrenKey, - }) - if (isLazy() && !state.tree.nodes.length) { - await invokeLazyLoad({ - root: true, - loading: true, - text: '', - value: '', - }) - } - state.panes = [ - { - nodes: state.tree.nodes, - selectedNode: null, - paneKey: 'c1', - }, - ] - syncValue() - setOptionsData(state.panes) - } - // 处理有默认值时的数据 - const syncValue = async () => { - const currentValue = innerValue + }, [visible, value]) - if ( - currentValue === undefined || - ![defaultValue, value].includes(currentValue) || - !state.tree.nodes.length - ) { - return - } + useEffect(() => { + setInnerOptions(outerOptions) + }, [outerOptions]) - if (currentValue.length === 0) { - state.tabsCursor = 0 - return + useEffect(() => { + setTabActiveIndex(levels.length - 1) + }, [innerValue, innerOptions, outerOptions]) + useEffect(() => { + const max = levels.length - 1 + if (tabActiveIndex > max) { + setTabActiveIndex(max) } - - let needToSync = currentValue - - if (isLazy() && Array.isArray(currentValue) && currentValue.length) { - needToSync = [] - const parent: any = state.tree.nodes.find( - (node) => node.value === currentValue[0] - ) - - if (parent) { - needToSync = [parent.value] - state.initLoading = true - - const last = await currentValue - .slice(1) - .reduce(async (p: Promise, value) => { - const parent = await p - await invokeLazyLoad(parent) - const node: any = parent?.children?.find( - (item: any) => item.value === value - ) - if (node) { - needToSync.push(value) - } + }, [tabActiveIndex, levels, innerOptions, outerOptions]) + useEffect(() => { + const load = async () => { + const parent = { children: [] } + try { + await innerValue.reduce(async (promise: Promise, val, key) => { + const pane = await onLoad({ value: val }, key) + const parent = await promise + parent.children = pane + if (key === innerValue.length - 1) { + return Promise.resolve(parent) + } + if (pane) { + const node = pane.find((p) => p.value === val) return Promise.resolve(node) - }, Promise.resolve(parent)) - await invokeLazyLoad(last) - state.initLoading = false - } - } - - if (needToSync.length && [defaultValue, value].includes(currentValue)) { - const pathNodes = state.tree.getPathNodesByValue(needToSync) - pathNodes.forEach((node, index) => { - state.tabsCursor = index - // 当有默认值时,不触发 chooseItem 里的 emit 事件 - chooseItem(node, true) - }) - } - } - - const invokeLazyLoad = async (node?: CascaderOption | void) => { - if (!node) { - return - } - - if (!state.configs.onLoad) { - node.leaf = true - return - } - - if ( - state.tree.isLeaf(node, isLazy()) || - state.tree.hasChildren(node, isLazy()) - ) { - return - } - - node.loading = true - - const parent = node.root ? null : node - let lazyLoadPromise = state.lazyLoadMap.get(node) - - if (!lazyLoadPromise) { - lazyLoadPromise = new Promise((resolve) => { - // 外部必须resolve - state.configs.onLoad?.(node, resolve) - }) - state.lazyLoadMap.set(node, lazyLoadPromise) - } - - const nodes: CascaderOption[] | void = await lazyLoadPromise - - if (Array.isArray(nodes) && nodes.length > 0) { - state.tree.updateChildren(nodes, parent) - } else { - // 如果加载完成后没有提供子节点,作为叶子节点处理 - node.leaf = true - } - node.loading = false - state.lazyLoadMap.delete(node) - } - - const close = () => { - setInnerVisible(false) - onClose && onClose() - } - - const closePopup = () => { - close() - } + } + }, Promise.resolve(parent)) - /* type: 是否是静默模式,是的话不触发事件 - tabsCursor: tab的索引 */ - const chooseItem = async (node: CascaderOption, type: boolean) => { - if ((!type && node.disabled) || !state.panes[state.tabsCursor]) { - return - } - // 如果没有子节点 - if (state.tree.isLeaf(node, isLazy())) { - node.leaf = true - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, (node.level as number) + 1) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item.value) - onChange(optionParams, pathNodes) - onPathChange?.(optionParams, pathNodes) - setInnerValue(optionParams) + // 如果需要处理最终结果,可以在这里使用 last + setInnerOptions(parent.children) + } catch (error) { + console.error('Error loading data:', error) } - setOptionsData(state.panes) - close() - return } - // 如果有子节点,滑到下一个 - if (state.tree.hasChildren(node, isLazy())) { - const level = (node.level as number) + 1 - - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, level) - state.tabsCursor = level - state.panes.push({ - nodes: node.children || [], - selectedNode: null, - paneKey: `c${state.tabsCursor + 1}`, - }) - setOptionsData(state.panes) - setTabvalue(`c${state.tabsCursor + 1}`) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item?.value) - onPathChange?.(optionParams, pathNodes) - } - return + if (lazy) load() + }, [lazy]) + + const chooseItem = async (pane: CascaderOption, levelIndex: number) => { + if (pane.disabled) return + const nextValue = innerValue.slice(0, levelIndex) + const nextPathNodes = pathNodes.current.slice(0, levelIndex) + if (pane.value) { + setLoading(!!onLoad && { [levelIndex]: pane.value }) + nextValue[levelIndex] = pane.value + nextPathNodes[levelIndex] = pane + pathNodes.current = nextPathNodes + props?.onPathChange?.(nextValue, pathNodes.current) } - state.currentProcessNode = node - if (node.loading) { - return + if (onLoad) { + // 叶子节点不操作 + if (!pane.leaf) { + const asyncOptions = await onLoad(pane, levelIndex) + // 修改 options 触发渲染逻辑 + if (asyncOptions) pane.children = asyncOptions + } else { + setVisible(false) + setValue(nextValue) + } } - - await invokeLazyLoad(node) - if (state.currentProcessNode === node) { - state.panes[state.tabsCursor].selectedNode = node - chooseItem(node, type) + if (!pane.children && !onLoad) { + setVisible(false) + setValue(nextValue) } - setOptionsData(state.panes) + setInnerValue(nextValue) + setLoading({}) } - const renderItem = (pane: any, node: any, index: number) => { - const classPrefix2 = 'nut-cascader-item' - const checked = pane.selectedNode?.value === node.value - - const classes = classNames( - { - active: checked, - disabled: node.disabled, - }, - classPrefix2 - ) - - const classesTitle = classNames({ - [`${classPrefix2}-title`]: true, + const renderCascaderItem = (item: any, levelIndex: number) => { + return item.pane?.map((pane: CascaderOption, index: number) => { + const active = item.selected === pane.value + const classes = classNames( + { + active, + disabled: pane.disabled, + }, + 'nut-cascader-item' + ) + const showLoadingIcon = loading[levelIndex] === pane.value + return ( +
{ + chooseItem(pane, levelIndex) + }} + > +
{pane.text}
+ {showLoadingIcon && ( + + )} + {active && + (isValidElement(activeIcon) ? ( + activeIcon + ) : ( + + ))} +
+ ) }) - - const renderIcon = () => { - if (checked) { - if (isValidElement(activeIcon)) { - return activeIcon - } - return ( - - ) - } - return null - } - - return ( -
{ - chooseItem(node, false) - }} - > -
{node.text}
- {node.loading ? ( - - ) : ( - renderIcon() - )} -
- ) } - const renderTabs = () => { + const renderTab = () => { return ( -
+
{ - return optionsData.map((pane, index) => ( -
{ - setTabvalue(pane.paneKey) - state.tabsCursor = index - }} - className={`nut-tabs-titles-item ${ - tabvalue === pane.paneKey ? 'nut-tabs-titles-item-active' : '' - }`} - key={pane.paneKey} - > - - {!state.initLoading && - state.panes.length && - pane?.selectedNode?.text} - {!state.initLoading && - state.panes.length && - !pane?.selectedNode?.text && - `${locale.select}`} - {!(!state.initLoading && state.panes.length) && 'Loading...'} - - -
- )) + value={tabActiveIndex} + onChange={(index) => { + props.onTabsChange?.(Number(index)) + setTabActiveIndex(Number(index)) }} > - {!state.initLoading && state.panes.length ? ( - optionsData.map((pane) => ( - -
- {pane.nodes?.map((node: any, index: number) => - renderItem(pane, node, index) - )} -
-
- )) - ) : ( - -
+ {levels.map((pane, index) => ( + +
{renderCascaderItem(pane, index)}
- )} + ))}
) } - return ( - <> - {popup ? ( - - {renderTabs()} - - ) : ( - renderTabs() - )} - + return popup ? ( + setVisible(false)} + onCloseIconClick={() => setVisible(false)} + > + {renderTab()} + + ) : ( + renderTab() ) -} - -export const Cascader = React.forwardRef(InternalCascader) +}) Cascader.displayName = 'NutCascader' diff --git a/src/packages/cascader/demo.taro.tsx b/src/packages/cascader/demo.taro.tsx index 4227dbb103..a1e773d0e4 100644 --- a/src/packages/cascader/demo.taro.tsx +++ b/src/packages/cascader/demo.taro.tsx @@ -8,11 +8,13 @@ import Demo3 from './demos/taro/demo3' import Demo4 from './demos/taro/demo4' import Demo5 from './demos/taro/demo5' import Demo6 from './demos/taro/demo6' +import Demo7 from './demos/taro/demo7' const CascaderDemo = () => { const [translated] = useTranslate({ 'zh-CN': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定义属性名称', title2: '动态加载', title3: '部分数据动态加载', @@ -21,6 +23,7 @@ const CascaderDemo = () => { }, 'zh-TW': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定義屬性名稱', title2: '動態加載', title3: '部分數據動態加載', @@ -29,6 +32,7 @@ const CascaderDemo = () => { }, 'en-US': { basic: 'Basic Usage', + uncontrolled: 'uncontrolled', title1: 'Custom Attribute Name', title2: 'Async Loading', title3: 'Async Loading Of Partial Data', @@ -43,6 +47,8 @@ const CascaderDemo = () => {

{translated.basic}

+

{translated.uncontrolled}

+

{translated.title1}

{translated.title2}

diff --git a/src/packages/cascader/demo.tsx b/src/packages/cascader/demo.tsx index 484170b715..268f027718 100644 --- a/src/packages/cascader/demo.tsx +++ b/src/packages/cascader/demo.tsx @@ -6,11 +6,13 @@ import Demo3 from './demos/h5/demo3' import Demo4 from './demos/h5/demo4' import Demo5 from './demos/h5/demo5' import Demo6 from './demos/h5/demo6' +import Demo7 from './demos/h5/demo7' const CascaderDemo = () => { const [translated] = useTranslate({ 'zh-CN': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定义属性名称', title2: '动态加载', title3: '部分数据动态加载', @@ -19,6 +21,7 @@ const CascaderDemo = () => { }, 'zh-TW': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定義屬性名稱', title2: '動態加載', title3: '部分數據動態加載', @@ -27,6 +30,7 @@ const CascaderDemo = () => { }, 'en-US': { basic: 'Basic Usage', + uncontrolled: 'uncontrolled', title1: 'Custom Attribute Name', title2: 'Async Loading', title3: 'Async Loading Of Partial Data', @@ -40,6 +44,8 @@ const CascaderDemo = () => {

{translated.basic}

+

{translated.uncontrolled}

+

{translated.title1}

{translated.title2}

diff --git a/src/packages/cascader/demos/h5/demo1.tsx b/src/packages/cascader/demos/h5/demo1.tsx index 6ad63821de..ac49452f62 100644 --- a/src/packages/cascader/demos/h5/demo1.tsx +++ b/src/packages/cascader/demos/h5/demo1.tsx @@ -1,98 +1,106 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' const Demo1 = () => { - const [isVisibleDemo1, setIsVisibleDemo1] = useState(false) - const [value1, setValue1] = useState([]) - const [optionsDemo1] = useState([ - { - value: '浙江', - text: '浙江', - children: [ - { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + console.log('onchange', value, path) + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setOptions([ { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '芙蓉区', text: '芙蓉区' }, - { value: '岳麓区', text: '岳麓区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, ], }, { - value: '岳阳', - text: '岳阳', + value: '福建', + text: '福建', children: [ - { value: '岳阳楼区', text: '岳阳楼区' }, - { value: '云溪区', text: '云溪区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ - { - value: '福州', - text: '福州', - children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, - ], - }, - ], - }, - ]) - const change1 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue1(value) - } + ]) + }, 300) + }, []) return ( <> { - setIsVisibleDemo1(true) + setVisible(true) }} /> { - setIsVisibleDemo1(false) + setVisible(false) + }} + onChange={onChange} + onPathChange={(value, path) => { + console.log(value, path) }} - onChange={change1} /> ) diff --git a/src/packages/cascader/demos/h5/demo2.tsx b/src/packages/cascader/demos/h5/demo2.tsx index 0b8059a5d8..9fd82558ab 100644 --- a/src/packages/cascader/demos/h5/demo2.tsx +++ b/src/packages/cascader/demos/h5/demo2.tsx @@ -1,91 +1,95 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react' const Demo2 = () => { - const [isVisibleDemo2, setIsVisibleDemo2] = useState(false) - const [value2, setValue2] = useState(['福建', '福州', '台江区']) - const [optionsDemo2] = useState([ - { - value1: '浙江', - text1: '浙江', - items: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['福建', '福州', '台江区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + setOptions([ { - value1: '杭州', - text1: '杭州', - disabled: true, - items: [ - { value1: '西湖区', text1: '西湖区', disabled: true }, - { value1: '余杭区', text1: '余杭区' }, - ], - }, - { - value1: '温州', - text1: '温州', + value1: '浙江', + text1: '浙江', items: [ - { value1: '鹿城区', text1: '鹿城区' }, - { value1: '瓯海区', text1: '瓯海区' }, + { + value1: '杭州', + text1: '杭州', + disabled: true, + items: [ + { value1: '西湖区', text1: '西湖区', disabled: true }, + { value1: '余杭区', text1: '余杭区' }, + ], + }, + { + value1: '温州', + text1: '温州', + items: [ + { value1: '鹿城区', text1: '鹿城区' }, + { value1: '瓯海区', text1: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value1: '湖南', - text1: '湖南', - disabled: true, - items: [ { - value1: '长沙', - text1: '长沙', + value1: '湖南', + text1: '湖南', disabled: true, items: [ - { value1: '芙蓉区', text1: '芙蓉区' }, - { value1: '岳麓区', text1: '岳麓区' }, - ], - }, - { - value1: '岳阳', - text1: '岳阳', - children: [ - { value1: '岳阳楼区', text1: '岳阳楼区' }, - { value1: '云溪区', text1: '云溪区' }, + { + value1: '长沙', + text1: '长沙', + disabled: true, + items: [ + { value1: '芙蓉区', text1: '芙蓉区' }, + { value1: '岳麓区', text1: '岳麓区' }, + ], + }, + { + value1: '岳阳', + text1: '岳阳', + children: [ + { value1: '岳阳楼区', text1: '岳阳楼区' }, + { value1: '云溪区', text1: '云溪区' }, + ], + }, ], }, - ], - }, - { - value1: '福建', - text1: '福建', - items: [ { - value1: '福州', - text1: '福州', + value1: '福建', + text1: '福建', items: [ - { value1: '鼓楼区', text1: '鼓楼区' }, - { value1: '台江区', text1: '台江区' }, + { + value1: '福州', + text1: '福州', + items: [ + { value1: '鼓楼区', text1: '鼓楼区' }, + { value1: '台江区', text1: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change2 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue2(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo2(true) + setVisible(true) }} /> { }} closeable onClose={() => { - setIsVisibleDemo2(false) + setVisible(false) }} - onChange={change2} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/h5/demo3.tsx b/src/packages/cascader/demos/h5/demo3.tsx index 48387206e4..72e052eafb 100644 --- a/src/packages/cascader/demos/h5/demo3.tsx +++ b/src/packages/cascader/demos/h5/demo3.tsx @@ -1,57 +1,51 @@ import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react' const Demo3 = () => { - const [isVisibleDemo3, setIsVisibleDemo3] = useState(false) - const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32']) + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['A11', 'A21', 'A31', 'A41']) - const lazyLoadDemo3 = (node: any, resolve: (children: any) => void) => { - setTimeout(() => { - if (node.root) { - resolve([ - { value: 'A0', text: 'A0' }, - { value: 'B0', text: 'B0' }, - { value: 'C0', text: 'C0' }, - ]) - } else { - const { value, level } = node - const text = value.substring(0, 1) + const loadCascaderItemData = ( + node: CascaderOption, + level: number + ): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node + const text = value?.toString().substring(0, 1) const value1 = `${text}${level + 1}1` const value2 = `${text}${level + 1}2` - const value3 = `${text}${level + 1}3` resolve([ - { value: value1, text: value1, leaf: level >= 6 }, - { value: value2, text: value2, leaf: level >= 6 }, - { value: value3, text: value3, leaf: level >= 6 }, + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 2 }, ]) - } - }, 2000) + }, 500) + }) } - const change3 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue3(value) + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo3(true) + setVisible(true) }} /> { - setIsVisibleDemo3(false) + setVisible(false) }} - onChange={change3} + onChange={onChange} lazy - onLoad={lazyLoadDemo3} + onLoad={loadCascaderItemData} /> ) diff --git a/src/packages/cascader/demos/h5/demo4.tsx b/src/packages/cascader/demos/h5/demo4.tsx index 1cfdce0e5e..d3a89f6eb5 100644 --- a/src/packages/cascader/demos/h5/demo4.tsx +++ b/src/packages/cascader/demos/h5/demo4.tsx @@ -1,59 +1,67 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' const Demo4 = () => { - const [isVisibleDemo4, setIsVisibleDemo4] = useState(false) - const [value4, setValue4] = useState([]) - const [optionsDemo4] = useState([ - { value: 'A0', text: 'A0' }, - { - value: 'B0', - text: 'B0', - children: [ - { value: 'B11', text: 'B11', leaf: true }, - { value: 'B12', text: 'B12' }, - ], - }, - { value: 'C0', text: 'C0' }, - ]) - - const lazyLoadDemo4 = (node: any, resolve: (children: any) => void) => { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + useEffect(() => { setTimeout(() => { - const { value, level } = node - const text = value.substring(0, 1) - const value1 = `${text}${level + 1}1` - const value2 = `${text}${level + 1}2` - resolve([ - { value: value1, text: value1, leaf: level >= 2 }, - { value: value2, text: value2, leaf: level >= 1 }, + setOptions([ + { value: 'A0', text: 'A0' }, + { + value: 'B0', + text: 'B0', + children: [ + { value: 'B11', text: 'B11', leaf: true }, + { value: 'B12', text: 'B12' }, + ], + }, + { value: 'C0', text: 'C0' }, ]) - }, 500) + }, 300) + }, []) + + const lazyLoadDemo4 = async ( + node: any, + level: number + ): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node + const text = value.substring(0, 1) + const value1 = `${text}${level + 1}1` + const value2 = `${text}${level + 1}2` + resolve([ + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 1 }, + ]) + }, 500) + }) } const change4 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue4(value) + setValue(value) } return ( <> { - setIsVisibleDemo4(true) + setVisible(true) }} /> { - setIsVisibleDemo4(false) + setVisible(false) }} onChange={change4} - lazy onLoad={lazyLoadDemo4} /> diff --git a/src/packages/cascader/demos/h5/demo5.tsx b/src/packages/cascader/demos/h5/demo5.tsx index 92834f7fc1..12ae9501ec 100644 --- a/src/packages/cascader/demos/h5/demo5.tsx +++ b/src/packages/cascader/demos/h5/demo5.tsx @@ -1,47 +1,55 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react' const Demo5 = () => { - const [isVisibleDemo5, setIsVisibleDemo5] = useState(false) - const [value5, setValue5] = useState(['广东省', '广州市']) - const [optionsDemo5] = useState([ - { value: '北京', text: '北京', id: 1, pidd: null }, - { value: '通州区', text: '通州区', id: 11, pidd: 1 }, - { value: '经海路', text: '经海路', id: 111, pidd: 11 }, - { value: '广东省', text: '广东省', id: 2, pidd: null }, - { value: '广州市', text: '广州市', id: 21, pidd: 2 }, - ]) - const [convertConfigDemo5, setConvertConfigDemo5] = useState({ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const format = { topId: null, idKey: 'id', pidKey: 'pidd', - sortKey: '', - }) - const change5 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue5(value) + } + + useEffect(() => { + setTimeout(() => { + setValue(['广东省', '广州市']) + setOptions([ + { value: '北京', text: '北京', id: 1, pidd: null }, + { value: '通州区', text: '通州区', id: 11, pidd: 1, sortKey: 2 }, + { value: '大兴区', text: '大兴区', id: 12, pidd: 1, sortKey: 1 }, + { value: '经海路', text: '经海路', id: 111, pidd: 12, sortKey: 2 }, + { value: '黄亦路', text: '黄亦路', id: 112, pidd: 12, sortKey: 1 }, + { value: '广东省', text: '广东省', id: 2, pidd: null }, + { value: '广州市', text: '广州市', id: 21, pidd: 2 }, + ]) + }, 300) + }, []) + + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo5(true) + setVisible(true) }} /> { - setIsVisibleDemo5(false) + setVisible(false) }} - onChange={change5} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/h5/demo6.tsx b/src/packages/cascader/demos/h5/demo6.tsx index b7c21c3b5a..e452791c8b 100644 --- a/src/packages/cascader/demos/h5/demo6.tsx +++ b/src/packages/cascader/demos/h5/demo6.tsx @@ -1,5 +1,10 @@ -import React, { useState } from 'react' -import { Cell, Cascader, ConfigProvider } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { + Cell, + Cascader, + ConfigProvider, + CascaderOption, +} from '@nutui/nutui-react' const customTheme = { nutuiCascaderItemHeight: '48px', @@ -11,74 +16,78 @@ const customTheme = { } const Demo6 = () => { - const [isVisibleDemo6, setIsVisibleDemo6] = useState(false) - const [value6, setValue6] = useState([]) - const [optionsDemo6] = useState([ - { - value: '浙江', - text: '浙江', - children: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['浙江', '温州', '鹿城区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + setOptions([ { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '西湖区', text: '西湖区' }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', - children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区' }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ { - value: '福州', - text: '福州', + value: '福建', + text: '福建', children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change6 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue6(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } const onPathChange = (value: any, path: any) => { console.log('onPathChange', value, path) @@ -88,26 +97,26 @@ const Demo6 = () => { <> { - setIsVisibleDemo6(true) + setVisible(true) }} /> { - setIsVisibleDemo6(false) + setVisible(false) }} - onChange={change6} + onChange={onChange} onPathChange={onPathChange} - />{' '} + /> ) diff --git a/src/packages/cascader/demos/h5/demo7.tsx b/src/packages/cascader/demos/h5/demo7.tsx new file mode 100644 index 0000000000..64ff2669f6 --- /dev/null +++ b/src/packages/cascader/demos/h5/demo7.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' + +const Demo7 = () => { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + console.log('onchange', value, path) + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setValue(['浙江', '温州', '鹿城区']) + setOptions([ + { + value: '浙江', + text: '浙江', + children: [ + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, + ], + }, + { + value: '湖南', + text: '湖南', + disabled: true, + children: [ + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, + ], + }, + { + value: '福建', + text: '福建', + children: [ + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, + ], + }, + ]) + }, 300) + }, []) + return ( + <> + { + setVisible(true) + }} + /> + { + setVisible(false) + }} + onChange={onChange} + onPathChange={(value, path) => { + console.log(value, path) + }} + /> + + ) +} +export default Demo7 diff --git a/src/packages/cascader/demos/taro/demo1.tsx b/src/packages/cascader/demos/taro/demo1.tsx index b8dbadeafd..3f0da4da82 100644 --- a/src/packages/cascader/demos/taro/demo1.tsx +++ b/src/packages/cascader/demos/taro/demo1.tsx @@ -1,99 +1,102 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react-taro' const Demo1 = () => { - const [isVisibleDemo1, setIsVisibleDemo1] = useState(false) - const [value1, setValue1] = useState([]) - const [optionsDemo1] = useState([ - { - value: '浙江', - text: '浙江', - children: [ - { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setOptions([ { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '芙蓉区', text: '芙蓉区' }, - { value: '岳麓区', text: '岳麓区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, ], }, { - value: '岳阳', - text: '岳阳', + value: '福建', + text: '福建', children: [ - { value: '岳阳楼区', text: '岳阳楼区' }, - { value: '云溪区', text: '云溪区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ - { - value: '福州', - text: '福州', - children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, - ], - }, - ], - }, - ]) - const change1 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue1(value) - } - + ]) + }, 300) + }, []) return ( <> { - setIsVisibleDemo1(true) + setVisible(true) }} /> { - setIsVisibleDemo1(false) + setVisible(false) }} - onChange={change1} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/taro/demo2.tsx b/src/packages/cascader/demos/taro/demo2.tsx index 2ad2a411a1..7c328c3ebc 100644 --- a/src/packages/cascader/demos/taro/demo2.tsx +++ b/src/packages/cascader/demos/taro/demo2.tsx @@ -1,91 +1,95 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react-taro' const Demo2 = () => { - const [isVisibleDemo2, setIsVisibleDemo2] = useState(false) - const [value2, setValue2] = useState(['福建', '福州', '台江区']) - const [optionsDemo2] = useState([ - { - value1: '浙江', - text1: '浙江', - items: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['福建', '福州', '台江区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + setOptions([ { - value1: '杭州', - text1: '杭州', - disabled: true, - items: [ - { value1: '西湖区', text1: '西湖区', disabled: true }, - { value1: '余杭区', text1: '余杭区' }, - ], - }, - { - value1: '温州', - text1: '温州', + value1: '浙江', + text1: '浙江', items: [ - { value1: '鹿城区', text1: '鹿城区' }, - { value1: '瓯海区', text1: '瓯海区' }, + { + value1: '杭州', + text1: '杭州', + disabled: true, + items: [ + { value1: '西湖区', text1: '西湖区', disabled: true }, + { value1: '余杭区', text1: '余杭区' }, + ], + }, + { + value1: '温州', + text1: '温州', + items: [ + { value1: '鹿城区', text1: '鹿城区' }, + { value1: '瓯海区', text1: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value1: '湖南', - text1: '湖南', - disabled: true, - items: [ { - value1: '长沙', - text1: '长沙', + value1: '湖南', + text1: '湖南', disabled: true, items: [ - { value1: '芙蓉区', text1: '芙蓉区' }, - { value1: '岳麓区', text1: '岳麓区' }, - ], - }, - { - value1: '岳阳', - text1: '岳阳', - children: [ - { value1: '岳阳楼区', text1: '岳阳楼区' }, - { value1: '云溪区', text1: '云溪区' }, + { + value1: '长沙', + text1: '长沙', + disabled: true, + items: [ + { value1: '芙蓉区', text1: '芙蓉区' }, + { value1: '岳麓区', text1: '岳麓区' }, + ], + }, + { + value1: '岳阳', + text1: '岳阳', + children: [ + { value1: '岳阳楼区', text1: '岳阳楼区' }, + { value1: '云溪区', text1: '云溪区' }, + ], + }, ], }, - ], - }, - { - value1: '福建', - text1: '福建', - items: [ { - value1: '福州', - text1: '福州', + value1: '福建', + text1: '福建', items: [ - { value1: '鼓楼区', text1: '鼓楼区' }, - { value1: '台江区', text1: '台江区' }, + { + value1: '福州', + text1: '福州', + items: [ + { value1: '鼓楼区', text1: '鼓楼区' }, + { value1: '台江区', text1: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change2 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue2(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo2(true) + setVisible(true) }} /> { }} closeable onClose={() => { - setIsVisibleDemo2(false) + setVisible(false) }} - onChange={change2} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/taro/demo3.tsx b/src/packages/cascader/demos/taro/demo3.tsx index b087ba3247..c89357afb5 100644 --- a/src/packages/cascader/demos/taro/demo3.tsx +++ b/src/packages/cascader/demos/taro/demo3.tsx @@ -1,56 +1,51 @@ import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react-taro' const Demo3 = () => { - const [isVisibleDemo3, setIsVisibleDemo3] = useState(false) - const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32']) + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['A11', 'A21', 'A31', 'A41']) - const lazyLoadDemo3 = (node: any, resolve: (children: any) => void) => { - setTimeout(() => { - if (node.root) { - resolve([ - { value: 'A0', text: 'A0' }, - { value: 'B0', text: 'B0' }, - { value: 'C0', text: 'C0' }, - ]) - } else { - const { value, level } = node - const text = value.substring(0, 1) + const loadCascaderItemData = ( + node: CascaderOption, + level: number + ): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node + const text = value?.toString().substring(0, 1) const value1 = `${text}${level + 1}1` const value2 = `${text}${level + 1}2` - const value3 = `${text}${level + 1}3` resolve([ - { value: value1, text: value1, leaf: level >= 6 }, - { value: value2, text: value2, leaf: level >= 6 }, - { value: value3, text: value3, leaf: level >= 6 }, + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 2 }, ]) - } - }, 2000) + }, 500) + }) } - const change3 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue3(value) + const onChange = (value: any, path: any) => { + setValue(value) } + return ( <> { - setIsVisibleDemo3(true) + setVisible(true) }} /> { - setIsVisibleDemo3(false) + setVisible(false) }} - onChange={change3} + onChange={onChange} lazy - onLoad={lazyLoadDemo3} + onLoad={loadCascaderItemData} /> ) diff --git a/src/packages/cascader/demos/taro/demo4.tsx b/src/packages/cascader/demos/taro/demo4.tsx index a22c6c7aeb..225a3e9807 100644 --- a/src/packages/cascader/demos/taro/demo4.tsx +++ b/src/packages/cascader/demos/taro/demo4.tsx @@ -1,59 +1,67 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react-taro' const Demo4 = () => { - const [isVisibleDemo4, setIsVisibleDemo4] = useState(false) - const [value4, setValue4] = useState([]) - const [optionsDemo4] = useState([ - { value: 'A0', text: 'A0' }, - { - value: 'B0', - text: 'B0', - children: [ - { value: 'B11', text: 'B11', leaf: true }, - { value: 'B12', text: 'B12' }, - ], - }, - { value: 'C0', text: 'C0' }, - ]) - - const lazyLoadDemo4 = (node: any, resolve: (children: any) => void) => { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + useEffect(() => { setTimeout(() => { - const { value, level } = node - const text = value.substring(0, 1) - const value1 = `${text}${level + 1}1` - const value2 = `${text}${level + 1}2` - resolve([ - { value: value1, text: value1, leaf: level >= 2 }, - { value: value2, text: value2, leaf: level >= 1 }, + setOptions([ + { value: 'A0', text: 'A0' }, + { + value: 'B0', + text: 'B0', + children: [ + { value: 'B11', text: 'B11', leaf: true }, + { value: 'B12', text: 'B12' }, + ], + }, + { value: 'C0', text: 'C0' }, ]) - }, 500) + }, 300) + }, []) + + const lazyLoadDemo4 = async ( + node: any, + level: number + ): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node + const text = value.substring(0, 1) + const value1 = `${text}${level + 1}1` + const value2 = `${text}${level + 1}2` + resolve([ + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 1 }, + ]) + }, 500) + }) } const change4 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue4(value) + setValue(value) } return ( <> { - setIsVisibleDemo4(true) + setVisible(true) }} /> { - setIsVisibleDemo4(false) + setVisible(false) }} onChange={change4} - lazy onLoad={lazyLoadDemo4} /> diff --git a/src/packages/cascader/demos/taro/demo5.tsx b/src/packages/cascader/demos/taro/demo5.tsx index 4df6f2023b..79ef7e20d5 100644 --- a/src/packages/cascader/demos/taro/demo5.tsx +++ b/src/packages/cascader/demos/taro/demo5.tsx @@ -1,47 +1,55 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react-taro' const Demo5 = () => { - const [isVisibleDemo5, setIsVisibleDemo5] = useState(false) - const [value5, setValue5] = useState(['广东省', '广州市']) - const [optionsDemo5] = useState([ - { value: '北京', text: '北京', id: 1, pidd: null }, - { value: '通州区', text: '通州区', id: 11, pidd: 1 }, - { value: '经海路', text: '经海路', id: 111, pidd: 11 }, - { value: '广东省', text: '广东省', id: 2, pidd: null }, - { value: '广州市', text: '广州市', id: 21, pidd: 2 }, - ]) - const [convertConfigDemo5, setConvertConfigDemo5] = useState({ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const format = { topId: null, idKey: 'id', pidKey: 'pidd', - sortKey: '', - }) - const change5 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue5(value) + } + + useEffect(() => { + setTimeout(() => { + setValue(['广东省', '广州市']) + setOptions([ + { value: '北京', text: '北京', id: 1, pidd: null }, + { value: '通州区', text: '通州区', id: 11, pidd: 1, sortKey: 2 }, + { value: '大兴区', text: '大兴区', id: 12, pidd: 1, sortKey: 1 }, + { value: '经海路', text: '经海路', id: 111, pidd: 12, sortKey: 2 }, + { value: '黄亦路', text: '黄亦路', id: 112, pidd: 12, sortKey: 1 }, + { value: '广东省', text: '广东省', id: 2, pidd: null }, + { value: '广州市', text: '广州市', id: 21, pidd: 2 }, + ]) + }, 300) + }, []) + + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo5(true) + setVisible(true) }} /> { - setIsVisibleDemo5(false) + setVisible(false) }} - onChange={change5} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/taro/demo6.tsx b/src/packages/cascader/demos/taro/demo6.tsx index 20c66f81bb..a1687fd6a4 100644 --- a/src/packages/cascader/demos/taro/demo6.tsx +++ b/src/packages/cascader/demos/taro/demo6.tsx @@ -1,5 +1,10 @@ -import React, { useState } from 'react' -import { Cell, Cascader, ConfigProvider } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { + Cell, + Cascader, + ConfigProvider, + CascaderOption, +} from '@nutui/nutui-react-taro' const customTheme = { nutuiCascaderItemHeight: '48px', @@ -11,74 +16,78 @@ const customTheme = { } const Demo6 = () => { - const [isVisibleDemo6, setIsVisibleDemo6] = useState(false) - const [value6, setValue6] = useState([]) - const [optionsDemo6] = useState([ - { - value: '浙江', - text: '浙江', - children: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['浙江', '温州', '鹿城区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + setOptions([ { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '西湖区', text: '西湖区' }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', - children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区' }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ { - value: '福州', - text: '福州', + value: '福建', + text: '福建', children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change6 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue6(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } const onPathChange = (value: any, path: any) => { console.log('onPathChange', value, path) @@ -88,26 +97,26 @@ const Demo6 = () => { <> { - setIsVisibleDemo6(true) + setVisible(true) }} /> { - setIsVisibleDemo6(false) + setVisible(false) }} - onChange={change6} + onChange={onChange} onPathChange={onPathChange} - />{' '} + /> ) diff --git a/src/packages/cascader/demos/taro/demo7.tsx b/src/packages/cascader/demos/taro/demo7.tsx new file mode 100644 index 0000000000..7160f12a7d --- /dev/null +++ b/src/packages/cascader/demos/taro/demo7.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react-taro' + +const Demo7 = () => { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + console.log('onchange', value, path) + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setValue(['浙江', '温州', '鹿城区']) + setOptions([ + { + value: '浙江', + text: '浙江', + children: [ + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, + ], + }, + { + value: '湖南', + text: '湖南', + disabled: true, + children: [ + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, + ], + }, + { + value: '福建', + text: '福建', + children: [ + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, + ], + }, + ]) + }, 300) + }, []) + return ( + <> + { + setVisible(true) + }} + /> + { + setVisible(false) + }} + onChange={onChange} + onPathChange={(value, path) => { + console.log(value, path) + }} + /> + + ) +} +export default Demo7 diff --git a/src/packages/cascader/doc.en-US.md b/src/packages/cascader/doc.en-US.md index e382f5d190..cf6e423f1a 100644 --- a/src/packages/cascader/doc.en-US.md +++ b/src/packages/cascader/doc.en-US.md @@ -20,6 +20,16 @@ Pass in the `options` list ::: +### Basic Usage - Uncontroled + +Pass in the `options` list + +:::demo + + + +::: + ### Custom attribute name use `optionKey` Specify the property name. @@ -32,7 +42,8 @@ use `optionKey` Specify the property name. ### Async loading -Use `lazy` to identify whether data needs to be obtained dynamically. At this time, not transmitting `options` means that all data needs to be loaded through `lazyload` . The first loading is distinguished by the `root` attribute. When a non leaf node is encountered, the `lazyload` method will be called. The parameters are the current node and the `resolve` method. Note that the `resolve` method must be called. If it is not transmitted to a child node, it will be treated as a leaf node. +The `lazy` attribute indicates that automatic data loading is enabled. Cascader implements the logic of automatic data loading through `value` and `onLoad`. The `lazy` attribute must be set at the same time as the `onLoad` attribute. +The data type returned by the `onLoad` method is `CascaderOption[]` :::demo @@ -42,6 +53,9 @@ Use `lazy` to identify whether data needs to be obtained dynamically. At this ti ### Async loading of partial data +Dynamic loading of some data means that the initial `options` has been set. For example, the data of the user's current address is first obtained and assigned to `options`. The data loading when the user switches provinces or regions is implemented through the `onLoad` method. +**No need to set the `lazy` attribute. ** + :::demo @@ -88,9 +102,9 @@ Use configprovider to set custom CSS | closeIconPosition | Cancel the button position and inherit the popup component | `string` | `top-right` | | closeIcon | Customize the close button and inherit the popup component | `ReactNode` | `close` | | closeable | Whether to display the close button and inherit the popup component | `boolean` | `true` | -| onLoad | Dynamic loading callback, which takes effect when dynamic loading is enabled | `(node: any, resolve: any) => void` | `-` | -| onChange | Triggered when the selected value changes | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | Triggered when the selected item changes | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | Dynamic loading callback, which takes effect when dynamic loading is enabled | `(node: any) => void` | `-` | +| onChange | Triggered when the selected value changes | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | Triggered when the selected item changes | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref diff --git a/src/packages/cascader/doc.md b/src/packages/cascader/doc.md index 8758db7ea1..e0c5c82c61 100644 --- a/src/packages/cascader/doc.md +++ b/src/packages/cascader/doc.md @@ -20,6 +20,16 @@ import { Cascader } from '@nutui/nutui-react' ::: +### 基础用法-非受控 + +传入`options`列表 + +:::demo + + + +::: + ### 自定义属性名称 可通过`optionKey` 指定属性名。 @@ -32,7 +42,8 @@ import { Cascader } from '@nutui/nutui-react' ### 动态加载 -使用`lazy`标识是否需要动态获取数据,此时不传`options`代表所有数据都需要通过`onLoad`加载,首次加载通过`root`属性区分,当遇到非叶子节点时会调用`onLoad`方法,参数为当前节点和`resolve`方法,注意`resolve`方法必须调用,不传子节点时会被当做叶子节点处理。 +`lazy` 属性表示开启数据的自动加载,Cascader 内部通过 `value` 和 `onLoad` 实现了自动加载数据的逻辑。`lazy` 属性必须和 `onLoad` 属性同时设置。 +`onLoad`方法返回的数据类型为 `CascaderOption[]` :::demo @@ -42,6 +53,9 @@ import { Cascader } from '@nutui/nutui-react' ### 部分数据动态加载 +部分数据动态加载是指已经设置了初始的 `options`,例如,首先获取了用户当前地址的数据,并赋值给 `options`,用户切换省份或地区的数据加载则通过 `onLoad` 方法实现。 +**无需设置 `lazy` 属性。** + :::demo @@ -88,9 +102,9 @@ import { Cascader } from '@nutui/nutui-react' | closeIconPosition | 取消按钮位置,继承 Popup 组件 | `string` | `top-right` | | closeIcon | 自定义关闭按钮,继承 Popup 组件 | `ReactNode` | `close` | | closeable | 是否显示关闭按钮,继承 Popup 组件 | `boolean` | `true` | -| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any, resolve: any) => void` | `-` | -| onChange | 选中值改变时触发 | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | 选中项改变时触发 | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any) => void` | `-` | +| onChange | 选中值改变时触发 | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | 选中项改变时触发 | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref diff --git a/src/packages/cascader/doc.taro.md b/src/packages/cascader/doc.taro.md index 3d729965ff..d50fa543f3 100644 --- a/src/packages/cascader/doc.taro.md +++ b/src/packages/cascader/doc.taro.md @@ -20,6 +20,16 @@ import { Cascader } from '@nutui/nutui-react-taro' ::: +### 基础用法-非受控 + +传入`options`列表 + +:::demo + + + +::: + ### 自定义属性名称 可通过`textKey`、`valueKey`、`childrenKey`指定属性名。 @@ -32,7 +42,8 @@ import { Cascader } from '@nutui/nutui-react-taro' ### 动态加载 -使用`lazy`标识是否需要动态获取数据,此时不传`options`代表所有数据都需要通过`onLoad`加载,首次加载通过`root`属性区分,当遇到非叶子节点时会调用`onLoad`方法,参数为当前节点和`resolve`方法,注意`resolve`方法必须调用,不传子节点时会被当做叶子节点处理。 +`lazy` 属性表示开启数据的自动加载,Cascader 内部通过 `value` 和 `onLoad` 实现了自动加载数据的逻辑。`lazy` 属性必须和 `onLoad` 属性同时设置。 +`onLoad`方法返回的数据类型为 `CascaderOption[]` :::demo @@ -42,6 +53,9 @@ import { Cascader } from '@nutui/nutui-react-taro' ### 部分数据动态加载 +部分数据动态加载是指已经设置了初始的 `options`,例如,首先获取了用户当前地址的数据,并赋值给 `options`,用户切换省份或地区的数据加载则通过 `onLoad` 方法实现。 +**无需设置 `lazy` 属性。** + :::demo @@ -88,9 +102,9 @@ import { Cascader } from '@nutui/nutui-react-taro' | closeIconPosition | 取消按钮位置,继承 Popup 组件 | `string` | `top-right` | | closeIcon | 自定义关闭按钮,继承 Popup 组件 | `ReactNode` | `close` | | closeable | 是否显示关闭按钮,继承 Popup 组件 | `boolean` | `true` | -| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any, resolve: any) => void` | `-` | -| onChange | 选中值改变时触发 | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | 选中项改变时触发 | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any) => void` | `-` | +| onChange | 选中值改变时触发 | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | 选中项改变时触发 | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref diff --git a/src/packages/cascader/doc.zh-TW.md b/src/packages/cascader/doc.zh-TW.md index 7ac7f7104f..21321550c5 100644 --- a/src/packages/cascader/doc.zh-TW.md +++ b/src/packages/cascader/doc.zh-TW.md @@ -20,6 +20,16 @@ import { Cascader } from '@nutui/nutui-react' ::: +### 基础用法-非受控 + +传入`options`列表 + +:::demo + + + +::: + ### 自定義屬性名稱 可通過`textKey`、`valueKey`、`childrenKey`指定屬性名。 @@ -32,7 +42,8 @@ import { Cascader } from '@nutui/nutui-react' ### 動態加載 -使用`lazy`標識是否需要動態獲取數據,此時不傳`options`代表所有數據都需要通過`onLoad`加載,首次加載通過`root`屬性區分,當遇到非葉子節點時會調用`onLoad`方法,參數為當前節點和`resolve`方法,註意`resolve`方法必須調用,不傳子節點時會被當做葉子節點處理。 +`lazy` 屬性表示開啟資料的自動加載,Cascader 內部透過 `value` 和 `onLoad` 實作了自動載入資料的邏輯。 `lazy` 屬性必須同時和 `onLoad` 屬性設定。 +`onLoad`方法傳回的資料類型為 `CascaderOption[]` :::demo @@ -42,6 +53,9 @@ import { Cascader } from '@nutui/nutui-react' ### 部分數據動態加載 +部分數據動態載入是指已經設定了初始的 `options`,例如,首先獲取了用戶當前地址的數據,並賦值給 `options`,用戶切換省份或地區的數據加載則透過 `onLoad` 方法實現。 +**無需設定 `lazy` 屬性。 ** + :::demo @@ -88,9 +102,9 @@ import { Cascader } from '@nutui/nutui-react' | closeIconPosition | 取消按鈕位置,繼承 Popup 組件 | `string` | `top-right` | | closeIcon | 自定義關閉按鈕,繼承 Popup 組件 | `ReactNode` | `close` | | closeable | 是否顯示關閉按鈕,繼承 Popup 組件 | `boolean` | `true` | -| onLoad | 動態加載回調,開啟動態加載時生效 | `(node: any, resolve: any) => void` | `-` | -| onChange | 選中值改變時觸發 | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | 選中項改變時觸發 | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | 動態加載回調,開啟動態加載時生效 | `(node: any) => void` | `-` | +| onChange | 選中值改變時觸發 | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | 選中項改變時觸發 | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref diff --git a/src/packages/cascader/helper.ts b/src/packages/cascader/helper.ts deleted file mode 100644 index 2e1e14cf23..0000000000 --- a/src/packages/cascader/helper.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { CascaderOption, CascaderConfig, CascaderFormat } from './types' - -export const formatTree = ( - tree: CascaderOption[], - parent: CascaderOption | null, - config: CascaderConfig -): CascaderOption[] => - tree.map((node: any) => { - const { - value: valueKey = 'value', - text: textKey = 'text', - children: childrenKey = 'children', - } = config - const { - [valueKey]: value, - [textKey]: text, - [childrenKey]: children, - ...others - } = node - const newNode: CascaderOption = { - loading: false, - ...others, - level: parent ? ((parent && parent.level) || 0) + 1 : 0, - value, - text, - children, - _parent: parent, - } - if (newNode.children && newNode.children.length) { - newNode.children = formatTree(newNode.children, newNode, config) - } - return newNode - }) - -export const eachTree = ( - tree: CascaderOption[], - cb: (node: CascaderOption) => unknown -): void => { - let i = 0 - let node: CascaderOption - while ((node = tree[i++])) { - if (cb(node) === true) { - break - } - if (node.children && node.children.length) { - eachTree(node.children, cb) - } - } -} - -const defaultConvertConfig = { - topId: null, - idKey: 'id', - pidKey: 'pid', - sortKey: '', -} -export const convertListToOptions = ( - list: CascaderOption[], - options: CascaderFormat -): CascaderOption[] => { - const mergedOptions = { - ...defaultConvertConfig, - ...(options || {}), - } - const { topId, idKey, pidKey, sortKey } = mergedOptions - let result: CascaderOption[] = [] - let map: any = {} - list.forEach((node: any) => { - node = { ...node } - const { [idKey]: id, [pidKey]: pid } = node - const children = (map[pid] = map[pid] || []) - if (!result.length && pid === topId) { - result = children - } - children.push(node) - node.children = map[id] || (map[id] = []) - }) - - if (sortKey) { - Object.keys(map).forEach((i) => { - if (map[i].length > 1) { - map[i].sort((a: any, b: any) => a[sortKey] - b[sortKey]) - } - }) - } - map = null - return result -} diff --git a/src/packages/cascader/tree.ts b/src/packages/cascader/tree.ts deleted file mode 100644 index a4e0ce24b2..0000000000 --- a/src/packages/cascader/tree.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { CascaderOption, CascaderConfig, CascaderValue } from './types' -import { formatTree, eachTree } from './helper' - -class Tree { - nodes: CascaderOption[] - - readonly config: CascaderConfig - - constructor(nodes: CascaderOption[], config?: CascaderConfig) { - this.config = { - value: 'value', - text: 'text', - children: 'children', - ...(config || {}), - } - this.nodes = formatTree(nodes, null, this.config) - } - - updateChildren(nodes: CascaderOption[], parent: CascaderOption | null): void { - if (!parent) { - this.nodes = formatTree(nodes, null, this.config) - } else { - parent.children = formatTree(nodes, parent, this.config) - } - } - - // for test - getNodeByValue(value: CascaderOption['value']): CascaderOption | void { - let foundNode - eachTree(this.nodes, (node: CascaderOption) => { - if (node.value === value) { - foundNode = node - return true - } - return null - }) - return foundNode - } - - getPathNodesByValue(value: CascaderValue): CascaderOption[] { - if (!value.length) { - return [] - } - - const pathNodes = [] - let currentNodes: CascaderOption[] | void = this.nodes - - while (currentNodes && currentNodes.length) { - const foundNode: CascaderOption | void = currentNodes.find( - (node) => node.value === value[node.level as number] - ) - - if (!foundNode) { - break - } - - pathNodes.push(foundNode) - currentNodes = foundNode.children - } - - return pathNodes - } - - // eslint-disable-next-line class-methods-use-this - isLeaf = (node: CascaderOption, lazy: boolean): boolean => { - const { leaf, children } = node - const hasChildren = Array.isArray(children) && Boolean(children.length) - return leaf || (!hasChildren && !lazy) - } - - hasChildren = (node: CascaderOption, lazy: boolean): boolean => { - const isLeaf = this.isLeaf(node, lazy) - - if (isLeaf) { - return false - } - - const { children } = node - return Array.isArray(children) && Boolean(children.length) - } -} - -export default Tree diff --git a/src/packages/cascader/types.ts b/src/packages/cascader/types.ts index 008a3d07c4..3e57cfb998 100644 --- a/src/packages/cascader/types.ts +++ b/src/packages/cascader/types.ts @@ -11,9 +11,9 @@ export interface CascaderOption { disabled?: boolean children?: CascaderOption[] leaf?: boolean - level?: number loading?: boolean - root?: boolean + + [key: string]: any } export interface CascaderConfig { @@ -36,3 +36,8 @@ export interface CascaderFormat { pidKey?: string sortKey?: string } + +export type CascaderActions = { + open: () => void + close: () => void +} diff --git a/src/packages/cascader/utils.ts b/src/packages/cascader/utils.ts new file mode 100644 index 0000000000..872fdc7cb2 --- /dev/null +++ b/src/packages/cascader/utils.ts @@ -0,0 +1,61 @@ +import { CascaderOption, CascaderFormat, CascaderOptionKey } from './types' + +export const normalizeOptions = ( + options: CascaderOption[], + keyMap: CascaderOptionKey +): CascaderOption[] | undefined => { + if (!options) return undefined + return options.map((opt: any) => { + const { + [keyMap.textKey]: text, + [keyMap.valueKey]: value, + [keyMap.childrenKey]: children, + ...others + } = opt + return { + text, + value, + children: normalizeOptions(children, keyMap), + ...others, + } as CascaderOption + }) +} + +export const normalizeListOptions = ( + options: CascaderOption[], + format: CascaderFormat +) => { + const defaultConvertConfig = { + topId: null, + idKey: 'id', + pidKey: 'pid', + sortKey: 'sortKey', + } + const mergedFormat = { + ...defaultConvertConfig, + ...format, + } + const { topId, idKey, pidKey, sortKey } = mergedFormat + const map: { [key: string]: CascaderOption[] } = {} + options.forEach((opt) => { + const { [pidKey]: pid, [idKey]: id, ...others } = opt as any + const newNode: any = { pid, id, ...others } + if (map[pid]) { + map[pid].push(newNode) + } else { + map[pid] = [newNode] + } + }) + for (const key in map) { + // eslint-disable-next-line no-continue + if (!Object.prototype.hasOwnProperty.call(map, key)) continue + map[key].sort((a: any, b: any) => a[sortKey] - b[sortKey]) + map[key].forEach((option: any) => { + if (map[option.id]) { + option.children = map[option.id] + } + }) + } + // @ts-ignore + return map[topId] +} diff --git a/src/utils/is-empty.ts b/src/utils/is-empty.ts new file mode 100644 index 0000000000..c4570c2773 --- /dev/null +++ b/src/utils/is-empty.ts @@ -0,0 +1,6 @@ +export const isEmpty = (p: T): boolean => { + if (p === null || p === undefined) return true + if (typeof p === 'object' && Object.keys(p).length === 0) return true + if (Array.isArray(p) && p.length === 0) return true + return false +}