improve select

This commit is contained in:
chenos 2021-07-10 16:21:05 +08:00
parent 15fca2b009
commit 9cc5363f49
3 changed files with 616 additions and 106 deletions

View File

@ -5,6 +5,7 @@ import {
Schema, Schema,
useFieldSchema, useFieldSchema,
observer, observer,
SchemaExpressionScopeContext,
} from '@formily/react'; } from '@formily/react';
import { SchemaRenderer, useDesignable } from '../DesignableSchemaField'; import { SchemaRenderer, useDesignable } from '../DesignableSchemaField';
import get from 'lodash/get'; import get from 'lodash/get';
@ -40,6 +41,8 @@ function useDefaultValues() {
} }
export const Form: any = observer((props: any) => { export const Form: any = observer((props: any) => {
const scope = useContext(SchemaExpressionScopeContext);
const { useValues = useDefaultValues } = props; const { useValues = useDefaultValues } = props;
const values = useValues(); const values = useValues();
const form = useMemo(() => { const form = useMemo(() => {
@ -67,6 +70,8 @@ export const Form: any = observer((props: any) => {
return ( return (
<div ref={ref} className={'nb-form'}> <div ref={ref} className={'nb-form'}>
<SchemaRenderer <SchemaRenderer
scope={scope}
// components={options.components}
onRefresh={(subSchema: Schema) => { onRefresh={(subSchema: Schema) => {
designableSchema.properties = subSchema.properties; designableSchema.properties = subSchema.properties;
refresh(); refresh();

View File

@ -11,7 +11,10 @@ group:
# Select - 选择器 # Select - 选择器
## 基本用法
## Select
### 单选
```tsx ```tsx
import React from 'react'; import React from 'react';
@ -70,7 +73,7 @@ export default () => {
}; };
``` ```
## 多选 ### 多选
```tsx ```tsx
import React from 'react'; import React from 'react';
@ -134,7 +137,7 @@ export default () => {
}; };
``` ```
## 分组 ### 分组
```tsx ```tsx
import React from 'react'; import React from 'react';
@ -200,7 +203,11 @@ export default () => {
}; };
``` ```
## 值为对象 ## Select.Object
选项值为 Object 类型
### 单选
```tsx ```tsx
import React from 'react'; import React from 'react';
@ -233,12 +240,14 @@ const schema = {
'x-component': 'Select.Object', 'x-component': 'Select.Object',
'x-component-props': { 'x-component-props': {
placeholder: 'please enter', placeholder: 'please enter',
labelField: 'title', // mode: 'tags',
valueField: 'id', fieldNames: {
labelInValue: true, label: 'title',
value: 'id',
},
}, },
'x-reactions': { 'x-reactions': {
target: 'name2', target: 'read',
fulfill: { fulfill: {
state: { state: {
value: '{{$self.value}}', value: '{{$self.value}}',
@ -250,16 +259,17 @@ const schema = {
interface: 'select', interface: 'select',
type: 'string', type: 'string',
title: `阅读模式`, title: `阅读模式`,
required: true,
enum: dataSource, enum: dataSource,
'x-read-pretty': true, 'x-read-pretty': true,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select.Object', 'x-component': 'Select.Object',
'x-component-props': { 'x-component-props': {
placeholder: 'please enter', placeholder: 'please enter',
labelField: 'title', // mode: 'tags',
valueField: 'id', fieldNames: {
labelInValue: true, label: 'title',
value: 'id',
},
}, },
} }
} }
@ -270,4 +280,282 @@ export default () => {
<SchemaRenderer schema={schema} /> <SchemaRenderer schema={schema} />
); );
}; };
``` ```
### 多选
```tsx
import React from 'react';
import { SchemaRenderer } from '../';
const dataSource = [
{
id: 1,
title: '标题1',
},
{
id: 2,
title: '标题2',
},
{
id: 3,
title: '标题3',
},
];
const schema = {
type: 'object',
properties: {
input: {
interface: 'select',
type: 'string',
title: `编辑模式`,
enum: dataSource,
'x-decorator': 'FormItem',
'x-component': 'Select.Object',
'x-component-props': {
placeholder: 'please enter',
mode: 'tags',
fieldNames: {
label: 'title',
value: 'id',
},
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'select',
type: 'string',
title: `阅读模式`,
enum: dataSource,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Select.Object',
'x-component-props': {
placeholder: 'please enter',
mode: 'tags',
fieldNames: {
label: 'title',
value: 'id',
},
},
}
}
};
export default () => {
return (
<SchemaRenderer schema={schema} />
);
};
```
## Select.Drawer
```tsx
import React from 'react';
import { SchemaRenderer } from '../';
import { useSelect } from './';
console.log({ useSelect })
const dataSource = [
{
id: 1,
title: '标题1',
},
{
id: 2,
title: '标题2',
},
{
id: 3,
title: '标题3',
},
];
function useValues() {
return {
table: dataSource,
}
}
const schema = {
type: 'object',
properties: {
input: {
interface: 'select',
type: 'string',
title: `编辑模式`,
enum: dataSource,
default: [
{
id: 1,
title: '标题1',
},
{
id: 2,
title: '标题2',
},
],
'x-decorator': 'FormItem',
'x-component': 'Select.Drawer',
'x-component-props': {
placeholder: 'please enter',
mode: 'tags',
fieldNames: {
label: 'title',
value: 'id',
},
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
properties: {
options: {
type: 'void',
'x-component': 'Select.Options',
properties: {
form: {
type: 'void',
'x-component': 'Form',
'x-component-props': {
useValues: '{{ useValues }}',
},
properties: {
table: {
type: 'array',
'x-component': 'Table',
properties: {
actionbar: {
type: 'void',
'x-component': 'Table.ActionBar',
'x-component-props': {
align: 'bottom',
},
properties: {
action: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useSelect }}',
},
title: '确定',
},
},
},
column1: {
type: 'void',
title: '标题',
'x-component': 'Table.Column',
'x-component-props': {
// title: 'z1',
},
// 'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
title: {
type: 'string',
required: true,
'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
},
},
read: {
interface: 'select',
type: 'string',
title: `阅读模式`,
enum: dataSource,
default: [
{
id: 1,
title: '标题1',
},
{
id: 2,
title: '标题2',
},
],
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Select.Drawer',
'x-component-props': {
placeholder: 'please enter',
mode: 'tags',
fieldNames: {
label: 'title',
value: 'id',
},
},
properties: {
option: {
type: 'void',
'x-component': 'Select.OptionTag',
properties: {
form: {
type: 'void',
'x-component': 'Form',
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
},
},
}
}
};
export default () => {
return (
<SchemaRenderer scope={{ useSelect, useValues }} schema={schema} />
);
};
```
<!--
<Select />
<Select.Object />
<Select.Drawer>
<Select.Options>
<Table />
<Action />
<Select.Options>
<Select.ItemDetails>
<Form/>
</Select.ItemDetails>
<Select.Drawer>
<Action />
<Action>
<Drawer />
</Action>
-->

View File

@ -1,38 +1,50 @@
import React from 'react'; import React from 'react';
import { connect, mapReadPretty, mapProps, useField } from '@formily/react'; import {
import { Select as AntdSelect, Tag } from 'antd'; connect,
mapReadPretty,
mapProps,
useField,
observer,
RecursionField,
useFieldSchema,
} from '@formily/react';
import { Drawer, Select as AntdSelect, Tag } from 'antd';
import { PreviewText } from '@formily/antd'; import { PreviewText } from '@formily/antd';
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import { SelectProps } from 'antd/lib/select'; import { SelectProps } from 'antd/lib/select';
import { isArr, isValid } from '@formily/shared'; import { isArr, isValid, toArr } from '@formily/shared';
import { Field } from '@formily/core/esm/models/Field'; import { useState } from 'react';
import { useAction } from '../../state'; import { useDesignable } from '../DesignableSchemaField';
import { Display } from '../display'; import { createContext } from 'react';
import { useRequest } from 'ahooks'; import { useContext } from 'react';
import uniq from 'lodash/uniq'; import { useTableContext } from '../table';
type ComposedSelect = React.FC<SelectProps<any>> & { export const Select: any = connect(
Object?: React.FC;
};
export const Select: ComposedSelect = connect(
(props) => { (props) => {
const { options = [], ...others } = props; const { options = [], ...others } = props;
return <AntdSelect {...others}> return (
{options.map((option: any, key: any) => { <AntdSelect {...others}>
if (option.children) { {options.map((option: any, key: any) => {
return ( if (option.children) {
<AntdSelect.OptGroup key={key} label={option.label}> return (
{option.children.map((child: any, childKey: any) => ( <AntdSelect.OptGroup key={key} label={option.label}>
<AntdSelect.Option key={`${key}-${childKey}`} {...child}>{child.label}</AntdSelect.Option> {option.children.map((child: any, childKey: any) => (
))} <AntdSelect.Option key={`${key}-${childKey}`} {...child}>
</AntdSelect.OptGroup> {child.label}
) </AntdSelect.Option>
} else { ))}
return <AntdSelect.Option key={key} {...option}>{option.label}</AntdSelect.Option> </AntdSelect.OptGroup>
} );
})} } else {
</AntdSelect> return (
<AntdSelect.Option key={key} {...option}>
{option.label}
</AntdSelect.Option>
);
}
})}
</AntdSelect>
);
}, },
mapProps( mapProps(
{ {
@ -54,9 +66,8 @@ export const Select: ComposedSelect = connect(
mapReadPretty((props) => { mapReadPretty((props) => {
const field = useField<any>(); const field = useField<any>();
const dataSource = field.dataSource || []; const dataSource = field.dataSource || [];
console.log('field.value', field.value, dataSource) console.log('field.value', field.value, dataSource);
const value = Array.isArray(field.value) ? field.value : [field.value]; const values = toArr(field.value);
const values = uniq(value);
const findOptions = (options: any[]) => { const findOptions = (options: any[]) => {
let current = []; let current = [];
for (const option of options) { for (const option of options) {
@ -64,100 +75,96 @@ export const Select: ComposedSelect = connect(
current.push(option); current.push(option);
} }
if (option.children) { if (option.children) {
current = current.concat(findOptions(option.children)); const children = findOptions(option.children);
current.push(...children);
} }
} }
return current; return current;
} };
const options = findOptions(dataSource); const options = findOptions(dataSource);
return ( return (
<div> <div>
{options {options.map((option, key) => (
.map((option, key) => ( <Tag key={key} color={option.color}>
<Tag key={key} color={option.color}>{option.label}</Tag> {option.label}
))} </Tag>
))}
</div> </div>
); );
}), }),
); );
function getFieldName(field: any) {
if (typeof field === 'string') {
return field;
}
if (typeof field === 'object') {
return field.name;
}
}
Select.Object = connect( Select.Object = connect(
(props) => { (props) => {
const field = useField(); const field = useField();
const { const {
value, value,
onChange, onChange,
dataRequest, fieldNames = {
labelField: lf, label: 'label',
valueField: vf, value: 'value',
},
...others ...others
} = props; } = props;
const options = field?.['dataSource'] || props.options || []; const options = field?.['dataSource'] || props.options || [];
console.log({options}); let optionValue = undefined;
const labelField = getFieldName(lf); if (isArr(value)) {
const valueField = getFieldName(vf); optionValue = value.map((val) => {
const { data = options, loading } = useRequest(() => dataRequest ? dataRequest : Promise.resolve(options), {
refreshDeps: [options]
});
const getValues = () => {
if (!isValid(value)) {
return [];
}
return isArr(value) ? value : [value];
}
const getOptionValue = () => {
const values = getValues().map((item) => {
return { return {
value: item[valueField], label: val[fieldNames.label],
label: item[labelField], value: val[fieldNames.value],
}; };
}); });
if (props.mode === 'multiple' || props.mode === 'tags') { } else if (value) {
return values; optionValue = {
} label: value[fieldNames.label],
return values.length ? values[0] : null; value: value[fieldNames.value],
}; };
const getOptions = () => {
return data.map(item => {
return {
value: item[valueField],
label: item[labelField],
};
})
} }
return ( return (
<AntdSelect <AntdSelect
allowClear
labelInValue
{...others} {...others}
loading={loading} value={optionValue}
onChange={(option: any) => { onChange={(selectValue: any) => {
if (!isValid(option)) { if (!selectValue) {
onChange(null); onChange(null);
} }
if (isArr(option)) { if (isArr(selectValue)) {
const optionValues = option.map(item => item.value); const selectValues = selectValue.map((s) => s.value);
const selectValue = data.filter( const values = {};
(item) => optionValues.includes(item[valueField]), if (isArr(value)) {
); value.forEach((option) => {
onChange(selectValue); const val = option[fieldNames.value];
if (selectValues.includes(val)) {
values[val] = option;
}
});
}
options.forEach((option) => {
const val = option[fieldNames.value];
if (selectValues.includes(val)) {
values[val] = option;
}
});
console.log({ selectValue, values });
onChange(Object.values(values));
} else { } else {
const selectValue = data.find( // 这里不能用 undefined需要用 null
(item) => item[valueField] === option.value, onChange(
options.find(
(option) => option[fieldNames.value] === selectValue.value,
),
); );
onChange(selectValue);
} }
}} }}
labelInValue options={options.map((option) => {
value={getOptionValue()} return {
options={getOptions()} label: option[fieldNames.label],
value: option[fieldNames.value],
};
})}
/> />
); );
}, },
@ -179,7 +186,217 @@ Select.Object = connect(
}; };
}, },
), ),
mapReadPretty(Display.ObjectSelect), mapReadPretty(
observer((props: any) => {
const { value, fieldNames = { label: 'label' }, ...others } = props;
if (!value) {
return null;
}
const values = toArr(value);
return (
<div>
{values.map((val) =>
fieldNames.color ? (
<Tag color={val[fieldNames.color]}>{val[fieldNames.label]}</Tag>
) : (
<Tag>{val[fieldNames.label]}</Tag>
),
)}
</div>
);
}),
),
); );
const OptionTagContext = createContext(null);
const SelectContext = createContext<any>({});
Select.Drawer = connect(
(props) => {
const field = useField();
const {
value,
onChange,
fieldNames = {
label: 'label',
value: 'value',
},
...others
} = props;
const [visible, setVisible] = useState(false);
const schema = useFieldSchema();
const options = field?.['dataSource'] || props.options || [];
let optionValue = undefined;
if (isArr(value)) {
optionValue = value.map((val) => {
return {
label: val[fieldNames.label],
value: val[fieldNames.value],
};
});
} else if (value) {
optionValue = {
label: value[fieldNames.label],
value: value[fieldNames.value],
};
}
const onFieldChange = (selectValue) => {
if (!isValid(selectValue)) {
onChange(null);
return;
}
if (isArr(selectValue)) {
const values = {};
if (isArr(value)) {
value.forEach((option) => {
const val = option[fieldNames.value];
if (selectValue.includes(val)) {
values[val] = option;
}
});
}
options.forEach((option) => {
const val = option[fieldNames.value];
if (selectValue.includes(val)) {
values[val] = option;
}
});
onChange(Object.values(values));
} else {
// 这里不能用 undefined需要用 null
onChange(
options.find((option) => option[fieldNames.value] === selectValue),
);
}
};
return (
<>
<AntdSelect
{...others}
labelInValue
open={false}
value={optionValue}
onClick={() => {
setVisible(true);
}}
onChange={(selectValue: any) => {
if (!selectValue) {
onChange(null);
return;
}
if (isArr(selectValue)) {
const selectValues = selectValue.map((s) => s.value);
onFieldChange(selectValues);
} else {
onFieldChange(selectValue.value);
}
}}
></AntdSelect>
<Drawer
width={'50%'}
visible={visible}
onClose={() => setVisible(false)}
>
<SelectContext.Provider
value={{
onChange(selectValue) {
onFieldChange(selectValue);
setVisible(false);
},
}}
>
<RecursionField
onlyRenderProperties
schema={schema}
filterProperties={(s) => {
return s['x-component'] === 'Select.Options';
}}
/>
</SelectContext.Provider>
</Drawer>
</>
);
},
mapProps(
{
dataSource: 'options',
loading: true,
},
(props, field) => {
return {
...props,
options: field?.['dataSource'],
suffixIcon:
field?.['loading'] || field?.['validating'] ? (
<LoadingOutlined />
) : (
props.suffixIcon
),
};
},
),
mapReadPretty(
observer((props: any) => {
const field = useField<Formily.Core.Models.Field>();
const { fieldNames = { label: 'label' }, ...others } = props;
const value = field.value || field.initialValue;
const schema = useFieldSchema();
console.log({ field, value });
if (!value) {
return null;
}
const values = toArr(value);
return (
<div>
{values.map((data, index) => {
return (
<OptionTagContext.Provider value={{ index, data, fieldNames }}>
<RecursionField
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === 'Select.OptionTag';
}}
/>
</OptionTagContext.Provider>
);
})}
</div>
);
}),
),
);
Select.Options = observer((props) => {
return <>{props.children}</>;
});
export function useSelect() {
const { onChange } = useContext(SelectContext);
const { data, selectedRowKeys } = useTableContext();
return {
async run() {
console.log({ data, selectedRowKeys });
onChange && onChange(selectedRowKeys);
},
};
}
Select.OptionTag = observer((props) => {
const [visible, setVisible] = useState(false);
const { data, fieldNames } = useContext(OptionTagContext);
return (
<>
<Tag onClick={() => setVisible(true)}>{data[fieldNames.label]}</Tag>
<Drawer width={'50%'} visible={visible} onClose={() => setVisible(false)}>
{props.children}
</Drawer>
</>
);
});
export default Select; export default Select;