['modifyProps'] = (props) => {
304 | const { itemProps = {} } = props;
305 | const { children } = itemProps;
306 | return {
307 | itemProps: {
308 | viewProps: {
309 | format: (value, pureValue) => {
310 | if (pureValue) {
311 | return value;
312 | }
313 | return isValidElement(children) ? (
314 |
315 | ) : (
316 | children
317 | );
318 | },
319 | },
320 | ...itemProps,
321 | },
322 | };
323 | };
324 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/__test__/Submit.test.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-console */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { mount } from 'enzyme';
5 | import SecureButton from '../component/SecureButton';
6 | import { delay, waitForComponentToPaint } from './utils';
7 |
8 | describe('SecureButton', () => {
9 | // 组件卸载
10 | let container: any;
11 | beforeEach(() => {
12 | container = document.createElement('div');
13 | mount(, { attachTo: container });
14 | });
15 |
16 | afterEach(() => {
17 | ReactDOM.unmountComponentAtNode(container);
18 | });
19 |
20 | test('click', async () => {
21 | const onClick = () => {};
22 | const wrapper = mount();
23 | await wrapper.find('.ant-btn').at(0).simulate('click');
24 | });
25 |
26 | test('click loading', async () => {
27 | const onClick = jest.fn();
28 | const wrapper = mount();
29 | await wrapper.find('.ant-btn').at(0).simulate('click');
30 | expect(onClick).not.toHaveBeenCalled();
31 | });
32 |
33 | test('click error', async () => {
34 | const onClick = async () => {
35 | await Promise.reject();
36 | };
37 | const wrapper = mount();
38 | waitForComponentToPaint(wrapper);
39 | await wrapper.find('.ant-btn').at(0).simulate('click');
40 | });
41 | test('click delay', async () => {
42 | const onClick = async () => {
43 | await delay(600);
44 | };
45 | const wrapper = mount();
46 | waitForComponentToPaint(wrapper);
47 | await wrapper.find('.ant-btn').at(0).simulate('click');
48 | });
49 | test('click 500', async () => {
50 | const onClick = async () => {};
51 | const onLoaded = jest.fn();
52 | const wrapper = mount();
53 | waitForComponentToPaint(wrapper);
54 | await wrapper.find('.ant-btn').at(0).simulate('click');
55 | await delay(500);
56 | expect(onLoaded).toHaveBeenCalled();
57 | });
58 | test('click 600', async () => {
59 | const onClick = async () => {
60 | await delay(600);
61 | };
62 | const onLoaded = jest.fn();
63 | const wrapper = mount();
64 | waitForComponentToPaint(wrapper);
65 | await wrapper.find('.ant-btn').at(0).simulate('click');
66 | await delay(700);
67 | expect(onLoaded).toHaveBeenCalled();
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/__test__/Utils.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { searchSelect, calculateStrLength, oneLineItemStyle, getFieldKeyValue } from '../utils';
3 |
4 | const dom = (
5 |
6 |
7 | 12
8 |
9 |
10 | 23
11 |
12 | 34
13 | 45
14 |
15 | );
16 |
17 | describe('Utils test', () => {
18 | test('test Utils', () => {
19 | searchSelect.filterOption('1', dom);
20 | expect(calculateStrLength()).toBe(0);
21 | expect(calculateStrLength('我')).toBe(2);
22 | expect(calculateStrLength(123)).toBe(3);
23 | expect(oneLineItemStyle()).toEqual([]);
24 | expect(oneLineItemStyle(['50%', 8, '50%'])).toEqual([
25 | { display: 'inline-block', width: 'calc(50% - 4px)' },
26 | { display: 'inline-block', width: '8px' },
27 | { display: 'inline-block', width: 'calc(50% - 4px)' },
28 | ]);
29 | expect(getFieldKeyValue({ id: 1 }, 1, 'id')).toBe(1);
30 | expect(getFieldKeyValue({ id: 1 }, 1, () => undefined)).toBe(1);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/__test__/fields.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input } from 'antd';
3 | import moment from 'moment';
4 | import { YFormItemProps } from '../Items';
5 |
6 | const options = [
7 | { id: '1', name: '语文' },
8 | { id: '2', name: '数学' },
9 | ];
10 |
11 | const initialValues = {
12 | name: '张三',
13 | age: '10',
14 | textarea: '这里是长文本',
15 | text: '这里是文本',
16 | custom: '这里是自定义文本',
17 | money: '999999999',
18 | 单选: true,
19 | 开关: true,
20 | 多选: ['1', '2'],
21 | 下拉框多选: ['1', '2'],
22 | 下拉框: '1',
23 | radio: '1',
24 | users: [
25 | { name: '张三', age: '10' },
26 | { name: '李四', age: '20' },
27 | ],
28 | phones: [{ phone: '18888888888' }, { phone: '18888888888' }],
29 | date: moment(),
30 | };
31 |
32 | const fields: YFormItemProps['children'] = [
33 | { type: 'input', label: '空值', name: 'names', scenes: { base: false } },
34 | {
35 | type: 'list',
36 | name: 'phones',
37 | componentProps: {
38 | showIcons: { showBottomAdd: { text: '添加手机号' }, showAdd: true, showRemove: false },
39 | onShowIcons: () => ({
40 | showAdd: true,
41 | showRemove: true,
42 | }),
43 | },
44 | items: ({ index }) => {
45 | return [
46 | {
47 | label: index === 0 && '手机号',
48 | type: 'input',
49 | name: [index, 'phone'],
50 | componentProps: { placeholder: '请输入手机号' },
51 | },
52 | ];
53 | },
54 | },
55 | {
56 | label: '用户',
57 | type: 'list',
58 | name: 'users',
59 | items: ({ index }) => {
60 | return [
61 | {
62 | type: 'oneLine',
63 | componentProps: { oneLineStyle: ['50%', 8, '50%'] },
64 | items: () => [
65 | { label: '姓名', type: 'input', name: [index, 'name'] },
66 | { type: 'custom', children: },
67 | { label: '年龄', type: 'input', name: [index, 'age'] },
68 | ],
69 | },
70 | ];
71 | },
72 | },
73 | {
74 | type: 'input',
75 | label: '姓名',
76 | name: 'name',
77 | format: (name) => `${name} 修改了`,
78 | },
79 | {
80 | type: 'datePicker',
81 | label: '日期',
82 | name: 'date',
83 | componentProps: { style: { width: '100%' } },
84 | format: (date) => moment(date).format('YYYY-MM-DD'),
85 | },
86 | {
87 | type: 'money',
88 | label: '金额',
89 | componentProps: { suffix: '元' },
90 | name: 'money',
91 | },
92 | { type: 'textarea', label: '文本域', name: 'textarea' },
93 | { type: 'checkbox', label: '单选', name: '单选', componentProps: { children: '已阅读' } },
94 | {
95 | type: 'checkboxGroup',
96 | label: '多选',
97 | name: '多选',
98 | componentProps: { options },
99 | },
100 | {
101 | type: 'checkboxGroup',
102 | label: '多选2',
103 | name: '多选2',
104 | componentProps: { onAddProps: () => ({ disabled: true }), renderOption: () => '1', options },
105 | },
106 | {
107 | type: 'switch',
108 | label: '开关',
109 | name: '开关',
110 | componentProps: { checkedChildren: '开', unCheckedChildren: '关' },
111 | },
112 | {
113 | type: 'select',
114 | label: '下拉框',
115 | name: '下拉框',
116 | componentProps: {
117 | optionLabelProp: 'checked-value',
118 | onAddProps: (item) => ({ 'checked-value': `(${item.name})` }),
119 | showField: (record) => `${record.id}-${record.name}`,
120 | options,
121 | },
122 | },
123 | {
124 | type: 'select',
125 | label: '下拉框多选',
126 | name: '下拉框多选',
127 | componentProps: { mode: 'multiple', options },
128 | },
129 | {
130 | type: 'radio',
131 | label: '单选按钮',
132 | name: 'radio',
133 | componentProps: { showField: (record) => `${record.id}-${record.name}`, options },
134 | },
135 | {
136 | type: 'radio',
137 | label: '单选按钮2',
138 | name: 'radio2',
139 | componentProps: { onAddProps: () => ({ disabled: true }), options },
140 | },
141 | {
142 | type: 'radio',
143 | label: '单选按钮3',
144 | name: 'radio3',
145 | componentProps: { renderOption: () => '1', options },
146 | },
147 | { label: '文本', name: 'text', type: 'text' },
148 | {
149 | label: '自定义渲染',
150 | type: 'custom',
151 | name: 'custom',
152 | children: ,
153 | },
154 | { type: 'submit' },
155 |
156 | {
157 | label: '精简使用',
158 | type: 'input',
159 | name: 'children_field2',
160 | shouldUpdate: (prevValues, curValues) => prevValues.type !== curValues.type,
161 | isShow: (values) => values.type === '2',
162 | },
163 | ];
164 |
165 | export { fields, initialValues };
166 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/__test__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { mount } from 'enzyme';
4 |
5 | import { YForm } from '../..';
6 | import { layoutMore } from '../utils';
7 | import { YFormProps } from '../Form';
8 |
9 | const YFormDemo = (props: YFormProps) => {
10 | const onFinish = (values: any) => {
11 | console.log('Success:', values);
12 | };
13 |
14 | const { onFormatFieldsValue, formatFieldsValue } = YForm.useFormatFieldsValue();
15 |
16 | onFormatFieldsValue([
17 | { name: ['demo', 'aa'], format: (_, values: { users: any }) => values.users },
18 | ]);
19 |
20 | return (
21 |
27 | {[
28 | { type: 'input', name: 'demo' },
29 | { type: 'money', name: 'money', label: 'money' },
30 | {
31 | dataSource: [
32 | {
33 | type: 'button',
34 | noStyle: true,
35 | componentProps: {
36 | type: 'primary',
37 | htmlType: 'submit',
38 | children: 'submit',
39 | },
40 | },
41 | ],
42 | },
43 | ]}
44 |
45 | );
46 | };
47 |
48 | describe('YForm', () => {
49 | test('renders', () => {
50 | const wrapper = render();
51 | expect(wrapper).toMatchSnapshot();
52 | });
53 | test('isShow', () => {
54 | const wrapper = render();
55 | expect(wrapper).toMatchSnapshot();
56 | });
57 | test('loading', () => {
58 | const wrapper = render();
59 | expect(wrapper).toMatchSnapshot();
60 | });
61 | test('layout', () => {
62 | const wrapper = render(
63 |
64 |
65 |
66 |
,
67 | );
68 | expect(wrapper).toMatchSnapshot();
69 | });
70 | test('layout2', () => {
71 | const itemsType: any = {
72 | demo: { component: 123
, formatStr: '请输入${label}' },
73 | };
74 | YForm.Config({ itemsType });
75 | const wrapper = render({[{ type: 'demo' }]});
76 | expect(wrapper).toMatchSnapshot();
77 | });
78 | test('onFinish', () => {
79 | const wrapper = mount();
80 | wrapper.find('Button').simulate('submit');
81 | const wrapper2 = mount();
82 | wrapper2.find('Button').simulate('submit');
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/__test__/utils.tsx:
--------------------------------------------------------------------------------
1 | import { act } from 'react-dom/test-utils';
2 | import { ReactWrapper } from 'enzyme';
3 |
4 | export const delay = (timeout = 0) =>
5 | new Promise((resolve) => {
6 | setTimeout(resolve, timeout);
7 | });
8 |
9 | export async function waitForComponentToPaint(wrapper: ReactWrapper
, amount = 0) {
10 | await act(async () => {
11 | await new Promise((resolve) => setTimeout(resolve, amount));
12 | wrapper.update();
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Card.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { Card } from 'antd';
3 | import { CardProps } from 'antd/lib/card';
4 |
5 | import YForm from '..';
6 | import { YFormItemProps } from '../Items';
7 |
8 | export interface YFormCardProps extends YFormItemProps {
9 | componentProps?: CardProps;
10 | items?: YFormItemProps['children'];
11 | }
12 |
13 | export default forwardRef((props, ref) => {
14 | const itemProps = React.useContext(YForm.YFormItemContext);
15 | const { items } = itemProps as YFormCardProps;
16 |
17 | React.useImperativeHandle(ref, () => props);
18 |
19 | return (
20 |
21 | {items}
22 |
23 | );
24 | });
25 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/CheckboxGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { Checkbox } from 'antd';
3 | import { map } from 'lodash';
4 | import { CheckboxGroupProps } from 'antd/lib/checkbox';
5 |
6 | import { getFieldKeyValue } from '../utils';
7 | import { OptionsProps } from '../ItemsType';
8 | import { useGetOptions } from '../hooks';
9 |
10 | export interface YCheckGroupProps extends OptionsProps, Omit {}
11 |
12 | export default forwardRef((props, ref) => {
13 | const {
14 | value,
15 | postField = 'id',
16 | showField = 'name',
17 | options,
18 | renderOption,
19 | onAddProps,
20 | getOptions,
21 | ...rest
22 | } = props;
23 | const list = useGetOptions();
24 |
25 | const children = map(list, (item, index: number) => {
26 | if (item) {
27 | const _postField = getFieldKeyValue(item, index, postField);
28 | const _showField = getFieldKeyValue(item, index, showField);
29 | return (
30 |
36 | {/* 如果有 renderOption 就渲染 renderOption 如果没有默认用 showField 字段 */}
37 | {renderOption ? renderOption(item) : _showField}
38 |
39 | );
40 | }
41 | });
42 | React.useImperativeHandle(ref, () => props);
43 |
44 | return (
45 |
46 | {children}
47 |
48 | );
49 | });
50 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/ComponentView.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import classNames from 'classnames';
3 | import { get, isArray } from 'lodash';
4 | import { ConfigContext } from 'antd/lib/config-provider';
5 |
6 | import { YForm } from '../..';
7 | import { YFormItemProps } from '../Items';
8 |
9 | export const noData = -/-;
10 |
11 | export interface YFormComponentViewComponentProps {
12 | addonBefore?: React.ReactNode;
13 | addonAfter?: React.ReactNode;
14 | suffix?: React.ReactNode;
15 | prefix?: React.ReactNode;
16 | className?: string;
17 | oldValue?: any;
18 | }
19 |
20 | export interface YFormComponentViewProps extends YFormItemProps {
21 | componentProps?: YFormComponentViewComponentProps;
22 | }
23 |
24 | export default forwardRef((props, ref) => {
25 | const AntdConfig = React.useContext(ConfigContext);
26 | const itemProps = React.useContext(YForm.YFormItemContext);
27 | const { viewProps: { format } = {}, valuePropName = 'value' } = itemProps;
28 | const { addonBefore, addonAfter, suffix, prefix, className, oldValue } = props;
29 | // diff 渲染使用 oldValue
30 | let _value = 'oldValue' in props ? oldValue : get(props, valuePropName);
31 | if (format) {
32 | _value = format(_value);
33 | }
34 | const prefixCls = AntdConfig.getPrefixCls('');
35 |
36 | return (
37 |
38 | {addonBefore && {addonBefore} }
39 | {prefix && {prefix} }
40 | {_value === undefined || _value === '' || (isArray(_value) && _value.length === 0)
41 | ? noData
42 | : _value}
43 | {suffix && {suffix}}
44 | {addonAfter && {addonAfter}}
45 |
46 | );
47 | });
48 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Diff.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { get, concat } from 'lodash';
3 | import classNames from 'classnames';
4 |
5 | import ComponentView from './ComponentView';
6 | import { YForm } from '../..';
7 | import { modifyType } from '../ItemsType';
8 |
9 | const DiffDom = (props: modifyType) => {
10 | const { formProps, itemProps, componentProps } = props;
11 | const { oldValues = {} } = formProps;
12 | const { name } = itemProps;
13 |
14 | const context = React.useContext(YForm.ListContent);
15 | const allName = context.prefixName ? concat(context.prefixName, name) : name;
16 | const _oldValue = 'oldValue' in itemProps ? itemProps.oldValue : get(oldValues, allName);
17 | return (
18 |
19 | {[
20 | {
21 | noStyle: true,
22 | shouldUpdate: (prevValues, curValues) => {
23 | return get(prevValues, allName) !== get(curValues, allName);
24 | },
25 | children: ({ getFieldValue }) => {
26 | // 如果字段为 undefined 则改为 '',为了字段输入值再删除一样的道理
27 | const value = getFieldValue(allName) === undefined ? '' : getFieldValue(allName);
28 | const oldValue = _oldValue === undefined ? '' : _oldValue;
29 | let equal = value === oldValue;
30 | // 如果有渲染方法,就按照次来对比
31 | if (itemProps.viewProps) {
32 | const { format } = itemProps.viewProps;
33 | if (format) {
34 | // 这里用的 pureValue = true(纯值),直接 === 判断就可以。
35 | equal = format(value, true) === format(oldValue, true);
36 | }
37 | }
38 | if (itemProps.diffProps) {
39 | const { onEqual } = itemProps.diffProps;
40 | if (onEqual) {
41 | // 如果有 onEqual,则最终使用该方法判断
42 | equal = onEqual(value, oldValue);
43 | }
44 | }
45 |
46 | if (equal) {
47 | return null;
48 | }
49 | return (
50 |
51 |
52 |
57 |
58 |
59 | );
60 | },
61 | },
62 | ]}
63 |
64 | );
65 | };
66 |
67 | export default DiffDom;
68 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/List.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form } from 'antd';
3 | import { MinusCircleOutlined, PlusOutlined, PlusCircleOutlined } from '@ant-design/icons';
4 | import { map, isArray, concat } from 'lodash';
5 | import classNames from 'classnames';
6 |
7 | import YForm from '../index';
8 | import { oneLineItemStyle, mergeWithDom } from '../utils';
9 | import { YFormItemProps } from '../Items';
10 |
11 | export type ShowIconsType = {
12 | showBottomAdd?: boolean | { text?: string };
13 | showAdd?: boolean;
14 | showRemove?: boolean;
15 | };
16 |
17 | export interface YFormListComponentProps {
18 | maxNum?: number;
19 | minNum?: number;
20 | showRightIcons?: boolean;
21 | isUseIconStyle?: boolean;
22 | showIcons?: ShowIconsType;
23 | onShowIcons?: (p: { index: number }) => Pick;
24 | }
25 |
26 | export interface YFormListItems {
27 | index: number;
28 | field: { name: number; key: number; fieldKey: number; isListField?: boolean };
29 | add: () => void;
30 | remove: (index: number) => void;
31 | move: (from: number, to: number) => void;
32 | icons: React.ReactNode;
33 | iconsWidth: number;
34 | layoutStyles: React.CSSProperties[];
35 | }
36 |
37 | export interface YFormListProps extends YFormItemProps {
38 | items?: (p: YFormListItems) => YFormItemProps['children'];
39 | componentProps?: YFormListComponentProps;
40 | // 用于 diff 状态下处理数据
41 | addonBefore?: React.ReactNode;
42 | }
43 |
44 | export default (props: YFormListProps['componentProps']) => {
45 | const itemProps = React.useContext(YForm.YFormItemContext);
46 | const { disabled, scenes, label, items, name, addonBefore, offset } = itemProps as YFormListProps;
47 | const {
48 | maxNum,
49 | minNum,
50 | showRightIcons = true,
51 | isUseIconStyle = true,
52 | showIcons: { showBottomAdd = true, showAdd = true, showRemove = true } = {},
53 | onShowIcons,
54 | } = props;
55 | const context = React.useContext(YForm.ListContent);
56 | // 支持多级 List name 拼接
57 | const _name = context.prefixName ? concat(context.prefixName, name) : name;
58 |
59 | return (
60 |
61 | {addonBefore}
62 |
63 | {(fields, { add, remove, move }) => {
64 | const isMax = maxNum ? fields.length < maxNum : true;
65 | const isMin = minNum ? fields.length > minNum : true;
66 |
67 | return (
68 | <>
69 | {fields.map((field, index) => {
70 | const _showIcons = onShowIcons ? onShowIcons({ index }) : {};
71 | const { showAdd: _showAdd, showRemove: _showRemove } = mergeWithDom(
72 | { showAdd, showRemove },
73 | _showIcons,
74 | );
75 | const icons: React.ReactNode[] = [];
76 | if (!disabled) {
77 | if (isMax && _showAdd) {
78 | icons.push(
79 | {
82 | // 先增加一位
83 | add();
84 | // 再把最后一位移动到当前
85 | move(fields.length, index);
86 | }}
87 | />,
88 | );
89 | }
90 | if (isMin && _showRemove) {
91 | icons.push( remove(index)} />);
92 | }
93 | }
94 |
95 | const iconsWidth = icons.length * (8 + 14);
96 | const _oneLineStyle = oneLineItemStyle(['100%', iconsWidth]);
97 | // 第一行有 label(当然,外部需要传 label 参数)
98 | const _label = index === 0 && label;
99 |
100 | const _iconsDom = {icons}
;
101 |
102 | const style = {
103 | ...(!disabled && showRightIcons && isUseIconStyle && _oneLineStyle[0]),
104 | };
105 | const dataSource =
106 | items &&
107 | items({
108 | index,
109 | field,
110 | add,
111 | remove,
112 | move,
113 | icons: _iconsDom,
114 | iconsWidth,
115 | layoutStyles: _oneLineStyle,
116 | });
117 | let _children = dataSource;
118 | if (isArray(dataSource)) {
119 | _children = map(dataSource, (item, index) => {
120 | const _item = mergeWithDom(
121 | {
122 | componentProps: { style },
123 | label: index === 0 && _label,
124 | hideLable: label,
125 | },
126 | item,
127 | {
128 | // 内部使用
129 | _addonAfter: [
130 | showRightIcons && !disabled && index === 0 && (
131 |
136 | {icons}
137 |
138 | ),
139 | ].filter((x) => x),
140 | },
141 | );
142 | return _item;
143 | });
144 | }
145 | return (
146 |
150 |
151 | {_children}
152 |
153 |
154 | );
155 | })}
156 | {showBottomAdd && isMax && !disabled && (
157 |
158 | {[
159 | {
160 | type: 'button',
161 | offset,
162 | componentProps: {
163 | disabled,
164 | type: 'dashed',
165 | onClick: () => add(),
166 | style: { width: '100%' },
167 | children: (
168 | <>
169 |
170 | {typeof showBottomAdd === 'object'
171 | ? showBottomAdd.text
172 | : `添加${label || ''}`}
173 | >
174 | ),
175 | },
176 | },
177 | ]}
178 |
179 | )}
180 | >
181 | );
182 | }}
183 |
184 |
185 | );
186 | };
187 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Money.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import Numbro from 'numbro';
3 | import { Input } from 'antd';
4 | import { InputProps } from 'antd/lib/input';
5 | import { convertMoney } from '../utils';
6 |
7 | export interface YMoneyProps extends InputProps {
8 | onChange?: (value: any) => void;
9 | }
10 |
11 | export default forwardRef((props, ref) => {
12 | const { onChange, ...rest } = props;
13 | const { value } = rest;
14 | const handleNumberChange = (e: React.ChangeEvent) => {
15 | if (onChange) {
16 | const { value } = e.target;
17 | if (value === undefined || value === '') {
18 | return value;
19 | }
20 | const _number = parseFloat(value);
21 | if (Number.isNaN(_number)) {
22 | return onChange(0);
23 | }
24 | // 如果有小数,保留 2 位小数。
25 | onChange(
26 | Numbro(_number).format({
27 | trimMantissa: true,
28 | mantissa: 2,
29 | }),
30 | );
31 | // 这个会强制有小数
32 | // onChange(number.toFixed(2));
33 | }
34 | };
35 | return (
36 |
37 |
38 |
{convertMoney(`${value || ''}`)}
39 |
40 | );
41 | });
42 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/OneLine.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { map, get, isArray, isObject } from 'lodash';
3 | import classNames from 'classnames';
4 | import warning from 'warning';
5 |
6 | import YForm from '..';
7 | import { oneLineItemStyle, mergeWithDom } from '../utils';
8 | import { BaseComponentProps } from '../ItemsType';
9 | import { YFormItemProps } from '../Items';
10 |
11 | export interface YFormOneLineProps extends YFormItemProps {
12 | componentProps?: BaseComponentProps & { oneLineStyle?: (string | number)[] };
13 | items?: (p: { style: React.CSSProperties[] }) => YFormItemProps['children'];
14 | oneLineStyle?: (string | number)[];
15 | }
16 |
17 | export default forwardRef((props, ref) => {
18 | const itemProps = React.useContext(YForm.YFormItemContext);
19 | const { scenes, items } = itemProps as YFormOneLineProps;
20 | const { oneLineStyle, className, style } = props;
21 | if (get(props, 'name')) {
22 | warning(false, 'oneLine 不支持 name');
23 | return null;
24 | }
25 |
26 | const styleObj = oneLineItemStyle(oneLineStyle || []);
27 | const _dataSource = items && items({ style: styleObj });
28 | let _childrenDataSource = _dataSource;
29 | if (isArray(_dataSource)) {
30 | _childrenDataSource = map(_dataSource, (item, index) => {
31 | if (!item) return;
32 | const _style = get(styleObj, index, {});
33 | if (isObject(item)) {
34 | return mergeWithDom({ display: 'inline-block', style: { ..._style }, scenes }, item);
35 | }
36 | }).filter((x) => x);
37 | }
38 | return (
39 |
40 | {_childrenDataSource}
41 |
42 | );
43 | });
44 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Radio.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { Radio } from 'antd';
3 | import { map } from 'lodash';
4 | import { RadioGroupProps } from 'antd/lib/radio';
5 |
6 | import { getFieldKeyValue } from '../utils';
7 | import { OptionsProps } from '../ItemsType';
8 | import { useGetOptions } from '../hooks';
9 |
10 | export interface YRadioProps extends OptionsProps, Omit {}
11 |
12 | export default forwardRef((props, ref) => {
13 | const {
14 | value,
15 | postField = 'id',
16 | showField = 'name',
17 | options,
18 | renderOption,
19 | onAddProps,
20 | getOptions,
21 | ...rest
22 | } = props;
23 | // 可以是方法返回的异步数据
24 | const list = useGetOptions();
25 | const children = map(list, (item, index: number) => {
26 | if (item) {
27 | const _postField = getFieldKeyValue(item, index, postField);
28 | const _showField = getFieldKeyValue(item, index, showField);
29 | return (
30 |
36 | {/* 如果有 renderOption 就渲染 renderOption 如果没有默认用 showField 字段 */}
37 | {renderOption ? renderOption(item) : _showField}
38 |
39 | );
40 | }
41 | });
42 | React.useImperativeHandle(ref, () => props);
43 | return (
44 |
45 | {children}
46 |
47 | );
48 | });
49 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/SecureButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useCallback } from 'react';
2 | import { Button } from 'antd';
3 | import { ButtonProps } from 'antd/lib/button';
4 |
5 | export interface YFormSecureButtonProps {
6 | componentProps?: ButtonProps & { onLoaded?: () => void; minBtnLoadingTime?: number };
7 | }
8 |
9 | const SecureButton: React.FC = (props) => {
10 | const { onClick, onLoaded, minBtnLoadingTime = 500, ...rest } = props;
11 | const [loading, setLoading] = useState(false);
12 | const timeOut = useRef(null);
13 |
14 | useEffect(() => {
15 | return () => clearTimeout(timeOut.current);
16 | }, []);
17 |
18 | const handleSetFalseLoading = useCallback(
19 | (end: number, begin: number, err?: any) => {
20 | if (err) {
21 | setLoading(false);
22 | } else if (end - begin > minBtnLoadingTime) {
23 | // 如果 onClick 执行时间大于 0.5s,就立刻取消 loading
24 | setLoading(false);
25 | if (onLoaded) onLoaded();
26 | } else {
27 | // 如果 onClick 执行时间小于 0.5s,就设置 0.5s 后再取消 loading
28 | timeOut.current = window.setTimeout(() => {
29 | setLoading(false);
30 | if (onLoaded) onLoaded();
31 | }, minBtnLoadingTime);
32 | }
33 | },
34 | [onLoaded, minBtnLoadingTime],
35 | );
36 |
37 | const handleClick = async (e: React.MouseEvent) => {
38 | if (onClick) {
39 | setLoading(true);
40 | const begin = new Date().getTime();
41 | try {
42 | await onClick(e);
43 | const end = new Date().getTime();
44 | handleSetFalseLoading(end, begin);
45 | } catch (e) {
46 | const end = new Date().getTime();
47 | handleSetFalseLoading(end, begin, e || 'secure-button click error');
48 | }
49 | }
50 | };
51 |
52 | return ;
53 | };
54 |
55 | export default SecureButton;
56 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Select.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { Select } from 'antd';
3 | import { map } from 'lodash';
4 | import { SelectProps } from 'antd/lib/select';
5 |
6 | import { getFieldKeyValue } from '../utils';
7 | import { OptionsProps } from '../ItemsType';
8 | import { useGetOptions } from '../hooks';
9 |
10 | export interface YSelectProps extends OptionsProps, Omit, 'options'> {}
11 |
12 | export default forwardRef((props, ref) => {
13 | const {
14 | postField = 'id',
15 | showField = 'name',
16 | options,
17 | renderOption,
18 | onAddProps,
19 | getOptions,
20 | ...rest
21 | } = props;
22 | const list = useGetOptions();
23 |
24 | const children = map(list, (item, index: number) => {
25 | if (item) {
26 | const _postField = getFieldKeyValue(item, index, postField);
27 | const _showField = getFieldKeyValue(item, index, showField);
28 | return (
29 |
35 | {/* 如果有 renderOption 就渲染 renderOption 如果没有默认用 showField 字段 */}
36 | {renderOption ? renderOption(item) : _showField}
37 |
38 | );
39 | }
40 | });
41 | return (
42 |
45 | );
46 | });
47 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Space.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { map, isArray } from 'lodash';
3 | import { Space } from 'antd';
4 | import { SpaceProps } from 'antd/lib/space';
5 |
6 | import YForm from '..';
7 | import { YFormItemProps } from '../Items';
8 |
9 | export interface YFormSpaceProps extends YFormItemProps {
10 | componentProps?: SpaceProps;
11 | items?: YFormItemProps['children'];
12 | }
13 |
14 | export default (props: YFormSpaceProps['componentProps']) => {
15 | const itemProps = React.useContext(YForm.YFormItemContext) as YFormSpaceProps;
16 | const { items, scenes } = itemProps;
17 |
18 | return (
19 |
20 | {isArray(items)
21 | ? map(items, (item, index) => {
22 | return (
23 |
24 | {[item]}
25 |
26 | );
27 | })
28 | : items}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Submit.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { reverse, mergeWith } from 'lodash';
3 | import { ButtonProps } from 'antd/lib/button';
4 |
5 | import { YForm, mergeWithDom } from '../..';
6 | import { YFormDataSource } from '../Items';
7 | import { YFormSecureButtonProps } from './SecureButton';
8 | import { YFormSpaceProps } from './Space';
9 |
10 | export interface ShowBtns {
11 | showSubmit?: ButtonProps;
12 | showSave?: YFormSecureButtonProps['componentProps'];
13 | showCancel?: ButtonProps;
14 | showEdit?: ButtonProps;
15 | showBack?: ButtonProps;
16 | }
17 |
18 | type showBtns = {
19 | [P in keyof ShowBtns]?: boolean | ShowBtns[P];
20 | };
21 |
22 | export interface YFormSubmitComponentProps {
23 | showBtns?: showBtns | boolean;
24 | reverseBtns?: boolean;
25 | spaceProps?: YFormSpaceProps;
26 | }
27 | export interface YFormSubmitProps {
28 | componentProps?: YFormSubmitComponentProps;
29 | }
30 |
31 | export default (props: YFormSubmitProps['componentProps']) => {
32 | const { showBtns = true, reverseBtns, spaceProps } = props;
33 | const formProps = useContext(YForm.YFormContext);
34 | const itemsProps = useContext(YForm.YFormItemsContext);
35 | const { form, onSave, submitComponentProps, disabled } = mergeWithDom({}, formProps, itemsProps);
36 | const { getFieldsValue, getFormatFieldsValue } = form;
37 |
38 | const handleOnSave = async (e) => {
39 | e.preventDefault();
40 | if (onSave) {
41 | await onSave(getFormatFieldsValue(getFieldsValue(true)));
42 | }
43 | };
44 |
45 | const _showBtns: ShowBtns = {
46 | showSubmit: { children: '提交', type: 'primary', htmlType: 'submit' },
47 | showSave: { children: '保存', type: 'primary', onClick: handleOnSave },
48 | showCancel: { children: '取消' },
49 | showEdit: { children: '编辑' },
50 | showBack: { children: '返回', type: 'link' },
51 | };
52 | mergeWithDom(_showBtns, submitComponentProps.showBtns);
53 | const { showSubmit, showSave, showCancel, showEdit, showBack } = mergeWith(
54 | _showBtns,
55 | showBtns,
56 | (objValue, srcValue) => {
57 | // boolean 类型则用 objValue
58 | if (typeof srcValue === 'boolean') {
59 | return srcValue ? objValue : srcValue;
60 | }
61 | },
62 | );
63 |
64 | const actionBtns: Record = {
65 | submit: { type: 'button', noStyle: true, isShow: !!showSubmit, componentProps: showSubmit },
66 | save: { type: 'secureButton', noStyle: true, isShow: !!showSave, componentProps: showSave },
67 | cancel: { type: 'button', noStyle: true, isShow: !!showCancel, componentProps: showCancel },
68 | edit: { type: 'button', noStyle: true, isShow: !!showEdit, componentProps: showEdit },
69 | back: { type: 'button', noStyle: true, isShow: !!showBack, componentProps: showBack },
70 | };
71 |
72 | let btns = [];
73 | if (disabled) {
74 | btns = [actionBtns.edit, actionBtns.back];
75 | } else {
76 | btns = [actionBtns.submit, actionBtns.save, actionBtns.cancel];
77 | }
78 | if (reverseBtns) {
79 | btns = reverse(btns);
80 | }
81 | return (
82 |
83 | {[{ type: 'space', ...spaceProps, items: btns }]}
84 |
85 | );
86 | };
87 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { Input } from 'antd';
3 | import { TextAreaProps } from 'antd/lib/input';
4 |
5 | import { calculateStrLength } from '../utils';
6 |
7 | export interface YTextAreaProps extends TextAreaProps {
8 | inputMax?: number;
9 | }
10 |
11 | export default forwardRef((props, ref) => {
12 | const { inputMax, ...rest } = props;
13 | const { value } = rest;
14 | return (
15 |
16 |
17 |
18 | {inputMax && `${calculateStrLength(`${value || ''}`)}/${inputMax}`}
19 |
20 |
21 | );
22 | });
23 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/component/Typography.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { Typography } from 'antd';
3 | import { TextProps } from 'antd/lib/typography/Text';
4 | import { isObject } from 'lodash';
5 |
6 | export default forwardRef void }>(
7 | (props, ref) => {
8 | const { value, onChange, editable = true, ...rest } = props;
9 |
10 | const handleChange = (value: any) => {
11 | if (onChange) {
12 | onChange(value);
13 | }
14 | };
15 | const _props: TextProps = {};
16 | // 如果 editable 为 bool 类型,并且是 true ,就默认追加 form 的 onChange
17 | if (editable === true) {
18 | _props.editable = { onChange: handleChange };
19 | } else if (isObject(editable)) {
20 | _props.editable = { ...editable, onChange: handleChange };
21 | }
22 | React.useImperativeHandle(ref, () => props);
23 | return (
24 |
25 | {value}
26 |
27 | );
28 | },
29 | );
30 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/hooks/index.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState, useEffect } from 'react';
2 | import { get } from 'lodash';
3 |
4 | import YForm from '../index';
5 | import { getParentNameData } from '../utils';
6 | import { OptionsType } from '../ItemsType';
7 |
8 | export const useGetOptions = () => {
9 | const { form } = useContext(YForm.YFormContext);
10 | const { name, reRender, componentProps } = useContext(YForm.YFormItemContext);
11 | const { getOptions, options } = componentProps;
12 |
13 | const [list, setList] = useState();
14 | useEffect(() => {
15 | (async () => {
16 | if (getOptions) {
17 | const parentValue = getParentNameData(form.getFieldsValue(true), name);
18 | const data = await getOptions(
19 | get(parentValue, name),
20 | parentValue,
21 | form.getFieldsValue(true),
22 | );
23 | setList(data);
24 | }
25 | })();
26 | }, [form, getOptions, reRender, name]);
27 | return getOptions ? list : options;
28 | };
29 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/index.less:
--------------------------------------------------------------------------------
1 | // @ant-prefix: ant;
2 | // 引入变量
3 | @import '~antd/lib/style/themes/default.less';
4 |
5 | .yforms {
6 | .one-line {
7 | display: inline-block;
8 | width: 100%;
9 | .@{ant-prefix}-form-item-label {
10 | float: left;
11 | }
12 | transition: all 0.3s;
13 | }
14 | .list {
15 | .padding-icons {
16 | span + span {
17 | padding-left: 8px;
18 | }
19 | padding-left: 8px;
20 | }
21 | .inline-icons {
22 | position: absolute;
23 | // TODO 适用于 input 高度 32px
24 | top: 5px;
25 | }
26 | }
27 |
28 | .dib {
29 | display: inline-block;
30 | }
31 |
32 | .can-input-length {
33 | position: relative;
34 | margin-bottom: 14px;
35 | .length {
36 | color: #999999;
37 | font-size: 12px;
38 | line-height: 1;
39 | position: absolute;
40 | left: 3px;
41 | bottom: 0;
42 | transform: translate(0, 100%);
43 | }
44 | }
45 |
46 | .input-money {
47 | .zh {
48 | font-size: 14px;
49 | }
50 | }
51 |
52 | .form-spin {
53 | text-align: center;
54 | padding: 30px 50px;
55 | margin: 20px 0;
56 | }
57 |
58 | .mb0 {
59 | margin-bottom: 0;
60 | }
61 | .mb5 {
62 | margin-bottom: 5px;
63 | }
64 |
65 | div.@{ant-prefix}-typography-edit-content {
66 | left: 0px;
67 | margin-top: 0px;
68 | margin-bottom: 0;
69 | }
70 | .diff {
71 | .old-value {
72 | background: #fbe9eb;
73 | word-wrap: break-word;
74 | padding: 1px 0;
75 | width: 100%;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/index.ts:
--------------------------------------------------------------------------------
1 | import YForm, { Config, useFormatFieldsValue } from './Form';
2 | import Items from './Items';
3 | import useSubmit from './useSubmit';
4 | import useForm from './useForm';
5 |
6 | import './index.less';
7 | import { YFormItemsContext, YFormContext, YFormListContent, YFormItemContext } from './Context';
8 | import FormModal from './FormModal';
9 | import Item from './Item';
10 |
11 | type InternalYForm = typeof YForm;
12 | interface RefYForm extends InternalYForm {
13 | Config: typeof Config;
14 | Items: typeof Items;
15 | Item: typeof Item;
16 | useForm: typeof useForm;
17 | useFormatFieldsValue: typeof useFormatFieldsValue;
18 | useSubmit: typeof useSubmit;
19 | YFormContext: typeof YFormContext;
20 | YFormItemContext: typeof YFormItemContext;
21 | YFormItemsContext: typeof YFormItemsContext;
22 | ListContent: typeof YFormListContent;
23 | FormModal: typeof FormModal;
24 | }
25 |
26 | const Form: RefYForm = YForm as RefYForm;
27 |
28 | Form.Config = Config;
29 | Form.Items = Items;
30 | Form.Item = Item;
31 | Form.useForm = useForm;
32 | Form.useFormatFieldsValue = useFormatFieldsValue;
33 | Form.useSubmit = useSubmit;
34 | Form.YFormContext = YFormContext;
35 | Form.YFormItemContext = YFormItemContext;
36 | Form.YFormItemsContext = YFormItemsContext;
37 | Form.ListContent = YFormListContent;
38 | Form.FormModal = FormModal;
39 |
40 | export default Form;
41 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/scenes.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { forEach } from 'lodash';
3 |
4 | import { YFormConfig } from './Form';
5 | import { modifyType } from './ItemsType';
6 | import { replaceMessage, getLabelLayout, mergeWithDom } from './utils';
7 | import DiffDom from './component/Diff';
8 | import { DiffSetFields } from './scenesComps';
9 |
10 | // TODO 以下判断是如果有 name 并且不是 list 类型才当做为表单字段从而注入 view diff 等功能
11 | // itemProps.name && typeProps.type !== 'list'
12 | const scenes: YFormConfig = {
13 | getScene: {
14 | // 没有 label 也和有 label 对齐
15 | labelLayout: {
16 | item: ({ formProps, itemsProps, itemProps }) => {
17 | let _itemProps: modifyType['itemProps'] = {};
18 | const { label } = itemProps;
19 | const _base = mergeWithDom({}, formProps, itemsProps, itemProps);
20 |
21 | const { labelCol, wrapperCol, offset } = _base;
22 | const { noLabelLayoutValue, labelLayoutValue } = getLabelLayout({
23 | labelCol,
24 | wrapperCol,
25 | offset,
26 | });
27 | _itemProps = label ? labelLayoutValue : noLabelLayoutValue;
28 | return {
29 | itemProps: { ..._itemProps, ...itemProps },
30 | };
31 | },
32 | },
33 | // 移除 Col 栅格
34 | noCol: {
35 | item: ({ itemProps }) => {
36 | return { itemProps: { ...itemProps, labelCol: {}, wrapperCol: {} } };
37 | },
38 | },
39 | // 判断如果是 required 则每个 item 添加 rules
40 | required: {
41 | item: ({ itemProps, typeProps }) => {
42 | const _itemProps: modifyType['itemProps'] = {};
43 | const { label, hideLable, rules } = itemProps;
44 | const { formatStr } = mergeWithDom({}, typeProps, itemProps);
45 | const _label = label || hideLable;
46 | const _message =
47 | typeof _label === 'string' && replaceMessage(formatStr || '', { label: _label });
48 | if (itemProps.name && typeProps.type && typeProps.type !== 'list') {
49 | let hasRequired = false;
50 | forEach(rules, (item) => {
51 | if ('required' in item) {
52 | hasRequired = true;
53 | }
54 | });
55 | if (!hasRequired) {
56 | _itemProps.rules = [
57 | { required: true, message: _message || '此处不能为空' },
58 | ...(itemProps.rules || []),
59 | ];
60 | }
61 | }
62 | return {
63 | itemProps: { ...itemProps, ..._itemProps },
64 | };
65 | },
66 | },
67 | // 添加 placeholder
68 | placeholder: {
69 | item: ({ itemProps, componentProps, typeProps }) => {
70 | const _componentProps: modifyType['componentProps'] = {};
71 | const { label, hideLable } = itemProps;
72 | const { formatStr } = mergeWithDom({}, typeProps, itemProps);
73 | const _label = label || hideLable;
74 | const _message =
75 | typeof _label === 'string' && replaceMessage(formatStr || '', { label: _label });
76 | if (itemProps.name && typeProps.type && typeProps.type !== 'list') {
77 | // rangePicker 不需要设置 placeholder
78 | if (typeProps.type !== 'rangePicker' && _message) {
79 | _componentProps.placeholder = _message;
80 | }
81 | }
82 | return {
83 | componentProps: { ..._componentProps, ...componentProps },
84 | };
85 | },
86 | },
87 | // 判断 disabled 给每个 item componentProps 添加 disabled
88 | disabled: {
89 | item: ({ componentProps, itemsProps }) => {
90 | const { disabled } = itemsProps;
91 | let _componentProps = {};
92 | _componentProps = { disabled };
93 | return {
94 | componentProps: { ..._componentProps, ...componentProps },
95 | };
96 | },
97 | },
98 | // 查看情况下每个 item 使用 view 类型渲染
99 | view: {
100 | item: ({ itemProps, typeProps, componentProps }) => {
101 | let _itemProps;
102 | let _componentProps;
103 | if (itemProps.name && typeProps.type !== 'list') {
104 | // 使用 ComponentView 组件渲染
105 | _itemProps = { type: 'view' };
106 | }
107 | let hasRequired = false;
108 | forEach(itemProps.rules, (item) => {
109 | if ('required' in item) {
110 | hasRequired = item.required;
111 | }
112 | });
113 | return {
114 | // 清空 rules ,避免提交会校验
115 | // 如果之前是必填的,这里则保留红色 *
116 | itemProps: {
117 | ...itemProps,
118 | ..._itemProps,
119 | className: 'mb5',
120 | rules: [],
121 | required: hasRequired,
122 | },
123 | componentProps: { ...componentProps, ..._componentProps },
124 | };
125 | },
126 | },
127 | diff: {
128 | item: (props) => {
129 | const { itemProps, typeProps } = props;
130 |
131 | let _itemProps;
132 | if (typeProps.type === 'list') {
133 | _itemProps = {
134 | addonBefore: [itemProps.addonBefore, ],
135 | };
136 | }
137 | if (itemProps.name && typeProps.type !== 'list') {
138 | _itemProps = {
139 | addonAfter: [itemProps.addonAfter, ],
140 | };
141 | }
142 | return { itemProps: { ...itemProps, ..._itemProps } };
143 | },
144 | },
145 | // 搜索场景
146 | search: {
147 | form: ({ formProps }) => {
148 | const { form } = formProps;
149 | return {
150 | formProps: {
151 | // 搜索成功后不重置表单
152 | onCancel: ({ type }) => {
153 | if (type === 'onCancel') {
154 | form.resetFields();
155 | }
156 | },
157 | ...formProps,
158 | },
159 | };
160 | },
161 | items: ({ itemsProps }) => {
162 | // 字段样式去掉
163 | const _itemProps = { ...itemsProps };
164 | mergeWithDom(_itemProps, { noStyle: true, scenes: { noCol: true, required: false } });
165 | return { itemsProps: _itemProps };
166 | },
167 | item: ({ itemProps, componentProps, typeProps }) => {
168 | let _componentProps = {};
169 | if (typeProps.type !== 'rangePicker') {
170 | _componentProps = { ..._componentProps, placeholder: itemProps.label };
171 | }
172 | if (typeProps.type === 'submit') {
173 | _componentProps = {
174 | showBtns: { showSubmit: { children: '搜索' }, showCancel: { children: '重置' } },
175 | };
176 | }
177 | return {
178 | itemProps: { style: { marginRight: 10 }, ...itemProps, label: undefined },
179 | componentProps: mergeWithDom(componentProps, _componentProps),
180 | };
181 | },
182 | },
183 | },
184 | };
185 |
186 | export default scenes;
187 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/scenesComps.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { concat, get } from 'lodash';
3 |
4 | import { YForm } from '..';
5 | import { modifyType } from './ItemsType';
6 | import { YFormItemsProps, YFormItemProps } from './Items';
7 |
8 | const DiffSetFieldsChildren = (props: {
9 | value: any[];
10 | oldValue: any[];
11 | name: YFormItemProps['name'];
12 | form: YFormItemsProps['form'];
13 | }) => {
14 | const {
15 | name,
16 | value = [],
17 | form: { setFields },
18 | oldValue = [],
19 | } = props;
20 | const diffLength = oldValue.length - value.length;
21 | useEffect(() => {
22 | if (diffLength > 0) {
23 | setFields([{ name, value: concat(value, new Array(diffLength).fill(null)) }]);
24 | }
25 | }, [name, diffLength, value, setFields]);
26 | return null;
27 | };
28 | const DiffSetFields = (props: modifyType) => {
29 | const { itemProps, formProps } = props;
30 | const { initialValues, oldValues = {} } = formProps;
31 | const context = React.useContext(YForm.ListContent);
32 | const { name } = itemProps;
33 | const allName = context.prefixName ? concat(context.prefixName, name) : name;
34 |
35 | const value = get(initialValues, allName, []);
36 | const oldValue = 'oldValue' in itemProps ? itemProps.oldValue : get(oldValues, allName);
37 | return (
38 |
39 | {[
40 | {
41 | noStyle: true,
42 | shouldUpdate: (prevValues, curValues) => {
43 | return get(prevValues, allName, []).length !== get(curValues, allName, []).length;
44 | },
45 | children: (form) => (
46 |
47 | ),
48 | },
49 | ]}
50 |
51 | );
52 | };
53 |
54 | export { DiffSetFields };
55 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/useForm.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from 'antd';
2 | import { YFormInstance } from './Form';
3 |
4 | const useForm = (form?: YFormInstance): [YFormInstance] => {
5 | const [warpForm] = Form.useForm(form);
6 | return [warpForm as YFormInstance];
7 | };
8 |
9 | export default useForm;
10 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/useSubmit.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { ParamsObjType, ParamsType } from './Form';
3 | import { paramsType } from './utils';
4 |
5 | export interface YFormUseSubmitProps {
6 | disabled?: boolean;
7 | params?: ParamsType;
8 | }
9 |
10 | export interface YFormUseSubmitReturnProps {
11 | submit: { params: ParamsObjType; onDisabled?: (disabled?: boolean) => void; disabled?: boolean };
12 | }
13 |
14 | export default (props: YFormUseSubmitProps): YFormUseSubmitReturnProps => {
15 | const { params, disabled } = props || {};
16 | const paramsObj = paramsType(params);
17 | const { view } = paramsObj;
18 | // 同 Form 使用 view 当默认值
19 | const [thisDisabled, setThisDisabled] = useState('disabled' in props ? disabled : view);
20 |
21 | return { submit: { disabled: thisDisabled, params: paramsObj, onDisabled: setThisDisabled } };
22 | };
23 |
--------------------------------------------------------------------------------
/packages/yforms/src/YForm/utils.ts:
--------------------------------------------------------------------------------
1 | import { isValidElement, useRef } from 'react';
2 | import { isImmutable } from 'immutable';
3 | import {
4 | get,
5 | map,
6 | join,
7 | set,
8 | mapKeys,
9 | forEach,
10 | sortBy,
11 | isArray,
12 | find,
13 | isEqual,
14 | mergeWith,
15 | } from 'lodash';
16 | import { ColProps } from 'antd/lib/col';
17 |
18 | import warning from 'warning';
19 | import { stringAndFunc } from './ItemsType';
20 | import { KeyValue, ParamsType } from './Form';
21 | import { FormatFieldsValue, YFormItemProps } from './Items';
22 |
23 | const nzhcn = require('nzh/cn');
24 |
25 | export const layoutMore = {
26 | labelCol: { xs: { span: 24 }, sm: { span: 4 } },
27 | wrapperCol: { xs: { span: 24 }, sm: { span: 20 } },
28 | };
29 | export const tailLayout = { wrapperCol: { offset: 8, span: 16 } };
30 |
31 | export function replaceMessage(template: string, kv: Record): string {
32 | return template.replace(/\$\{\w+\}/g, (str: string) => {
33 | const key = str.slice(2, -1);
34 | return kv[key];
35 | });
36 | }
37 |
38 | export const mergeWithDom = (obj: any, ...params: any[]) => {
39 | return mergeWith(obj, ...params, (_, srcValue) => {
40 | // 如果是元素则返回要更改的值,不是则不处理
41 | if (isValidElement(srcValue)) {
42 | return srcValue;
43 | }
44 | // 如果是不可变数据,不处理合并
45 | if (isImmutable(srcValue)) {
46 | return srcValue;
47 | }
48 | });
49 | };
50 |
51 | // 获取一行多组件的 width
52 | export const oneLineItemStyle = (list?: (number | string)[]) => {
53 | if (!list || !Array.isArray(list)) return [];
54 | const _list: { display: string; width: string }[] = [];
55 | let width = 0;
56 | let count = 0;
57 | list.forEach((item) => {
58 | if (typeof item === 'number') {
59 | width += item;
60 | } else {
61 | count += 1;
62 | }
63 | });
64 |
65 | list.forEach((item) => {
66 | if (typeof item === 'number') {
67 | _list.push({ display: 'inline-block', width: `${item}px` });
68 | } else {
69 | _list.push({ display: 'inline-block', width: `calc(${item} - ${width / count}px)` });
70 | }
71 | });
72 | return _list;
73 | };
74 |
75 | export function getFieldKeyValue(record: T, index: number, field: stringAndFunc) {
76 | const recordKey = typeof field === 'function' ? field(record, index) : get(record, field);
77 | return recordKey === undefined ? index : recordKey;
78 | }
79 |
80 | export const searchSelect = {
81 | allowClear: true,
82 | showSearch: true,
83 | optionFilterProp: 'children',
84 | filterOption: (input: string, option: any) => {
85 | const getValue = (dom: React.ReactNode): string => {
86 | const _value = get(dom, 'props.children');
87 | if (Array.isArray(_value)) {
88 | const d = map(_value, (item) => {
89 | if (isValidElement(item)) {
90 | return getValue(item);
91 | }
92 | return item;
93 | });
94 | return join(d, '');
95 | }
96 | if (isValidElement(_value)) {
97 | return getValue(_value);
98 | }
99 | return join(_value, '');
100 | };
101 |
102 | const str = getValue(option);
103 | return str.toLowerCase().indexOf(input.toLowerCase()) >= 0;
104 | },
105 | };
106 |
107 | // jiesuan 项目中使用的计算中文、全角字符 x2
108 | export const calculateStrLength = (name?: string | number): number => {
109 | if (name === null || name === void 0) return 0;
110 | if (typeof name === 'number') {
111 | // eslint-disable-next-line no-param-reassign
112 | name = `${name}`;
113 | }
114 | let count = 0;
115 | const strArr = Array.from(name);
116 | strArr.forEach((c) => {
117 | // eslint-disable-next-line no-control-regex
118 | if (/[\x00-\xff]/.test(c)) {
119 | count += 1;
120 | } else {
121 | count += 2;
122 | }
123 | });
124 | return count;
125 | };
126 |
127 | export const convertMoney = (money: string | number) => {
128 | return nzhcn.encodeB(money, { tenMin: true });
129 | };
130 |
131 | interface NoLabelLayoutValueProps {
132 | labelCol?: ColProps;
133 | wrapperCol?: ColProps;
134 | offset?: number;
135 | }
136 | // 处理 label 宽度
137 | export const getLabelLayout = ({ labelCol, wrapperCol, offset = 0 }: NoLabelLayoutValueProps) => {
138 | const labelLayoutValue = {};
139 | const noLabelLayoutValue = {};
140 | const labelSpan = get(labelCol, 'span');
141 | const wrapperSpan = get(wrapperCol, 'span');
142 | if (labelSpan) {
143 | set(labelLayoutValue, ['labelCol', 'span'], Number(labelSpan) + offset);
144 | set(labelLayoutValue, ['wrapperCol', 'span'], Number(wrapperSpan) - offset);
145 | set(noLabelLayoutValue, ['wrapperCol', 'offset'], Number(labelSpan) + offset);
146 | set(noLabelLayoutValue, ['wrapperCol', 'span'], Number(wrapperSpan) - offset);
147 | } else {
148 | mapKeys(labelCol, (value, key) => {
149 | set(labelLayoutValue, ['labelCol', key, 'span'], value.span + offset);
150 | set(noLabelLayoutValue, ['wrapperCol', key, 'offset'], value.span + offset);
151 | });
152 | mapKeys(wrapperCol, (value, key) => {
153 | set(labelLayoutValue, ['wrapperCol', key, 'span'], value.span - offset);
154 | set(noLabelLayoutValue, ['wrapperCol', key, 'span'], value.span - offset);
155 | });
156 | }
157 |
158 | return { noLabelLayoutValue, labelLayoutValue };
159 | };
160 | // 返回上一级 name 的数据
161 | export const getParentNameData = (values: any, name: YFormItemProps['name']) => {
162 | const _values = { ...values };
163 | const _name = isArray(name) ? name : [name];
164 | if (_name.length === 1) {
165 | return _values;
166 | }
167 | return get(_values, _name.slice(0, _name.length - 1));
168 | };
169 |
170 | export function submitFormatValues(
171 | values: KeyValue,
172 | formatFieldsValue?: FormatFieldsValue[],
173 | ): KeyValue {
174 | const _values = mergeWithDom({}, values);
175 | const list: FormatFieldsValue[] = sortBy(formatFieldsValue, (item) => {
176 | if (!item) return;
177 | if (isArray(item.name)) {
178 | return -item.name.length;
179 | }
180 | return -`${item.name}`.length;
181 | }).filter((x) => x);
182 | forEach(list, (item) => {
183 | const { name, format } = item;
184 | if (name && format) {
185 | const parentValue = getParentNameData(values, name);
186 | // 如果上一级是 undefined,则不处理该字段。(List add 会生成空对象)
187 | if (parentValue === undefined) return;
188 | try {
189 | set(_values, name, format(get(values, name), parentValue, values));
190 | } catch (error) {
191 | // 如果 format 代码报错这里抛出异常
192 | // eslint-disable-next-line no-console
193 | console.error(error);
194 | warning(false, error);
195 | }
196 | }
197 | });
198 | return _values;
199 | }
200 |
201 | export const onFormatFieldsValue = (formatFieldsValue: FormatFieldsValue[]) => {
202 | return (list: FormatFieldsValue[]) => {
203 | const _formatFields = formatFieldsValue;
204 | forEach(list, (item) => {
205 | // 已存在不再注册
206 | if (!find(_formatFields, { name: item.name })) {
207 | _formatFields.push(item);
208 | }
209 | });
210 | return _formatFields;
211 | };
212 | };
213 |
214 | export const paramsType = (params?: ParamsType) => {
215 | const _params = params || ({} as ParamsType);
216 | const type = {
217 | id: _params.id,
218 | edit: _params.type === 'edit',
219 | create: _params.type === 'create',
220 | view: _params.type === 'view',
221 | };
222 | let typeName = '';
223 | if (type.create) typeName = '新建';
224 | if (type.edit) typeName = '编辑';
225 | if (type.view) typeName = '查看';
226 |
227 | return { ...type, typeName };
228 | };
229 |
230 | export const useImmutableValue = (value: any) => {
231 | const v = useRef(value);
232 | if (!isEqual(value, v.current)) {
233 | v.current = value;
234 | }
235 | return v.current;
236 | };
237 |
--------------------------------------------------------------------------------
/packages/yforms/src/index.tsx:
--------------------------------------------------------------------------------
1 | import './style';
2 |
3 | export { default as YForm } from './YForm';
4 | export { mergeWithDom, oneLineItemStyle } from './YForm/utils';
5 |
--------------------------------------------------------------------------------
/packages/yforms/src/style/index.less:
--------------------------------------------------------------------------------
1 | // @ant-prefix: ant;
2 | // 引入变量
3 | @import '~antd/lib/style/themes/default.less';
4 |
5 | .yforms {
6 | .one-line {
7 | display: inline-block;
8 | width: 100%;
9 | .@{ant-prefix}-form-item-label {
10 | float: left;
11 | }
12 | transition: all 0.3s;
13 | }
14 | .list {
15 | .padding-icons {
16 | span + span {
17 | padding-left: 8px;
18 | }
19 | padding-left: 8px;
20 | }
21 | .inline-icons {
22 | position: absolute;
23 | // TODO 适用于 input 高度 32px
24 | top: 5px;
25 | }
26 | }
27 |
28 | .dib {
29 | display: inline-block;
30 | }
31 |
32 | .can-input-length {
33 | position: relative;
34 | margin-bottom: 14px;
35 | .length {
36 | color: #999999;
37 | font-size: 12px;
38 | line-height: 1;
39 | position: absolute;
40 | left: 3px;
41 | bottom: 0;
42 | transform: translate(0, 100%);
43 | }
44 | }
45 |
46 | .input-money {
47 | .zh {
48 | font-size: 14px;
49 | }
50 | }
51 |
52 | .form-spin {
53 | text-align: center;
54 | padding: 30px 50px;
55 | margin: 20px 0;
56 | }
57 |
58 | .mb0 {
59 | margin-bottom: 0;
60 | }
61 | .mb5 {
62 | margin-bottom: 5px;
63 | }
64 |
65 | div.@{ant-prefix}-typography-edit-content {
66 | left: 0px;
67 | margin-top: 0px;
68 | margin-bottom: 0;
69 | }
70 | .diff {
71 | .old-value {
72 | background: #fbe9eb;
73 | word-wrap: break-word;
74 | padding: 1px 0;
75 | width: 100%;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/yforms/src/style/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.less';
2 |
--------------------------------------------------------------------------------
/packages/yforms/webpack.config.js:
--------------------------------------------------------------------------------
1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './src/style/index.tsx',
6 | devtool: false,
7 | mode: 'production',
8 | output: {
9 | filename: 'yforms.js',
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.less$/,
15 | use: [
16 | { loader: MiniCssExtractPlugin.loader },
17 | { loader: 'css-loader' },
18 | {
19 | loader: 'less-loader',
20 | options: {
21 | lessOptions: {
22 | javascriptEnabled: true,
23 | modifyVars: {
24 | // '@ant-prefix': 'demo'
25 | },
26 | },
27 | },
28 | },
29 | ],
30 | },
31 | { test: /\.tsx?$/, include: /src/, use: { loader: 'babel-loader' } },
32 | ],
33 | },
34 | plugins: [
35 | new CleanWebpackPlugin(),
36 | new MiniCssExtractPlugin({
37 | filename: 'yforms.css',
38 | }),
39 | ],
40 | };
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crazyair/yforms/21881ee841298d3a66281e0c80c42d7868c58254/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crazyair/yforms/21881ee841298d3a66281e0c80c42d7868c58254/public/logo.png
--------------------------------------------------------------------------------
/scripts/getChangeLog.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const { readFileSync } = fs;
4 |
5 | const stringFromFile = (path) => {
6 | return readFileSync(path, `utf8`);
7 | };
8 |
9 | const text = stringFromFile('./docs/changelog.md');
10 | const versionLogs = text;
11 |
12 | const allMatchingWords = versionLogs.split('## [');
13 | const changelog = allMatchingWords[1];
14 | // changelog = changelog.replace(/%/g, '%25');
15 | // changelog = changelog.replace(/\n/g, '%0A');
16 | // changelog = changelog.replace(/\r/g, '%0D');
17 |
18 | const demo = `::set-output name=changelog::## [${changelog}`;
19 | // eslint-disable-next-line no-console
20 | console.log(demo);
21 |
--------------------------------------------------------------------------------
/setupTests.js:
--------------------------------------------------------------------------------
1 | // import '@babel/polyfill';
2 |
3 | /* eslint-disable global-require */
4 | if (typeof window !== 'undefined') {
5 | global.window.resizeTo = (width, height) => {
6 | global.window.innerWidth = width || global.window.innerWidth;
7 | global.window.innerHeight = height || global.window.innerHeight;
8 | global.window.dispatchEvent(new Event('resize'));
9 | };
10 | global.window.scrollTo = () => {};
11 | // ref: https://github.com/ant-design/ant-design/issues/18774
12 | if (!window.matchMedia) {
13 | Object.defineProperty(global.window, 'matchMedia', {
14 | value: jest.fn((query) => ({
15 | matches: query.includes('max-width'),
16 | addListener: () => {},
17 | removeListener: () => {},
18 | })),
19 | });
20 | }
21 | }
22 |
23 | // The built-in requestAnimationFrame and cancelAnimationFrame not working with jest.runFakeTimes()
24 | // https://github.com/facebook/jest/issues/5147
25 | global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
26 | global.cancelAnimationFrame = (cb) => clearTimeout(cb, 0);
27 |
28 | const Enzyme = require('enzyme');
29 |
30 | const Adapter = require('enzyme-adapter-react-16');
31 |
32 | Enzyme.configure({ adapter: new Adapter() });
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "moduleResolution": "node",
5 | "baseUrl": "./packages",
6 | "paths": {
7 | "yforms": ["yforms/src"],
8 | "yforms/lib/*": ["yforms/src/*"],
9 | "yforms/es/*": ["yforms/src/*"]
10 | },
11 | "jsx": "preserve",
12 | "declaration": true,
13 | "skipLibCheck": true,
14 | "esModuleInterop": true
15 | },
16 | "exclude": ["tsconfig.json", "node_modules", "lib", "es", "src/**/__test__/*"],
17 | "include": [
18 | "packages/**/src",
19 | "docs/**/*",
20 | ".eslintrc.js",
21 | "jest.config.js",
22 | "setupTests.js",
23 | "packages/yforms/webpack.config.js"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.png';
3 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [{ "src": "dist/**/*", "use": "@now/static" }],
4 | "routes": [
5 | {
6 | "src": "/(.*[.js|.css])",
7 | "headers": { "Cache-Control": "max-age=63072000" },
8 | "continue": true
9 | },
10 | { "src": "/(.*)", "dest": "/dist/$1" },
11 | { "handle": "filesystem" },
12 | { "src": "/(.*)", "dest": "/dist/index.html" }
13 | ],
14 | "github": {
15 | "enable": false
16 | }
17 | }
18 |
--------------------------------------------------------------------------------