未验证 提交 fcc2a472 编写于 作者: A ailululu 提交者: GitHub

feat: 新增 cascader 组件 (#202)

上级 a6fb8f32
......@@ -97,5 +97,7 @@ module.exports = {
],
'react/require-default-props': 0,
'no-bitwise': 0,
'no-multi-assign': 0, // 禁止连续赋值
'no-cond-assign': 0, // 禁止条件表达式中出现赋值操作符
},
}
......@@ -474,6 +474,16 @@
"taro": false,
"author": "swag~jun"
},
{
"version": "0.1.0",
"name": "Cascader",
"type": "component",
"cName": "级联选择器",
"desc": "级联选择,用于多层级数据的选择,典型场景为省市区选择。",
"sort": 1,
"show": true,
"author": "ailululu"
},
{
"version": "1.0.0",
"name": "SearchBar",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cascader change tab 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="overlay-fade-enter-active nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
<div
class="round popup-bottom cascadar-popup undefined nut-popup"
style="z-index: 2000; animation-duration: 0.3s;"
>
<div
class="nut-cascader__title"
/>
<div
class="horizontal nut-tabs"
>
<div
class="line normal nut-tabs__titles"
>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
福建
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
福州
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item active"
>
<span
class="nut-tabs__titles-item__text"
>
鼓楼区
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
</div>
<div
class="nut-tabs__content"
style="transform: translate3d(-200%, 0, 0); transition-duration: 300ms;"
>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
浙江
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="disabled nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
湖南
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福建
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福州
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="active nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
鼓楼区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
台江区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Cascader change tab 2`] = `
<div>
<div
class="nut-cascader "
>
<div
class="overlay-fade-enter-active nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
<div
class="round popup-bottom cascadar-popup undefined nut-popup"
style="z-index: 2000; animation-duration: 0.3s;"
>
<div
class="nut-cascader__title"
/>
<div
class="horizontal nut-tabs"
>
<div
class="line normal nut-tabs__titles"
>
<div
class="nut-tabs__titles-item active"
>
<span
class="nut-tabs__titles-item__text"
>
福建
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
福州
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
鼓楼区
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
</div>
<div
class="nut-tabs__content"
style="transform: translate3d(-0%, 0, 0); transition-duration: 300ms;"
>
<div
class="active nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
浙江
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="disabled nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
湖南
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福建
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福州
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
鼓楼区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
台江区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Cascader no visible 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="overlay-fade-enter-active nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
<div
class="round popup-bottom cascadar-popup undefined nut-popup"
style="z-index: 2000; animation-duration: 0.3s;"
>
<div
class="nut-cascader__title"
/>
<div
class="horizontal nut-tabs"
>
<div
class="line normal nut-tabs__titles"
>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
福建
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
福州
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item active"
>
<span
class="nut-tabs__titles-item__text"
>
鼓楼区
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
</div>
<div
class="nut-tabs__content"
style="transform: translate3d(-200%, 0, 0); transition-duration: 300ms;"
>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
浙江
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="disabled nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
湖南
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福建
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福州
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="active nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
鼓楼区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
台江区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Cascader options 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="first-render hidden-render nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
</div>
</div>
`;
exports[`Cascader options with convertConfig 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="first-render hidden-render nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
</div>
</div>
`;
exports[`Cascader options with valueKey/textKey/childrenKey 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="first-render hidden-render nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
</div>
</div>
`;
exports[`Cascader select with lazy 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="first-render hidden-render nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
</div>
</div>
`;
exports[`Cascader select with lazy 2`] = `
<div>
<div
class="nut-cascader "
>
<div
class="first-render hidden-render nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
</div>
</div>
`;
exports[`Cascader value 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="first-render hidden-render nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
</div>
</div>
`;
exports[`Cascader value with lazy 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="overlay-fade-enter-active nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
<div
class="round popup-bottom cascadar-popup undefined nut-popup"
style="z-index: 2000; animation-duration: 0.3s;"
>
<div
class="nut-cascader__title"
/>
<div
class="horizontal nut-tabs"
>
<div
class="line normal nut-tabs__titles"
/>
<div
class="nut-tabs__content"
style="transform: translate3d(--100%, 0, 0); transition-duration: 300ms;"
/>
</div>
</div>
</div>
</div>
`;
exports[`Cascader visible true 1`] = `
<div>
<div
class="nut-cascader "
>
<div
class="overlay-fade-enter-active nut-overlay"
style="z-index: 2000; animation-duration: 0.3s;"
/>
<div
class="round popup-bottom cascadar-popup undefined nut-popup"
style="z-index: 2000; animation-duration: 0.3s;"
>
<div
class="nut-cascader__title"
/>
<div
class="horizontal nut-tabs"
>
<div
class="line normal nut-tabs__titles"
>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
福建
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item "
>
<span
class="nut-tabs__titles-item__text"
>
福州
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
<div
class="nut-tabs__titles-item active"
>
<span
class="nut-tabs__titles-item__text"
>
鼓楼区
</span>
<span
class="nut-tabs__titles-item__line"
/>
</div>
</div>
<div
class="nut-tabs__content"
style="transform: translate3d(-200%, 0, 0); transition-duration: 300ms;"
>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
浙江
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="disabled nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
湖南
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福建
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
福州
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
</div>
</div>
<div
class="active nut-tabpane"
>
<div
class="nut-cascader-pane"
>
<div
class="active nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
鼓楼区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist nut-cascader-item__icon-check"
/>
</div>
<div
class="nut-cascader-item"
>
<div
class="nut-cascader-item__title"
>
台江区
</div>
<i
class="nutui-iconfont nut-icon nut-icon-checklist "
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
import * as React from 'react'
import { render, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import { Cascader } from '../cascader'
import { CascaderOption } from '../types'
import Tree from '../tree'
import { formatTree, convertListToOptions } from '../helper'
const later = (t = 0) => new Promise((r) => setTimeout(r, t))
const mockOptions = [
{
value: '浙江',
text: '浙江',
children: [
{
value: '杭州',
text: '杭州',
disabled: true,
children: [
{ value: '西湖区', text: '西湖区' },
{ value: '余杭区', text: '余杭区' },
],
},
{
value: '温州',
text: '温州',
children: [
{ value: '鹿城区', text: '鹿城区' },
{ value: '瓯海区', text: '瓯海区' },
],
},
],
},
{
value: '湖南',
text: '湖南',
disabled: true,
},
{
value: '福建',
text: '福建',
children: [
{
value: '福州',
text: '福州',
children: [
{ value: '鼓楼区', text: '鼓楼区' },
{ value: '台江区', text: '台江区' },
],
},
],
},
]
const mockKeyConfigOptions = [
{
name: '浙江',
items: [
{
name: '杭州',
disabled: true,
items: [{ name: '西湖区' }, { name: '余杭区' }],
},
{
name: '温州',
items: [{ name: '鹿城区' }, { name: '瓯海区' }],
},
],
},
{
name: '湖南',
disabled: true,
},
{
name: '福建',
items: [
{
name: '福州',
items: [{ name: '鼓楼区' }, { name: '台江区' }],
},
],
},
]
const mockConvertOptions = [
{ value: '北京', text: '北京', nodeId: 1, nodePid: 0, sort: 2 },
{ value: '朝阳区', text: '朝阳区', nodeId: 11, nodePid: 1 },
{ value: '亦庄', text: '亦庄', nodeId: 111, nodePid: 11 },
{ value: '广东省', text: '广东省', nodeId: 2, nodePid: 0, sort: 1 },
{ 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('鼓楼区')
})
// test('updateChildren with CascaderConfig', () => {
// const tree = new Tree(
// [
// {
// name: '福建',
// items: [{ name: '福州' }],
// },
// ],
// {
// value: 'name',
// text: 'name',
// children: 'items',
// }
// )
// expect(tree.nodes).toMatchObject([
// {
// text: '福建',
// value: '福建',
// children: [{ text: '福州', value: '福州' }],
// },
// ])
// let node = tree.getNodeByValue('福州') as CascaderOption
// expect(node).toBeTruthy()
// tree.updateChildren([{ name: '鼓楼区' }], node)
// node = tree.getNodeByValue('鼓楼区') as CascaderOption
// expect(node).toBeTruthy()
// expect(node).toMatchObject({
// text: '鼓楼区',
// value: '鼓楼区',
// })
// })
})
describe('Cascader', () => {
it('options', async () => {
const { container } = render(
<Cascader value={['福建', '福州', '鼓楼区']} options={mockOptions} />
)
expect(container).toMatchSnapshot()
})
it('options with valueKey/textKey/childrenKey', async () => {
const { container } = render(
<Cascader
value={['福建', '福州', '鼓楼区']}
options={mockKeyConfigOptions}
valueKey="name"
textKey="name"
childrenKey="items"
/>
)
expect(container).toMatchSnapshot()
})
it('options with convertConfig', async () => {
const { container } = render(
<Cascader
value={['广东省', '广州市']}
options={mockConvertOptions}
convertConfig={{
topId: 0,
idKey: 'nodeId',
pidKey: 'nodePid',
sortKey: 'sort',
}}
/>
)
expect(container).toMatchSnapshot()
})
it('visible false', async () => {
const { container } = render(
<Cascader visible={false} options={mockOptions} />
)
expect(container.querySelector('.nut-popup')).toBeNull()
})
it('visible true', async () => {
const { container } = render(
<Cascader
visible
value={['福建', '福州', '鼓楼区']}
options={mockOptions}
/>
)
expect(container.querySelector('.nut-popup')).toBe
expect(container).toMatchSnapshot()
// 点击叶子节点时关闭popup
const el = container.querySelectorAll('.nut-cascader-pane')[2].childNodes[0]
fireEvent.click(el)
// expect(
// container.querySelectorAll('.nut-cascader-pane')[2].childNodes[0]
// ).toEqual(
// '<div class="active nut-cascader-item"><div class="nut-cascader-item__title">鼓楼区</div><i class="nut-cascader-item__icon-check nut-icon nutui-iconfont nut-icon-checklist"/></div>'
// )
})
it('value', async () => {
const { container } = render(<Cascader options={mockOptions} />)
expect(
container.querySelectorAll('.nut-cascader-item[aria-checked="true"]')
.length
).toBe(0)
expect(container).toMatchSnapshot()
})
it('select', async () => {
const change = jest.fn()
const pathChange = jest.fn()
const { container } = render(
<Cascader
visible
value={['福建', '福州', '鼓楼区']}
options={mockOptions}
onChange={change}
onPathChange={pathChange}
/>
)
// 模拟点击
const pane = container.querySelectorAll('.nut-cascader-pane')[2]
fireEvent.click(pane)
const item = pane.childNodes[0]
// console.log('item', item)
fireEvent.click(item)
// let pathChange: any = container.emitted().pathChange[0];
expect(pathChange).toBeCalled()
// ...
})
it('value with lazy', async () => {
const { container } = render(
<Cascader
visible
value={['A0', 'A12', 'A21']}
lazy
lazyLoad={(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 value1 = `${text}${level + 1}1`
const value2 = `${text}${level + 1}2`
resolve([
{ value: value1, text: value1, leaf: level >= 1 },
{ value: value2, text: value2, leaf: level >= 1 },
])
}
}, 50)
}}
/>
)
await later(60)
expect(container).toMatchSnapshot()
})
it('select with lazy', async () => {
const { container } = render(
<Cascader
lazy
lazyLoad={(node: any, resolve: (children: any) => void) => {
setTimeout(() => {
setTimeout(() => {
// root表示第一层数据
if (node.root) {
resolve([
{ value: 'A0', text: 'A0' },
{ value: 'B0', text: 'B0' },
])
} else {
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 >= 1 },
{ value: value2, text: value2, leaf: level >= 1 },
])
}
}, 50)
}, 50)
}}
/>
)
expect(container).toMatchSnapshot()
await later(60)
expect(container).toMatchSnapshot()
// ...
})
it('change tab', async () => {
const change = jest.fn()
const pathChange = jest.fn()
const { container } = render(
<Cascader
visible
value={['福建', '福州', '鼓楼区']}
options={mockOptions}
onChange={change}
onPathChange={pathChange}
/>
)
expect(container).toMatchSnapshot()
expect(container.querySelector('.nut-popup')).toBe
const tabPane = container.querySelectorAll('.nut-tabs__titles-item')[0]
fireEvent.click(tabPane)
expect(container).toMatchSnapshot()
})
})
@import '../popup/popup.scss';
.nut-cascader {
width: 100%;
font-size: $cascader-font-size;
line-height: $cascader-line-height;
&__title {
display: flex;
justify-content: center;
align-items: center;
padding: $cascader-title-padding;
text-align: center;
font-weight: bold;
line-height: $cascader-title-line-height;
color: #1a1a1a;
font-size: $cascader-title-font-size;
}
.nut-tabs__titles {
padding: $cascader-tabs-item-padding;
background: #fff;
}
.nut-tabs__titles-item {
flex: initial;
min-width: auto;
width: auto;
padding: $cascader-tabs-item-padding;
white-space: nowrap;
}
.nut-tabpane {
padding: 0;
}
&-pane {
display: block;
padding: 0;
margin: 0;
width: 100%;
padding-top: $cascader-pane-paddingTop;
height: $cascader-pane-height;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
&-item {
display: flex;
align-items: center;
padding: $cascader-item-padding;
margin: 0;
cursor: pointer;
font-size: $cascader-item-font-size;
color: $cascader-item-color;
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.active {
&:not(.disabled) {
color: $cascader-item-active-color;
}
.nut-cascader-item__icon-check {
visibility: visible;
color: $cascader-item-active-color;
}
}
}
&-item__title {
flex: 1;
}
.nut-icon-checklist {
margin-left: $cascader-icon-checklist-marginLeft;
visibility: hidden;
}
}
import React, {
ForwardRefRenderFunction,
PropsWithChildren,
useState,
useEffect,
CSSProperties,
} from 'react'
import classNames from 'classnames'
import Popup from '@/packages/popup'
import { Tabs } from '@/packages/tabs/tabs'
import { TabPane } from '@/packages/tabpane/tabpane'
import { CascaderItem } from './cascaderItem'
import bem from '@/utils/bem'
import { convertListToOptions } from './helper'
import {
CascaderPane,
CascaderOption,
CascaderValue,
convertConfig,
} from './types'
import Tree from './tree'
export interface CascaderProps {
className: string
style: CSSProperties
visible: boolean // popup 显示状态
options: CascaderOption[]
value: string[]
title: string
textKey: string
valueKey: string
childrenKey: string
convertConfig: Record<string, string | number | null>
closeable: boolean
closeIconPosition: string
closeIcon: string
lazy: boolean
lazyLoad: (node: any, resolve: any) => void
onClose?: () => void
onChange: (value: any, params: any) => void
onPathChange: (value: any, params: any) => void
}
const defaultProps = {
className: '',
style: {},
visible: false,
options: [],
value: [],
title: '',
textKey: 'text',
valueKey: 'value',
childrenKey: 'children',
convertConfig: {},
closeable: false,
closeIconPosition: 'top-right',
closeIcon: 'close',
lazy: false,
lazyLoad: () => {},
onClose: () => {},
onChange: () => {},
onPathChange: () => {},
...Popup.defaultProps,
} as CascaderProps
const InternalCascader: ForwardRefRenderFunction<
unknown,
PropsWithChildren<Partial<CascaderProps>>
> = (props) => {
const {
className,
style,
visible,
options,
value,
title,
textKey,
valueKey,
childrenKey,
convertConfig,
closeable,
closeIconPosition,
closeIcon,
lazy,
lazyLoad,
onClose,
onChange,
onPathChange,
} = { ...defaultProps, ...props }
const [tabvalue, setTabvalue] = useState('c1')
const [optiosData, setOptiosData] = useState<CascaderPane[]>([])
const isLazy = () => state.configs.lazy && Boolean(state.configs.lazyLoad)
const [state] = useState({
optionsData: [] as any,
panes: [
{
nodes: [] as any,
selectedNode: [] as CascaderOption | null,
paneKey: '',
},
],
innerValue: value as CascaderValue,
tree: new Tree([], {}),
tabsCursor: 0, // 选中的tab项
initLoading: false,
currentProcessNode: [] as CascaderOption | null,
configs: {
lazy,
lazyLoad,
valueKey,
textKey,
childrenKey,
convertConfig,
},
lazyLoadMap: new Map(),
})
const b = bem('cascader')
const classes = classNames(b(''))
const classesPane = classNames({
[`${b('')}-pane`]: true,
})
useEffect(() => {
initData()
}, [])
useEffect(() => {
if (value !== state.innerValue) {
state.innerValue = value as CascaderValue
}
}, [value])
useEffect(() => {
initData()
}, [options])
const initData = async () => {
// 初始化开始处理数据
state.lazyLoadMap.clear()
if (convertConfig && Object.keys(convertConfig).length > 0) {
state.optionsData = convertListToOptions(
options as CascaderOption[],
convertConfig as convertConfig
)
} else {
state.optionsData = options
}
state.tree = new Tree(state.optionsData as CascaderOption[], {
value: state.configs.valueKey,
text: state.configs.textKey,
children: state.configs.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()
setOptiosData(state.panes)
}
// 处理有默认值时的数据
const syncValue = async () => {
const currentValue = state.innerValue
if (currentValue === undefined || !state.tree.nodes.length) {
return
}
if (currentValue.length === 0) {
state.tabsCursor = 0
// state.panes = [{ nodes: state.tree.nodes, selectedNode: null }];
return
}
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<CascaderOption | void>, value) => {
const parent = await p
await invokeLazyLoad(parent)
const node: any = parent?.children?.find(
(item: any) => item.value === value
)
if (node) {
needToSync.push(value)
}
return Promise.resolve(node)
}, Promise.resolve(parent))
await invokeLazyLoad(last)
state.initLoading = false
}
}
if (needToSync.length && currentValue === value) {
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.lazyLoad) {
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.lazyLoad?.(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 = () => {
onClose && onClose()
}
const closePopup = () => {
close()
}
/* type: 是否是静默模式,是的话不触发事件
tabsCursor: tab的索引 */
const chooseItem = async (node: CascaderOption, type: boolean) => {
// console.log('chooseItem', node)
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)
}
setOptiosData(state.panes)
close()
return
}
// 如果有子节点,滑到下一个
// if (node.children && node.children.length > 0) {
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}`,
})
setTabvalue(`c${state.tabsCursor + 1}`)
setOptiosData(state.panes)
if (!type) {
const pathNodes = state.panes.map((item) => item.selectedNode)
const optionParams = pathNodes.map((item: any) => item?.value)
onPathChange(optionParams, pathNodes)
}
return
}
state.currentProcessNode = node
if (node.loading) {
return
}
await invokeLazyLoad(node)
if (state.currentProcessNode === node) {
state.panes[state.tabsCursor].selectedNode = node
chooseItem(node, type)
}
setOptiosData(state.panes)
}
return (
<div className={`${classes} ${className}`} style={style}>
<Popup
popClass="cascadar-popup"
visible={visible}
position="bottom"
round
closeable={closeable}
closeIconPosition={closeIconPosition}
closeIcon={closeIcon}
onClickOverlay={closePopup}
onClickCloseIcon={closePopup}
>
<div className={b('title')}>{title}</div>
<Tabs
value={tabvalue}
titleNode={() => {
return optiosData.map((pane, index) => (
<div
onClick={() => {
setTabvalue(pane.paneKey)
state.tabsCursor = index
}}
className={`nut-tabs__titles-item ${
tabvalue === pane.paneKey ? 'active' : ''
}`}
key={pane.paneKey}
>
<span className="nut-tabs__titles-item__text">
{/* {!state.initLoading && state.panes.length
? pane?.selectedNode?.text
? pane.selectedNode.text
: '请选择'
: 'Loading...'} */}
{!state.initLoading &&
state.panes.length &&
pane?.selectedNode?.text}
{!state.initLoading &&
state.panes.length &&
!pane?.selectedNode?.text &&
'请选择'}
{!(!state.initLoading && state.panes.length) && 'Loading...'}
</span>
<span className="nut-tabs__titles-item__line" />
</div>
))
}}
>
{!state.initLoading && state.panes.length ? (
optiosData.map((pane) => (
<TabPane key={pane.paneKey} paneKey={pane.paneKey}>
<div className={classesPane}>
{pane.nodes &&
pane.nodes.map((node: any, index: number) => (
<CascaderItem
key={index}
{...props}
data={node}
checked={pane.selectedNode?.value === node.value}
chooseItem={(node: any) => chooseItem(node, false)}
/>
))}
</div>
</TabPane>
))
) : (
<TabPane>
<div className={classesPane} />
</TabPane>
)}
</Tabs>
</Popup>
</div>
)
}
export const Cascader = React.forwardRef(InternalCascader)
Cascader.defaultProps = defaultProps
Cascader.displayName = 'NutCascader'
import React, {
ForwardRefRenderFunction,
PropsWithChildren,
useEffect,
} from 'react'
import classNames from 'classnames'
import { Icon } from '@/packages/icon/icon'
import bem from '@/utils/bem'
interface OptiosInfo {
text: string
value: string
paneKey: string
disabled?: boolean
loading?: boolean
children?: OptiosInfo[]
}
export interface CascaderItemProps {
data: {
text: string
value: string
paneKey: string
disabled?: boolean
loading?: boolean
children?: OptiosInfo[]
}
checked: boolean
chooseItem: (data: any) => void
}
const defaultProps = {
data: {
text: '',
value: '',
paneKey: '',
disabled: false,
loading: false,
children: [],
},
checked: false,
chooseItem: () => {},
} as CascaderItemProps
const InternalCascaderItem: ForwardRefRenderFunction<
unknown,
PropsWithChildren<Partial<CascaderItemProps>>
> = (props) => {
const { data, checked, chooseItem } = {
...defaultProps,
...props,
}
const b = bem('cascader-item')
const classes = classNames(
{
active: checked,
disabled: data.disabled,
},
b('')
)
const classesTitle = classNames({
[`${b('')}__title`]: true,
})
useEffect(() => {
initData()
}, [data])
const initData = () => {
// console.log('------data', data)
}
return (
<div
className={classes}
onClick={() => {
chooseItem(data)
}}
>
<div className={classesTitle}>{data.text}</div>
{data.loading ? (
<Icon
color="#969799"
className="nut-cascader-item__icon-loading"
name="loading"
/>
) : (
<Icon
className={`${checked ? b('icon-check') : ''}`}
name="checklist"
/>
)}
</div>
)
}
export const CascaderItem = React.forwardRef(InternalCascaderItem)
CascaderItem.defaultProps = defaultProps
CascaderItem.displayName = 'NutCascaderItem'
import React, { useState, useEffect } from 'react'
import { useTranslate } from '../../sites/assets/locale'
import { Cascader } from './cascader'
import { Cell } from '@/packages/cell/cell'
interface T {
basic: string
title1: string
title2: string
title3: string
title4: string
addressTip: string
addressTip1: string
}
const CascaderDemo = () => {
const [translated] = useTranslate<T>({
'zh-CN': {
basic: '基本用法',
title1: '自定义属性名称',
title2: '动态加载',
title3: '部分数据动态加载',
title4: '自动转换',
addressTip: '选择地址',
addressTip1: '请选择地址',
},
'zh-TW': {
basic: '基本用法',
title1: '自定義屬性名稱',
title2: '動態加載',
title3: '部分數據動態加載',
title4: '自動轉換',
addressTip: '選擇地址',
addressTip1: '請選擇地址',
},
'en-US': {
basic: 'Basic Usage',
title1: 'Custom attribute name',
title2: 'Async loading',
title3: 'Async loading of partial data',
title4: 'Automatic data conversion',
addressTip: 'Select address',
addressTip1: 'Please select an address',
},
})
const [isVisibleDemo1, setIsVisibleDemo1] = useState(false)
const [isVisibleDemo2, setIsVisibleDemo2] = useState(false)
const [isVisibleDemo3, setIsVisibleDemo3] = useState(false)
const [isVisibleDemo4, setIsVisibleDemo4] = useState(false)
const [isVisibleDemo5, setIsVisibleDemo5] = useState(false)
const [text, setText] = useState({
desc1: translated.addressTip1,
desc2: '福建福州台江区',
desc3: 'A0A12A23A32',
desc4: translated.addressTip1,
desc5: '广东省广州市',
})
useEffect(() => {
setText({
...text,
desc1: translated.addressTip1,
desc4: translated.addressTip1,
})
}, [translated])
const [value1, setValue1] = useState([])
const [value2, setValue2] = useState(['福建', '福州', '台江区'])
const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32'])
const [value4, setValue4] = useState([])
const [value5, setValue5] = useState(['广东省', '广州市'])
const [optionsDemo1, setOptionsDemo1] = useState([
{
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: '台江区' },
],
},
],
},
])
const [optionsDemo2, setOptionsDemo2] = useState([
{
value1: '浙江',
text1: '浙江',
items: [
{
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: '长沙',
disabled: true,
items: [
{ value1: '西湖区', text1: '西湖区' },
{ value1: '余杭区', text1: '余杭区' },
],
},
{
value1: '温州',
text1: '温州',
items: [
{ value1: '鹿城区', text1: '鹿城区' },
{ value1: '瓯海区', text1: '瓯海区' },
],
},
],
},
{
value1: '福建',
text1: '福建',
items: [
{
value1: '福州',
text1: '福州',
items: [
{ value1: '鼓楼区', text1: '鼓楼区' },
{ value1: '台江区', text1: '台江区' },
],
},
],
},
])
const [optionsDemo4, setOptionsDemo4] = 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 [optionsDemo5, setOptionsDemo5] = useState([
{ value: '北京', text: '北京', id: 1, pid: null },
{ value: '朝阳区', text: '朝阳区', id: 11, pid: 1 },
{ value: '亦庄', text: '亦庄', id: 111, pid: 11 },
{ value: '广东省', text: '广东省', id: 2, pid: null },
{ value: '广州市', text: '广州市', id: 21, pid: 2 },
])
const [convertConfigDemo5, setConvertConfigDemo5] = useState({
topId: null,
idKey: 'id',
pidKey: 'pid',
sortKey: '',
})
const change1 = (value: any, path: any) => {
console.log('onChange', value, path)
setText({
...text,
desc1: value,
})
setValue1(value)
}
const change2 = (value: any, path: any) => {
console.log('onChange', value, path)
setText({
...text,
desc2: value,
})
setValue2(value)
}
const change3 = (value: any, path: any) => {
console.log('onChange', value, path)
setText({
...text,
desc3: value,
})
setValue3(value)
}
const change4 = (value: any, path: any) => {
console.log('onChange', value, path)
setText({
...text,
desc4: value,
})
setValue4(value)
}
const change5 = (value: any, path: any) => {
console.log('onChange', value, path)
setText({
...text,
desc5: value,
})
setValue5(value)
}
const onPathChange = (value: any, path: any) => {
console.log('onPathChange', value, path)
}
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 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 },
])
}
}, 2000)
}
const lazyLoadDemo4 = (node: any, resolve: (children: any) => void) => {
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 },
])
}, 500)
}
return (
<>
<div className="demo">
<h2>{translated.basic}</h2>
<Cell
title={translated.addressTip}
desc={text.desc1}
onClick={() => {
setIsVisibleDemo1(true)
}}
/>
<Cascader
visible={isVisibleDemo1}
value={value1}
title={translated.addressTip}
options={optionsDemo1}
closeable
onClose={() => {
setIsVisibleDemo1(false)
}}
onChange={change1}
onPathChange={onPathChange}
/>
<h2>{translated.title1}</h2>
<Cell
title={translated.addressTip}
desc={text.desc2}
onClick={() => {
setIsVisibleDemo2(true)
}}
/>
<Cascader
visible={isVisibleDemo2}
value={value2}
title={translated.addressTip}
options={optionsDemo2}
textKey="text1"
valueKey="value1"
childrenKey="items"
closeable
onClose={() => {
setIsVisibleDemo2(false)
}}
onChange={change2}
onPathChange={onPathChange}
/>
<h2>{translated.title2}</h2>
<Cell
title={translated.addressTip}
desc={text.desc3}
onClick={() => {
setIsVisibleDemo3(true)
}}
/>
<Cascader
visible={isVisibleDemo3}
value={value3}
title={translated.addressTip}
closeable
onClose={() => {
setIsVisibleDemo3(false)
}}
onChange={change3}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo3}
/>
<h2>{translated.title3}</h2>
<Cell
title={translated.addressTip}
desc={text.desc4}
onClick={() => {
setIsVisibleDemo4(true)
}}
/>
<Cascader
visible={isVisibleDemo4}
value={value4}
title={translated.addressTip}
options={optionsDemo4}
closeable
onClose={() => {
setIsVisibleDemo4(false)
}}
onChange={change4}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo4}
/>
<h2>{translated.title4}</h2>
<Cell
title={translated.addressTip}
desc={text.desc5}
onClick={() => {
setIsVisibleDemo5(true)
}}
/>
<Cascader
visible={isVisibleDemo5}
value={value5}
title={translated.addressTip}
options={optionsDemo5}
convertConfig={convertConfigDemo5}
closeable
onClose={() => {
setIsVisibleDemo5(false)
}}
onChange={change5}
onPathChange={onPathChange}
/>
</div>
</>
)
}
export default CascaderDemo
# Cascader
### Introduce
The cascader component is used for the selection of multi-level data. The typical scene is the selection of provinces and cities.
### Install
```js
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
```
## Demo
### Basic Usage
Pass in the `options` list.
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo1, setIsVisibleDemo1] = useState(false)
const [value1, setValue1] = useState([])
const [optionsDemo1, setOptionsDemo1] = useState([
{
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: '台江区' },
],
},
],
},
])
const change1 = (value: any, path: any) => {
console.log('onChange', value, path)
setValue1(value)
}
const onPathChange = (value: any, path: any) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value1 ? value1 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo1(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo1}
value={value1}
title="地址选择"
options={optionsDemo1}
closeable
onClose={()=>{setIsVisibleDemo1(false)}}
onChange={change1}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
### Custom attribute name
use `textKey``valueKey``childrenKey`Specify the property name.
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo2, setIsVisibleDemo2] = useState(false)
const [value2, setValue2] = useState(['福建', '福州', '台江区'])
const [optionsDemo2, setOptionsDemo2] = useState([
{
value1: '浙江',
text1: '浙江',
items: [
{
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: '长沙',
disabled: true,
items: [
{ value1: '西湖区', text1: '西湖区' },
{ value1: '余杭区', text1: '余杭区' },
],
},
{
value1: '温州',
text1: '温州',
items: [
{ value1: '鹿城区', text1: '鹿城区' },
{ value1: '瓯海区', text1: '瓯海区' },
],
},
],
},
{
value1: '福建',
text1: '福建',
items: [
{
value1: '福州',
text1: '福州',
items: [
{ value1: '鼓楼区', text1: '鼓楼区' },
{ value1: '台江区', text1: '台江区' },
],
},
],
},
])
const change2 = (value, path) => {
console.log('onChange', value, path)
setValue2(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value2 ? value2 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo2(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo2}
value={value2}
title="地址选择"
options={optionsDemo2}
textKey="text1"
valueKey="value1"
childrenKey="items"
closeable
onClose={()=>{setIsVisibleDemo2(false)}}
onChange={change2}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
### 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.
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo3, setIsVisibleDemo3] = useState(false)
const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32'])
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 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 }
]);
}
}, 2000);
}
const change3 = (value, path) => {
console.log('onChange', value, path)
setValue3(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value3 ? value3 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo3(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo3}
value={value3}
title="地址选择"
closeable
onClose={()=>{setIsVisibleDemo3(false)}}
onChange={change3}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo3}
/>
</>
);
};
export default App;
```
:::
### Async loading of partial data
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo4, setIsVisibleDemo4] = useState(false)
const [value4, setValue4] = useState([])
const [optionsDemo4, setOptionsDemo4] = 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) => {
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 }
]);
}, 500);
}
const change4 = (value, path) => {
console.log('onChange', value, path)
setValue4(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value4 ? value4 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo4(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo4}
value={value4}
title="地址选择"
options={optionsDemo4}
closeable
onClose={()=>{setIsVisibleDemo4(false)}}
onChange={change4}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo4}
/>
</>
);
};
export default App;
```
:::
### Automatic data conversion
If your data is a flat structure that can be converted into a tree structure, you can tell the component that automatic conversion is required through `convertConfig`, ` convertConfig` accepts four parameters, `topid` is the parent ID of the top-level node, `idkey` is the unique ID of the node, `pidkey` is the attribute name pointing to the parent node ID, and if there is a `sortkey`, `Array.prototype.sort()` to sort at the same level.
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo5, setIsVisibleDemo5] = useState(false)
const [value5, setValue5] = useState(['广东省', '广州市'])
const [optionsDemo5, setOptionsDemo5] = useState([
{ value: '北京', text: '北京', id: 1, pid: null },
{ value: '朝阳区', text: '朝阳区', id: 11, pid: 1 },
{ value: '亦庄', text: '亦庄', id: 111, pid: 11 },
{ value: '广东省', text: '广东省', id: 2, pid: null },
{ value: '广州市', text: '广州市', id: 21, pid: 2 }
])
const [convertConfigDemo5, setConvertConfigDemo5] = useState({
topId: null,
idKey: 'id',
pidKey: 'pid',
sortKey: ''
})
const change5 = (value, path) => {
console.log('onChange', value, path)
setValue5(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value5 ? value5 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo5(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo5}
value={value5}
title="地址选择"
options={optionsDemo5}
convertConfig={convertConfigDemo5}
closeable
onClose={()=>{setIsVisibleDemo5(false)}}
onChange={change5}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
## API
### Props
| Props | Description | Type | Default |
| ------------- | --------------------------------------------- | -------- | ------ |
| value | Selected value | Array | - |
| options | Cascade data | Array | - |
| visible | Cascading show hidden states | Boolean | false |
| lazy | Whether to enable dynamic loading | Boolean | false |
| lazyLoad | Dynamic loading callback, which takes effect when dynamic loading is enabled | Function | - |
| valueKey | Customize the field of `value` in the `options` structure | String | - |
| textKey | Customize the fields of `text` in the `options` structure | String | - |
| childrenKey | Customize the fields of `children` in the `options` structure | String | - |
| convertConfig | When options is a flat structure that can be converted into a tree structure, configure the conversion rules | Object | - |
| title | Title | String | '' |
| closeIconPosition | Cancel the button position and inherit the popup component | String | "top-right" |
| close-icon | Customize the close button and inherit the popup component | String | "close" |
| closeable | Whether to display the close button and inherit the popup component | Boolean | true |
### Events
| Event | Description | Callback parameters |
| ---------- | ---------------- | ------------------ |
| onChange | Triggered when the selected value changes | (value, pathNodes) |
| onPathChange | Triggered when the selected item changes | (pathNodes) |
# Cascader 级联选择
### 介绍
级联选择器,用于多层级数据的选择,典型场景为省市区选择。
### 安装
```js
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
```
## 代码演示
### 基础用法
传入`options`列表。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo1, setIsVisibleDemo1] = useState(false)
const [value1, setValue1] = useState([])
const [optionsDemo1, setOptionsDemo1] = useState([
{
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: '台江区' },
],
},
],
},
])
const change1 = (value: any, path: any) => {
console.log('onChange', value, path)
setValue1(value)
}
const onPathChange = (value: any, path: any) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value1 ? value1 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo1(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo1}
value={value1}
title="地址选择"
options={optionsDemo1}
closeable
onClose={()=>{setIsVisibleDemo1(false)}}
onChange={change1}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
### 自定义属性名称
可通过`textKey``valueKey``childrenKey`指定属性名。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo2, setIsVisibleDemo2] = useState(false)
const [value2, setValue2] = useState(['福建', '福州', '台江区'])
const [optionsDemo2, setOptionsDemo2] = useState([
{
value1: '浙江',
text1: '浙江',
items: [
{
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: '长沙',
disabled: true,
items: [
{ value1: '西湖区', text1: '西湖区' },
{ value1: '余杭区', text1: '余杭区' },
],
},
{
value1: '温州',
text1: '温州',
items: [
{ value1: '鹿城区', text1: '鹿城区' },
{ value1: '瓯海区', text1: '瓯海区' },
],
},
],
},
{
value1: '福建',
text1: '福建',
items: [
{
value1: '福州',
text1: '福州',
items: [
{ value1: '鼓楼区', text1: '鼓楼区' },
{ value1: '台江区', text1: '台江区' },
],
},
],
},
])
const change2 = (value, path) => {
console.log('onChange', value, path)
setValue2(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value2 ? value2 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo2(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo2}
value={value2}
title="地址选择"
options={optionsDemo2}
textKey="text1"
valueKey="value1"
childrenKey="items"
closeable
onClose={()=>{setIsVisibleDemo2(false)}}
onChange={change2}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
### 动态加载
使用`lazy`标识是否需要动态获取数据,此时不传`options`代表所有数据都需要通过`lazyLoad`加载,首次加载通过`root`属性区分,当遇到非叶子节点时会调用`lazyLoad`方法,参数为当前节点和`resolve`方法,注意`resolve`方法必须调用,不传子节点时会被当做叶子节点处理。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo3, setIsVisibleDemo3] = useState(false)
const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32'])
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 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 }
]);
}
}, 2000);
}
const change3 = (value, path) => {
console.log('onChange', value, path)
setValue3(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value3 ? value3 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo3(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo3}
value={value3}
title="地址选择"
closeable
onClose={()=>{setIsVisibleDemo3(false)}}
onChange={change3}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo3}
/>
</>
);
};
export default App;
```
:::
### 部分数据动态加载
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo4, setIsVisibleDemo4] = useState(false)
const [value4, setValue4] = useState([])
const [optionsDemo4, setOptionsDemo4] = 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) => {
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 }
]);
}, 500);
}
const change4 = (value, path) => {
console.log('onChange', value, path)
setValue4(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value4 ? value4 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo4(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo4}
value={value4}
title="地址选择"
options={optionsDemo4}
closeable
onClose={()=>{setIsVisibleDemo4(false)}}
onChange={change4}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo4}
/>
</>
);
};
export default App;
```
:::
### 自动转换
如果你的数据为可转换为树形结构的扁平结构时,可以通过`convertConfig`告诉组件需要进行自动转换,`convertConfig`接受4个参数,`topId`为顶层节点的父级id,`idKey`为节点唯一id,`pidKey`为指向父节点id的属性名,存在`sortKey`将根据指定字段调用Array.prototype.sort()进行同层排序。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo5, setIsVisibleDemo5] = useState(false)
const [value5, setValue5] = useState(['广东省', '广州市'])
const [optionsDemo5, setOptionsDemo5] = useState([
{ value: '北京', text: '北京', id: 1, pid: null },
{ value: '朝阳区', text: '朝阳区', id: 11, pid: 1 },
{ value: '亦庄', text: '亦庄', id: 111, pid: 11 },
{ value: '广东省', text: '广东省', id: 2, pid: null },
{ value: '广州市', text: '广州市', id: 21, pid: 2 }
])
const [convertConfigDemo5, setConvertConfigDemo5] = useState({
topId: null,
idKey: 'id',
pidKey: 'pid',
sortKey: ''
})
const change5 = (value, path) => {
console.log('onChange', value, path)
setValue5(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value5 ? value5 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo5(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo5}
value={value5}
title="地址选择"
options={optionsDemo5}
convertConfig={convertConfigDemo5}
closeable
onClose={()=>{setIsVisibleDemo5(false)}}
onChange={change5}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| ------------- | ---------------------------------------------- | -------- | ------ |
| value | 选中值 | Array | - |
| options | 级联数据 | Array | - |
| visible | 级联显示隐藏状态 | Boolean | false |
| lazy | 是否开启动态加载 | Boolean | false |
| lazyLoad | 动态加载回调,开启动态加载时生效 | Function | - |
| valueKey | 自定义`options`结构中`value`的字段 | String | - |
| textKey | 自定义`options`结构中`text`的字段 | String | - |
| childrenKey | 自定义`options`结构中`children`的字段 | String | - |
| convertConfig | 当options为可转换为树形结构的扁平结构时,配置转换规则 | Object | - |
| title | 标题 | String | '' |
| closeIconPosition | 取消按钮位置,继承 Popup 组件 | String | "top-right" |
| close-icon | 自定义关闭按钮,继承 Popup 组件 | String | "close" |
| closeable | 是否显示关闭按钮,继承 Popup 组件 | Boolean | true |
### Events
| 事件名 | 说明 | 回调参数 |
| ---------- | ---------------- | ------------------ |
| onChange | 选中值改变时触发 | (value, pathNodes) |
| onPathChange | 选中项改变时触发 | (pathNodes) |
# Cascader 級聯選擇
### 介紹
級聯選擇器,用於多層級數據的選擇,典型場景為省市區選擇。
### 安裝
```js
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
```
## 代碼演示
### 基礎用法
傳入`options`列表。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo1, setIsVisibleDemo1] = useState(false)
const [value1, setValue1] = useState([])
const [optionsDemo1, setOptionsDemo1] = useState([
{
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: '台江区' },
],
},
],
},
])
const change1 = (value: any, path: any) => {
console.log('onChange', value, path)
setValue1(value)
}
const onPathChange = (value: any, path: any) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value1 ? value1 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo1(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo1}
value={value1}
title="地址选择"
options={optionsDemo1}
closeable
onClose={()=>{setIsVisibleDemo1(false)}}
onChange={change1}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
### 自定義屬性名稱
可通過`textKey``valueKey``childrenKey`指定屬性名。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo2, setIsVisibleDemo2] = useState(false)
const [value2, setValue2] = useState(['福建', '福州', '台江区'])
const [optionsDemo2, setOptionsDemo2] = useState([
{
value1: '浙江',
text1: '浙江',
items: [
{
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: '长沙',
disabled: true,
items: [
{ value1: '西湖区', text1: '西湖区' },
{ value1: '余杭区', text1: '余杭区' },
],
},
{
value1: '温州',
text1: '温州',
items: [
{ value1: '鹿城区', text1: '鹿城区' },
{ value1: '瓯海区', text1: '瓯海区' },
],
},
],
},
{
value1: '福建',
text1: '福建',
items: [
{
value1: '福州',
text1: '福州',
items: [
{ value1: '鼓楼区', text1: '鼓楼区' },
{ value1: '台江区', text1: '台江区' },
],
},
],
},
])
const change2 = (value, path) => {
console.log('onChange', value, path)
setValue2(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value2 ? value2 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo2(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo2}
value={value2}
title="地址选择"
options={optionsDemo2}
textKey="text1"
valueKey="value1"
childrenKey="items"
closeable
onClose={()=>{setIsVisibleDemo2(false)}}
onChange={change2}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
### 動態加載
使用`lazy`標識是否需要動態獲取數據,此時不傳`options`代表所有數據都需要通過`lazyLoad`加載,首次加載通過`root`屬性區分,當遇到非葉子節點時會調用`lazyLoad`方法,參數為當前節點和`resolve`方法,註意`resolve`方法必須調用,不傳子節點時會被當做葉子節點處理。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo3, setIsVisibleDemo3] = useState(false)
const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32'])
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 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 }
]);
}
}, 2000);
}
const change3 = (value, path) => {
console.log('onChange', value, path)
setValue3(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value3 ? value3 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo3(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo3}
value={value3}
title="地址选择"
closeable
onClose={()=>{setIsVisibleDemo3(false)}}
onChange={change3}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo3}
/>
</>
);
};
export default App;
```
:::
### 部分數據動態加載
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo4, setIsVisibleDemo4] = useState(false)
const [value4, setValue4] = useState([])
const [optionsDemo4, setOptionsDemo4] = 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) => {
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 }
]);
}, 500);
}
const change4 = (value, path) => {
console.log('onChange', value, path)
setValue4(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value4 ? value4 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo4(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo4}
value={value4}
title="地址选择"
options={optionsDemo4}
closeable
onClose={()=>{setIsVisibleDemo4(false)}}
onChange={change4}
onPathChange={onPathChange}
lazy
lazyLoad={lazyLoadDemo4}
/>
</>
);
};
export default App;
```
:::
### 自動轉換
如果你的數據為可轉換為樹形結構的扁平結構時,可以通過`convertConfig`告訴組件需要進行自動轉換,`convertConfig`接受4個參數,`topId`為頂層節點的父級id,`idKey`為節點唯一id,`pidKey`為指向父節點id的屬性名,存在`sortKey`將根據指定字段調用Array.prototype.sort()進行同層排序。
:::demo
```jsx
import React from "react";
import { Cascader, Tabs, TabPane } from '@nutui/nutui-react';
const App = () => {
const [isVisibleDemo5, setIsVisibleDemo5] = useState(false)
const [value5, setValue5] = useState(['广东省', '广州市'])
const [optionsDemo5, setOptionsDemo5] = useState([
{ value: '北京', text: '北京', id: 1, pid: null },
{ value: '朝阳区', text: '朝阳区', id: 11, pid: 1 },
{ value: '亦庄', text: '亦庄', id: 111, pid: 11 },
{ value: '广东省', text: '广东省', id: 2, pid: null },
{ value: '广州市', text: '广州市', id: 21, pid: 2 }
])
const [convertConfigDemo5, setConvertConfigDemo5] = useState({
topId: null,
idKey: 'id',
pidKey: 'pid',
sortKey: ''
})
const change5 = (value, path) => {
console.log('onChange', value, path)
setValue5(value)
}
const onPathChange = (value, path) => {
console.log('onPathChange', value, path)
}
return (
<>
<Cell
title="选择地址"
desc={value5 ? value5 : '请选择地址'}
onClick={()=>{
setIsVisibleDemo5(true)
}}
>
</Cell>
<Cascader
visible={isVisibleDemo5}
value={value5}
title="地址选择"
options={optionsDemo5}
convertConfig={convertConfigDemo5}
closeable
onClose={()=>{setIsVisibleDemo5(false)}}
onChange={change5}
onPathChange={onPathChange}
/>
</>
);
};
export default App;
```
:::
## API
### Props
| 屬性 | 說明 | 類型 | 默認值 |
| ------------- | ---------------------------------------------- | -------- | ------ |
| value | 選中值 | Array | - |
| options | 級聯數據 | Array | - |
| visible | 級聯顯示隱藏狀態 | Boolean | false |
| lazy | 是否開啟動態加載 | Boolean | false |
| lazyLoad | 動態加載回調,開啟動態加載時生效 | Function | - |
| valueKey | 自定義`options`結構中`value`的字段 | String | - |
| textKey | 自定義`options`結構中`text`的字段 | String | - |
| childrenKey | 自定義`options`結構中`children`的字段 | String | - |
| convertConfig | 當options為可轉換為樹形結構的扁平結構時,配置轉換規則 | Object | - |
| title | 標題 | String | '' |
| closeIconPosition | 取消按鈕位置,繼承 Popup 組件 | String | "top-right" |
| close-icon | 自定義關閉按鈕,繼承 Popup 組件 | String | "close" |
| closeable | 是否顯示關閉按鈕,繼承 Popup 組件 | Boolean | true |
### Events
| 事件名 | 說明 | 回調參數 |
| ---------- | --------------- | ------------------ |
| onChange | 選中值改變時觸發 | (value, pathNodes) |
| onPathChange | 選中項改變時觸發 | (pathNodes) |
import { CascaderOption, CascaderConfig, convertConfig } 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: convertConfig
): 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] || [])
// const children = 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
}
import { Cascader } from './cascader'
export default Cascader
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
}
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
export interface CascaderPane {
nodes: []
selectedNode: CascaderOption | null
paneKey: string
}
export interface CascaderOption {
text?: string
value?: number | string
paneKey?: string
disabled?: boolean
children?: CascaderOption[]
leaf?: boolean
level?: number
loading?: boolean
root?: boolean
}
export interface CascaderConfig {
value?: string
text?: string
children?: string
}
export type CascaderValue = CascaderOption['value'][]
export interface convertConfig {
topId?: string | number | null
idKey?: string
pidKey?: string
sortKey?: string
}
......@@ -647,3 +647,18 @@ $countup-lr-margin: 1px !default;
$countup-bgcolor: #031f63 !default;
$countup-color: #ffffff !default;
$countup-width: 24px !default;
// cascader
$cascader-font-size: 14px !default;
$cascader-line-height: 22px !default;
$cascader-tabs-item-padding: 0 10px !default;
$cascader-title-padding: 24px 20px 17px !default;
$cascader-title-font-size: 18px !default;
$cascader-title-line-height: 20px !default;
$cascader-item-padding: 10px 20px !default;
$cascader-item-font-size: 14px !default;
$cascader-item-color: #1a1a1a !default;
$cascader-item-active-color: #fa2c19 !default;
$cascader-pane-height: 342px !default;
$cascader-pane-paddingTop: 10px !default;
$cascader-icon-checklist-marginLeft: 10px !default;
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册