Skip to content

Commit d6757ee

Browse files
authoredMar 25, 2022
feat(Cascader): 添加表单支持 & 增加清除按钮 (#713)
1 parent 910904a commit d6757ee

File tree

6 files changed

+238
-129
lines changed

6 files changed

+238
-129
lines changed
 

‎packages/react-cascader/README.md

+144-36
Original file line numberDiff line numberDiff line change
@@ -23,49 +23,157 @@ import { Cascader } from 'uiw';
2323
const Demo = () => {
2424

2525
const options = [
26-
{
27-
value: 'zhejiang',
28-
label: 'Zhejiang',
29-
children: [
30-
{
31-
value: 'hangzhou',
32-
label: 'Hangzhou',
33-
children: [
34-
{
35-
value: 'xihu',
36-
label: 'West Lake',
37-
},
38-
],
39-
},
40-
],
41-
},
42-
{
43-
value: 'jiangsu',
44-
label: 'Jiangsu',
45-
children: [
46-
{
47-
value: 'nanjing',
48-
label: 'Nanjing',
49-
children: [
50-
{
51-
value: 'zhonghuamen',
52-
label: 'Zhong Hua Men',
53-
},
54-
],
55-
},
56-
],
57-
},
58-
];
26+
{
27+
label: '上海市',
28+
value: 1,
29+
children: [
30+
{
31+
label: '徐汇区',
32+
value: 4,
33+
children: [
34+
{ label: '半淞园路街道', value: 6 },
35+
{ label: '南京东路街道', value: 7 },
36+
{ label: '外滩街道', value: 8 },
37+
]
38+
},
39+
]
40+
},
41+
{
42+
label: '北京市',
43+
value: 9,
44+
children: [
45+
{
46+
label: '崇文区',
47+
value: 12,
48+
children: [
49+
{ label: '东花市街道', value: 13 },
50+
{ label: '体育馆路街道', value: 14 },
51+
{ label: '前门街道', value: 15 },
52+
]
53+
},
54+
]
55+
},
56+
];
5957

60-
return(
58+
return (
6159
<div style={{ width: 200 }}>
62-
<Cascader placeholder="请选择" option={options} onChange={(value,seleteds)=>console.log(value,seleteds)}/>
60+
<Cascader allowClear={true} placeholder="请选择" value={[1, 4, 7]} option={options} onChange={(value, seleteds) => console.log(value, seleteds)} />
6361
</div>
6462
)
6563
};
6664
ReactDOM.render(<Demo />, _mount_);
6765
```
6866

67+
### 在表单中使用
68+
69+
[`<Form />`](#/components/form) 表单中应用 `<Cascader />` 组件。
70+
71+
<!--rehype:bgWhite=true&codeSandbox=true&codePen=true&noScroll=true-->
72+
```jsx
73+
import ReactDOM from 'react-dom';
74+
import { Form, Row, Col, Cascader, Button } from 'uiw';
75+
76+
const Demo = () => {
77+
const options = [
78+
{
79+
label: '上海市',
80+
value: 1,
81+
children: [
82+
{
83+
label: '徐汇区',
84+
value: 4,
85+
children: [
86+
{ label: '半淞园路街道', value: 6 },
87+
{ label: '南京东路街道', value: 7 },
88+
{ label: '外滩街道', value: 8 },
89+
]
90+
},
91+
]
92+
},
93+
{
94+
label: '北京市',
95+
value: 9,
96+
children: [
97+
{
98+
label: '崇文区',
99+
value: 12,
100+
children: [
101+
{ label: '东花市街道', value: 13 },
102+
{ label: '体育馆路街道', value: 14 },
103+
{ label: '前门街道', value: 15 },
104+
]
105+
},
106+
]
107+
},
108+
];
109+
110+
return (
111+
<div>
112+
<Form
113+
onSubmitError={(error) => {
114+
if (error.filed) {
115+
return { ...error.filed };
116+
}
117+
return null;
118+
}}
119+
onSubmit={({initial, current}) => {
120+
const errorObj = {};
121+
if (!current.selectField) {
122+
errorObj.selectField = '默认需要选择内容,选择入内容';
123+
}
124+
if(Object.keys(errorObj).length > 0) {
125+
const err = new Error();
126+
err.filed = errorObj;
127+
Notify.error({ title: '提交失败!', description: '请确认提交表单是否正确!' });
128+
throw err;
129+
}
130+
Notify.success({
131+
title: '提交成功!',
132+
description: `表单提交成功,选择值为:${current.selectField},将自动填充初始化值!`,
133+
});
134+
}}
135+
fields={{
136+
cascader: {
137+
initialValue:[1, 4, 7],
138+
children: (
139+
<Cascader
140+
allowClear={true}
141+
placeholder="请选择"
142+
option={options}
143+
onChange={(value, seleteds) => console.log(value, seleteds)}
144+
/>
145+
)
146+
},
147+
}}
148+
>
149+
{({ fields, state, canSubmit }) => {
150+
return (
151+
<div>
152+
<Row >
153+
<Col fixed style={{width:200}}>{fields.cascader}</Col>
154+
</Row>
155+
<Row>
156+
<Col fixed>
157+
<Button disabled={!canSubmit()} type="primary" htmlType="submit">提交</Button>
158+
</Col>
159+
</Row>
160+
<Row>
161+
<Col>
162+
<pre style={{ padding: 10, marginTop: 10 }}>
163+
{JSON.stringify(state.current, null, 2)}
164+
</pre>
165+
</Col>
166+
</Row>
167+
</div>
168+
)
169+
}}
170+
</Form>
171+
</div>
172+
);
173+
}
174+
ReactDOM.render(<Demo />, _mount_);
175+
```
176+
69177
## Props
70178

71179
| 参数 | 说明 | 类型 | 默认值 | 版本 |
@@ -74,4 +182,4 @@ ReactDOM.render(<Demo />, _mount_);
74182
| placeholder | 选择框默认文字 | String | - | - |
75183
| option | 选项菜单 | { value: String \| Number, label: React.ReactNode, children: Array<String \| Number>} | - | - |
76184
| value | 指定当前选中的条目,多选时为一个数组 | String[] \| Number[] | - | - |
77-
| onChange | 选中选项调用此函数 | function(value, selectedOptions) | - | - |
185+
| onChange | 选中选项调用此函数 | function( isSeleted, value, selectedOptions) | - | - |

‎packages/react-cascader/src/index.tsx

+72-19
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,85 @@ import { IProps } from '@uiw/utils';
44
import Dropdown, { DropdownProps } from '@uiw/react-dropdown';
55
import Menu from '@uiw/react-menu';
66
import Icon from '@uiw/react-icon';
7+
import './style/index.less';
78

89
type ValueType = Array<string | number>;
9-
type optionType = { value: string | number; label: React.ReactNode; children?: Array<optionType> };
10+
type OptionType = { value: string | number; label: React.ReactNode; children?: Array<OptionType> };
1011

1112
export interface CascaderProps extends IProps, DropdownProps {
12-
option?: Array<optionType>;
13+
option?: Array<OptionType>;
1314
value?: ValueType;
14-
onChange?: (value: ValueType, selectedOptions: Array<optionType>) => void;
15+
onChange?: (isSeleted: boolean, value: ValueType, selectedOptions: Array<OptionType>) => void;
1516
allowClear?: boolean;
1617
placeholder?: string;
1718
isOpen?: boolean;
1819
}
1920

2021
function Cascader(props: CascaderProps) {
21-
const { placeholder, prefixCls = 'w-search-select', className, style = { width: 200 }, option = [], others } = props;
22+
const {
23+
value,
24+
onChange,
25+
26+
allowClear,
27+
placeholder,
28+
prefixCls = 'w-cascader',
29+
className,
30+
style = { width: 200 },
31+
option = [],
32+
others,
33+
} = props;
2234

2335
const cls = [prefixCls, className].filter(Boolean).join(' ').trim();
2436
const [innerIsOpen, setInnerIsOpen] = useState(false);
25-
const [selectedValue, setSelectedValue] = useState<Array<optionType>>([]);
37+
const [selectedValue, setSelectedValue] = useState<Array<OptionType>>([]);
38+
const [selectIconType, setSelectIconType] = useState('');
39+
40+
useEffect(() => {
41+
const valueTemp: Array<OptionType> = [];
42+
let optChildren = option;
43+
value?.map((item) => {
44+
const findOpt = optChildren.find((opt) => opt.value === item);
45+
optChildren = findOpt?.children || [];
46+
valueTemp.push({ label: item, value: item, ...findOpt });
47+
});
48+
setSelectedValue(valueTemp);
49+
}, [value]);
2650

2751
function onVisibleChange(isOpen: boolean) {
2852
setInnerIsOpen(isOpen);
2953
}
3054

31-
const handleItemClick = (optionsItem: optionType, level: number) => {
55+
function renderSelectIcon(type: string) {
56+
let selectIconType;
57+
if (type === 'enter' && allowClear && selectedValue.length > 0) {
58+
selectIconType = 'close';
59+
} else {
60+
selectIconType = '';
61+
}
62+
setSelectIconType(selectIconType);
63+
}
64+
65+
const handleItemClick = (optionsItem: OptionType, level: number) => {
3266
selectedValue.splice(level, selectedValue.length - level, optionsItem);
33-
setSelectedValue([...selectedValue]);
3467

35-
handelChange();
68+
handelChange(true, selectedValue);
3669
};
3770

38-
const handelChange = () => {
71+
const handelChange = (isSeleted: boolean, selectedValue: Array<OptionType>) => {
72+
setSelectedValue([...selectedValue]);
3973
const value = selectedValue.map((item) => item.value);
40-
props.onChange?.(value, selectedValue);
74+
onChange?.(isSeleted, value, selectedValue);
75+
};
76+
77+
const onClear = (e: React.MouseEvent<any, MouseEvent>) => {
78+
e.stopPropagation();
79+
console.log(123);
80+
handelChange(false, []);
4181
};
4282

43-
const widths = (style?.width as number) * 0.6 || undefined;
83+
const widths = (style?.width as number) * 0.5 || undefined;
4484

45-
const OptionIter = (option: Array<optionType>, level: number = 0) => {
85+
const OptionIter = (option: Array<OptionType>, level: number = 0) => {
4686
if (!option) return;
4787

4888
return (
@@ -59,15 +99,15 @@ function Cascader(props: CascaderProps) {
5999
{!option || option.length === 0 ? (
60100
<div style={{ color: '#c7c7c7', fontSize: 12 }}>{'没有数据'}</div>
61101
) : (
62-
option.map((item, index) => {
63-
const active = selectedValue[level]?.value === item.value;
102+
option.map((opt, index) => {
103+
const active = selectedValue[level]?.value === opt.value;
64104
return (
65105
<Menu.Item
66106
active={active}
67107
key={index}
68-
text={item.label}
69-
addonAfter={item.children ? <Icon type="right" /> : undefined}
70-
onClick={() => handleItemClick(item, level)}
108+
text={opt.label}
109+
addonAfter={opt.children ? <Icon type="right" /> : undefined}
110+
onClick={() => handleItemClick(opt, level)}
71111
/>
72112
);
73113
})
@@ -77,7 +117,7 @@ function Cascader(props: CascaderProps) {
77117
};
78118

79119
const inputValue = useMemo(() => {
80-
return selectedValue.map((item) => item.label).join(' / ');
120+
return selectedValue.map((opt) => opt.label).join(' / ');
81121
}, [selectedValue.length]);
82122

83123
return (
@@ -101,7 +141,20 @@ function Cascader(props: CascaderProps) {
101141
</div>
102142
}
103143
>
104-
<Input value={inputValue} onChange={() => {}} placeholder={placeholder} style={{ width: style?.width }} />
144+
<span onMouseLeave={() => renderSelectIcon('leave')} onMouseOver={() => renderSelectIcon('enter')}>
145+
<Input
146+
value={inputValue}
147+
onChange={() => {}}
148+
placeholder={placeholder}
149+
style={{ width: style?.width }}
150+
readOnly
151+
addonAfter={
152+
selectIconType === 'close' && (
153+
<Icon type={`${selectIconType}`} onClick={onClear} className={`${prefixCls}-close`} />
154+
)
155+
}
156+
/>
157+
</span>
105158
</Dropdown>
106159
);
107160
}
+5-71
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,9 @@
1-
@w-search-select: ~'w-search-select';
1+
@w-cascader: ~'w-cascader';
22

3-
.@{w-search-select} {
4-
&-input-contents {
5-
input {
6-
box-shadow: none;
7-
padding: 0px;
8-
min-width: 50px;
9-
height: 22px;
10-
}
11-
12-
.w-input-inner {
13-
&:hover {
14-
box-shadow: none !important;
15-
}
16-
17-
&:focus {
18-
box-shadow: none !important;
19-
}
20-
21-
&.disabled {
22-
box-shadow: none;
23-
background: #dddddd;
24-
opacity: 0.75;
25-
color: #a5a5a5;
26-
cursor: not-allowed;
27-
resize: none;
28-
}
29-
}
30-
}
31-
32-
&-inner {
33-
display: flex;
34-
justify-content: space-between;
35-
outline: none;
36-
border: none;
37-
align-items: center;
38-
border-radius: 3px;
39-
box-shadow: 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15),
40-
inset 0 1px 1px rgba(16, 22, 26, 0.2);
41-
box-sizing: border-box;
42-
background: #fff;
43-
min-height: 30px;
44-
margin: 0 !important;
45-
padding: 3px 10px 3px 10px;
46-
vertical-align: middle;
47-
line-height: 30px;
3+
.@{w-cascader} {
4+
&-close {
5+
font-size: 15px;
6+
margin-right: 1px;
487
color: #393e48;
49-
font-weight: 400;
50-
font-size: inherit;
51-
transition: box-shadow 0.3s cubic-bezier(0.4, 1, 0.75, 0.9);
52-
appearance: none;
53-
54-
&:focus {
55-
box-shadow: 0 0 0 1px #393e48, 0 0 0 3px rgba(57, 62, 72, 0.17), inset 0 1px 1px rgba(16, 22, 26, 0.2);
56-
}
57-
58-
&:hover {
59-
box-shadow: 0 0 0 1px #6e6e6e, 0 0 0 3px rgba(57, 62, 72, 0), inset 0 1px 1px rgba(16, 22, 26, 0.2);
60-
}
61-
62-
&:focus&:hover {
63-
box-shadow: 0 0 0 1px #6e6e6e, 0 0 0 3px rgba(57, 62, 72, 0.17), inset 0 1px 1px rgba(16, 22, 26, 0.2);
64-
}
65-
66-
&:disabled {
67-
box-shadow: none;
68-
background: #dddddd;
69-
opacity: 0.75;
70-
color: #a5a5a5;
71-
cursor: not-allowed;
72-
resize: none;
73-
}
748
}
759
}

‎packages/react-search-select/src/index.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,12 @@ export default function SearchSelect(props: SearchSelectProps) {
298298
/>
299299
</div>
300300
{!disabled && (selectIconType === 'close' || (selectIconType === 'loading' && loading)) && (
301-
<Icon type={selectIconType} spin={loading && selectIconType === 'loading'} onClick={resetSelectedValue} />
301+
<Icon
302+
className={`${prefixCls}-multiple-colse`}
303+
type={selectIconType}
304+
spin={loading && selectIconType === 'loading'}
305+
onClick={resetSelectedValue}
306+
/>
302307
)}
303308
</div>
304309
) : (
@@ -315,6 +320,8 @@ export default function SearchSelect(props: SearchSelectProps) {
315320
(selectIconType === 'close' || (selectIconType === 'loading' && loading)) && (
316321
<Icon
317322
type={selectIconType}
323+
className={`${prefixCls}-singe-colse`}
324+
color="#393e48"
318325
spin={loading && selectIconType === 'loading'}
319326
onClick={resetSelectedValue}
320327
/>

‎packages/react-search-select/src/style/index.less

+7
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,11 @@
7272
resize: none;
7373
}
7474
}
75+
&-multiple-colse {
76+
left: 7px;
77+
font-size: 15px;
78+
}
79+
&-singe-colse {
80+
font-size: 15px;
81+
}
7582
}

‎website/src/routes/components/cascader/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React from 'react';
2-
import { Form, Row, Col, Cascader } from 'uiw';
2+
import { Form, Row, Col, Cascader, Button } from 'uiw';
33
import Markdown from '../../../components/Markdown';
44

55
export default () => (
66
<Markdown
77
path="https://github.com/uiwjs/uiw/tree/master/packages/react-cascader/README.md"
8-
dependencies={{ Form, Row, Col, React, Cascader }}
8+
dependencies={{ Form, Row, Col, React, Cascader, Button }}
99
renderPage={async () => {
1010
const md = await import('uiw/node_modules/@uiw/react-cascader/README.md');
1111
return md.default || md;

0 commit comments

Comments
 (0)
Please sign in to comment.