diff --git a/package-lock.json b/package-lock.json
index 8e02db5a..feab4de2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@babel/runtime": "^7.24.4",
"axios": "^0.27.2",
"es6-promise": "^4.2.8",
+ "lodash.clonedeepwith": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.kebabcase": "^4.1.1",
"lodash.throttle": "^4.1.1",
@@ -20686,6 +20687,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.clonedeepwith": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz",
+ "integrity": "sha512-QRBRSxhbtsX1nc0baxSkkK5WlVTTm/s48DSukcGcWZwIyI8Zz+lB+kFiELJXtzfH4Aj6kMWQ1VWW4U5uUDgZMA=="
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -46236,6 +46242,11 @@
"integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==",
"dev": true
},
+ "lodash.clonedeepwith": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz",
+ "integrity": "sha512-QRBRSxhbtsX1nc0baxSkkK5WlVTTm/s48DSukcGcWZwIyI8Zz+lB+kFiELJXtzfH4Aj6kMWQ1VWW4U5uUDgZMA=="
+ },
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
diff --git a/package.json b/package.json
index 258ba661..35a50615 100644
--- a/package.json
+++ b/package.json
@@ -133,6 +133,7 @@
"lodash.debounce": "^4.0.8",
"lodash.kebabcase": "^4.1.1",
"lodash.throttle": "^4.1.1",
+ "lodash.clonedeepwith": "^4.5.0",
"qrcode.react": "^3.1.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
diff --git a/packages/arcodesign/components/form/__test__/__snapshots__/index.spec.js.snap b/packages/arcodesign/components/form/__test__/__snapshots__/index.spec.js.snap
index de6862c0..ec9068b5 100644
--- a/packages/arcodesign/components/form/__test__/__snapshots__/index.spec.js.snap
+++ b/packages/arcodesign/components/form/__test__/__snapshots__/index.spec.js.snap
@@ -1,5 +1,807 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`form demo test form demo: custom-item.md renders correctly 1`] = `
+
+
+
+`;
+
exports[`form demo test form demo: index.md renders correctly 1`] = `
@@ -94,7 +896,7 @@ exports[`form demo test form demo: index.md renders correctly 1`] = `
role="search"
>
+
+
+
+
+
@@ -627,7 +1492,7 @@ exports[`form demo test form demo: index.md renders correctly 1`] = `
>
@@ -819,16 +1684,60 @@ exports[`form demo test form demo: index.md renders correctly 1`] = `
-
+
+
+
+
+
+
@@ -928,7 +1837,7 @@ exports[`form demo test form demo: use-form.md renders correctly 1`] = `
role="search"
>
diff --git a/packages/arcodesign/components/form/__test__/index.spec.js b/packages/arcodesign/components/form/__test__/index.spec.js
index 3bab9c36..591dc17e 100644
--- a/packages/arcodesign/components/form/__test__/index.spec.js
+++ b/packages/arcodesign/components/form/__test__/index.spec.js
@@ -5,6 +5,13 @@ import demoTest from '../../../tests/demoTest';
import mountTest from '../../../tests/mountTest';
import Form, { useForm } from '..';
import Input from '../../input';
+import Switch from '../../switch';
+import Picker from '../../picker';
+import Textarea from '../../textarea';
+import Checkbox from '../../checkbox';
+import Radio from '../../radio';
+import Slider from '../../slider';
+import Rate from '../../rate';
import '@testing-library/jest-dom';
demoTest('form');
@@ -51,6 +58,8 @@ describe('Form input', () => {
const onSubmit = jest.fn();
const onSubmitFailed = jest.fn();
const result = {};
+ const changeValues = {};
+ const innerChangeValues = {};
// eslint-disable-next-line @typescript-eslint/no-shadow
function App({ onSubmit, onSubmitFailed }) {
const [form] = useForm();
@@ -61,6 +70,15 @@ describe('Form input', () => {
initialValues={{ baz: 'baz' }}
onSubmit={onSubmit}
onSubmitFailed={onSubmitFailed}
+ onValuesChange={(changeVal, values) => {
+ Object.assign(changeValues, {
+ change: changeVal,
+ all: values,
+ });
+ }}
+ onChange={val => {
+ Object.assign(innerChangeValues, val);
+ }}
>
{
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Option content 1
+ Option content 2
+ Option content 3
+
+
+
+
+
+
+
+
+
+
+
+
);
}
- render();
+ const { container } = render(
+ ,
+ );
const submitBtn = screen.getByRole('button', { name: 'submit' });
await userEvent.click(submitBtn);
const error = await screen.findByText('required');
@@ -140,6 +228,18 @@ describe('Form input', () => {
await waitFor(() => {
expect(onSubmit).toBeCalled();
});
+ await waitFor(() => {
+ expect(changeValues).toMatchObject({
+ change: {
+ foo: 'foo',
+ bar: 'bar',
+ },
+ all: {
+ foo: 'foo',
+ bar: 'bar',
+ },
+ });
+ });
const btn3 = screen.getByRole('button', { name: 'getValue' });
await userEvent.click(btn3);
await waitFor(() => {
@@ -149,5 +249,18 @@ describe('Form input', () => {
all: { foo: 'foo', bar: 'bar' },
});
});
+
+ const textArea = container.querySelector('textarea');
+ userEvent.type(textArea, '1');
+ await waitFor(() => {
+ expect(innerChangeValues).toMatchObject({
+ comment: '11',
+ });
+ });
+ const btn4 = screen.getByRole('button', { name: 'reset' });
+ await userEvent.click(btn4);
+ await waitFor(() => {
+ expect(result).toMatchObject({ baz: 'baz' });
+ });
});
});
diff --git a/packages/arcodesign/components/form/demo/custom-item.md b/packages/arcodesign/components/form/demo/custom-item.md
new file mode 100644
index 00000000..cd9c1a8c
--- /dev/null
+++ b/packages/arcodesign/components/form/demo/custom-item.md
@@ -0,0 +1,180 @@
+## 自定义表单项 @en{Custom Form Item}
+
+#### 3
+
+```js
+import {
+ Form,
+ Input,
+ Textarea,
+ Switch,
+ DatePicker,
+ Picker,
+ Radio,
+ Button,
+ Checkbox,
+ Toast,
+ ImagePicker,
+ Rate,
+ Slider
+} from '@arco-design/mobile-react';
+import { ValidatorType } from '@arco-design/mobile-utils';
+
+const options = [
+ { label: 'horizontal', value: 'horizontal' },
+ { label: 'vertical', value: 'vertical' },
+];
+
+const genderOptions = [
+ { label: 'male', value: '1' },
+ { label: 'female', value: '2' },
+ { label: 'others', value: '3' },
+
+]
+
+
+const rules = {
+ names: [{
+ type: ValidatorType.Custom,
+ validator: (val, callback) => {
+ if (!val?.firstName) {
+ callback('Please input your first name');
+ } else if (!val?.familyName) {
+ callback('Please input your family name');
+ } else {
+ callback();
+ }
+ },
+ }]
+}
+
+
+function CustomInput(props) {
+ const value = props.value || {};
+
+ const handleChange = (newValue) => {
+ props.onChange && props.onChange(newValue);
+ };
+
+ console.log(value);
+
+ return (
+ {
+ handleChange({ ...value, firstName: v });
+ }}
+ style={{marginRight: '12px'}}
+ /> {
+ handleChange({ ...value, familyName: v });
+ }}
+ />
+ );
+}
+export default function FormDemo() {
+ const formRef = React.useRef();
+ const [layout, setLayout] = React.useState('horizontal');
+ const toSubmit = val => {
+ formRef.current.form.submit();
+ };
+ const onSubmit = (values, result) => {
+ console.log('----submit Successfully', values, result);
+ };
+
+ const onSubmitFailed = (values, errors = [], definedError = {}) => {
+ const firstField = (errors || [])?.[0];
+ if(firstField) {
+ firstField?.dom?.scrollIntoView();
+ }
+ console.log('----submit failed value:', values);
+ console.log('----submit error', errors);
+ };
+
+ const [disable, setDisable] = React.useState(true);
+ const handleClick = e => {
+ e.preventDefault();
+ };
+ const handleInput = (e, value) => {
+ if(/^[0-9]*$/.test(value)) {
+ formRef.current.form.setFieldValue('age', value);
+ } else {
+ formRef.current.form.setFieldValue('age', 0);
+ }
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Option content 1
+ Option content 2
+ Option content 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+```less
+#demo-form {
+ .form-custom-item-name-group {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ }
+}
+
+```
diff --git a/packages/arcodesign/components/form/demo/index.md b/packages/arcodesign/components/form/demo/index.md
index 1f4a9b7c..7e0bae1c 100644
--- a/packages/arcodesign/components/form/demo/index.md
+++ b/packages/arcodesign/components/form/demo/index.md
@@ -56,8 +56,14 @@ export default function FormDemo() {
const toSubmit = val => {
formRef.current.form.submit();
};
+
+ const toReset = () => {
+ formRef.current.form.resetFields();
+ };
const onSubmit = (values, result) => {
console.log('----submit Successfully', values, result);
+ values.pictures.push({ url: 'http://sf1-cdn-tos.toutiaostatic.com/obj/arco-mobile/_static_/large_image_1.jpg' });
+ console.log(formRef.current.form.getFieldsValue());
};
const onSubmitFailed = (values, errors = [], definedError = {}) => {
@@ -88,13 +94,22 @@ export default function FormDemo() {
onSubmit={onSubmit}
onSubmitFailed={onSubmitFailed}
layout={layout}
- initialValues={{ birthday: 1449730183515 }}
+ initialValues={{ birthday: 1449730183515, age: 12 }}
+ onChange={values => {
+ console.log('----onChange', values);
+ }}
+ onValuesChange={values => {
+ console.log('----onValuesChange', values);
+ }}
>
-
+
+
+
+
-
-
+
+
@@ -122,6 +137,19 @@ export default function FormDemo() {
maskClosable={true}
/>
+
+ {
+ if (type === 'second') {
+ return value % 5 === 0;
+ }
+ return true;
+ }}
+ maskClosable
+ />
+
@@ -133,11 +161,37 @@ export default function FormDemo() {
-
+
+
+
+
+
+
+
);
}
```
+
+```less
+#demo-form {
+ .form-custom-item-name-group {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ }
+}
+
+```
diff --git a/packages/arcodesign/components/form/demo/use-form.md b/packages/arcodesign/components/form/demo/use-form.md
index 3a4147fd..b53415b8 100644
--- a/packages/arcodesign/components/form/demo/use-form.md
+++ b/packages/arcodesign/components/form/demo/use-form.md
@@ -48,6 +48,10 @@ export default function FormDemo() {
const toSubmit = val => {
form.submit();
};
+
+ const toReset = () => {
+ form.resetFields();
+ };
const onSubmit = (values, result) => {
console.log('----submit Successfully', values, result);
};
@@ -93,8 +97,21 @@ export default function FormDemo() {
+
);
}
```
+```less
+#demo-form {
+ .form-custom-item-name-group {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ }
+}
+
+```
diff --git a/packages/arcodesign/components/form/form-item.tsx b/packages/arcodesign/components/form/form-item.tsx
index 6d419c05..d271ff0d 100644
--- a/packages/arcodesign/components/form/form-item.tsx
+++ b/packages/arcodesign/components/form/form-item.tsx
@@ -16,15 +16,21 @@ import { GlobalContext } from '../context-provider';
import {
IFieldError,
FieldValue,
- IFormItemContext,
IFormItemInnerProps,
FormItemProps,
ValidateStatus,
FormItemRef,
FormInternalComponentType,
+ ValueChangeType,
+ IFormItemContext,
} from './type';
-import { getErrorAndWarnings, isFieldRequired } from './utils';
+import { getDefaultValueForInterComponent, getErrorAndWarnings, isFieldRequired } from './utils';
import { DefaultDatePickerLinkedContainer, DefaultPickerLinkedContainer } from './linked-container';
+import { BasicInputProps } from '../input/props';
+import { DatePickerProps } from '../date-picker/type';
+import { PickerProps } from '../picker/type';
+import { SwitchProps } from '../switch';
+import { ImagePickerProps } from '../image-picker/type';
interface IFormItemInnerState {
validateStatus: ValidateStatus;
@@ -46,8 +52,8 @@ class FormItemInner extends PureComponent {};
if (props?.initialValue && props.field) {
- const { setInitialValues } = context.form.getInternalHooks();
- setInitialValues({ [props.field]: props.initialValue });
+ const { setInitialValue } = context.form.getInternalHooks();
+ setInitialValue(props.field, props.initialValue);
}
}
@@ -60,16 +66,37 @@ class FormItemInner extends PureComponent {
+ onValueChange = (
+ curValue: FieldValue,
+ preValue: any,
+ info?: { changeType: ValueChangeType },
+ ) => {
this._touched = true;
const { shouldUpdate } = this.props;
if (typeof shouldUpdate === 'function') {
shouldUpdate({ preValue, curValue }) && this.forceUpdate();
return;
}
+
+ if (info?.changeType === ValueChangeType.Reset) {
+ this.props.onValidateStatusChange({
+ errors: [],
+ warnings: [],
+ errorTypes: [],
+ });
+ this._errors = [];
+ }
this.forceUpdate();
};
+ getInitialValue = () => {
+ const { children, displayType } = this.props;
+ const { getInitialValue } = this.context.form.getInternalHooks();
+ const childrenType = displayType || children.type?.displayName;
+ // get user-defined initialValue or if not defined
+ return getInitialValue(this.props.field) ?? getDefaultValueForInterComponent(childrenType);
+ };
+
getFieldError = () => {
return this._errors;
};
@@ -78,14 +105,34 @@ class FormItemInner extends PureComponent => {
+ getAllRuleValidateTriggers = (): string[] => {
+ return (
+ (this.props.rules
+ ?.map(rule => rule.validateTrigger)
+ .flat()
+ .filter(v => !!v) as string[]) || []
+ );
+ };
+
+ validateField = (validateTrigger?: string): Promise => {
const { validateMessages } = this.context;
const { getFieldValue } = this.context.form;
const { field, rules, onValidateStatusChange } = this.props;
const value = getFieldValue(field);
- if (rules?.length && field) {
+ // rules: if validateTrigger is not defined, all rules will be validated
+ // if validateTrigger is defined, only rules with validateTrigger or without rule.validateTrigger will be validated
+ const curRules = validateTrigger
+ ? rules?.filter(rule => {
+ const triggerList: string[] = ([] as string[]).concat(
+ rule.validateTrigger || validateTrigger,
+ );
+ return triggerList.includes(validateTrigger);
+ })
+ : rules;
+
+ if (curRules?.length && field) {
const fieldDom = this.props.getFormItemRef();
- const fieldValidator = new Validator({ [field]: rules }, { validateMessages });
+ const fieldValidator = new Validator({ [field]: curRules }, { validateMessages });
return new Promise(resolve => {
fieldValidator.validate(
{ [field]: value },
@@ -114,26 +161,24 @@ class FormItemInner extends PureComponent {
- const { field } = this.props;
- const { setFieldValue } = this.context.form;
- setFieldValue(field, value);
- this.validateField();
+ const { field, trigger = 'onChange' } = this.props;
+ const { innerSetFieldValue } = this.context.form.getInternalHooks();
+ innerSetFieldValue(field, value);
+ this.validateField(trigger);
};
- innerTriggerFunction = (_, value, ...args) => {
+ innerTriggerFunctionWithValueFirst = (value, ...args) => {
this.setFieldData(value);
const { children, trigger = 'onChange' } = this.props;
if (trigger && children.props?.[trigger]) {
- children.props?.[trigger](_, value, ...args);
+ children.props?.[trigger](value, ...args);
}
};
- innerTriggerFunctionWithValueFirst = (value, ...args) => {
+ innerOnInputFunction = (_, value, ...args) => {
this.setFieldData(value);
- const { children, trigger = 'onChange' } = this.props;
- if (trigger && children.props?.[trigger]) {
- children.props?.[trigger](value, ...args);
- }
+ const { children } = this.props;
+ children.props?.onInput?.(_, value, ...args);
};
innerClearFunction = (...args) => {
@@ -151,80 +196,68 @@ class FormItemInner extends PureComponent {
+ childrenProps[triggerName] = e => {
+ this.validateField(triggerName);
+ children?.props?.[triggerName]?.(e);
+ };
+ });
const childrenType = displayType || children.type?.displayName;
switch (childrenType) {
case FormInternalComponentType.Input:
case FormInternalComponentType.Textarea:
- props = {
- value: getFieldValue(field) || '',
- onInput: this.innerTriggerFunction,
- onClear: this.innerClearFunction,
- disabled: this.props.disabled,
- };
- break;
- case FormInternalComponentType.Checkbox:
- case FormInternalComponentType.Radio:
- case FormInternalComponentType.Slider:
- case FormInternalComponentType.RadioGroup:
- case FormInternalComponentType.CheckboxGroup:
- props = {
- value: getFieldValue(field),
- onChange: this.innerTriggerFunctionWithValueFirst,
- disabled: this.props.disabled,
- };
+ (childrenProps as BasicInputProps).value =
+ getFieldValue(field) || '';
+ (childrenProps as BasicInputProps).onInput =
+ this.innerOnInputFunction;
+ (childrenProps as BasicInputProps).onClear =
+ this.innerClearFunction;
break;
case FormInternalComponentType.DatePicker:
- props = {
- currentTs: getFieldValue(field),
- onChange: this.innerTriggerFunctionWithValueFirst,
- disabled: this.props.disabled,
- renderLinkedContainer:
- children.props?.renderLinkedContainer ||
- ((ts, types) => ),
- };
+ (childrenProps as DatePickerProps).currentTs = getFieldValue(field);
+ (childrenProps as DatePickerProps).onChange =
+ this.innerTriggerFunctionWithValueFirst;
+ (childrenProps as DatePickerProps).renderLinkedContainer =
+ children.props?.renderLinkedContainer ||
+ ((ts, types) => );
break;
case FormInternalComponentType.Picker:
- props = {
- value: getFieldValue(field),
- onChange: this.innerTriggerFunctionWithValueFirst,
- disabled: this.props.disabled,
- renderLinkedContainer:
- children.props?.renderLinkedContainer ||
- (val => ),
- };
+ (childrenProps as PickerProps).value = getFieldValue(field) || '';
+ (childrenProps as PickerProps).onChange = this.innerTriggerFunctionWithValueFirst;
+ (childrenProps as PickerProps).renderLinkedContainer =
+ children.props?.renderLinkedContainer ||
+ (val => );
break;
case FormInternalComponentType.Switch:
- props = {
- checked: Boolean(getFieldValue(field)),
- onChange: this.innerTriggerFunctionWithValueFirst,
- disabled: this.props.disabled,
- };
+ (childrenProps as SwitchProps).checked = Boolean(getFieldValue(field));
+ (childrenProps as SwitchProps).onChange = this.innerTriggerFunctionWithValueFirst;
break;
case FormInternalComponentType.ImagePicker:
- props = {
- images: getFieldValue(field),
- onChange: this.innerTriggerFunctionWithValueFirst,
- disabled: this.props.disabled,
- };
+ (childrenProps as ImagePickerProps).images = getFieldValue(field);
+ (childrenProps as ImagePickerProps).onChange =
+ this.innerTriggerFunctionWithValueFirst;
break;
default:
- const originTrigger = children.props[trigger];
+ if (triggerPropsField) {
+ childrenProps[triggerPropsField] = getFieldValue(field);
+ }
// inject the validated result
- props.error = this._errors;
- props[trigger] = (newValue, ...args: any) => {
- this.setFieldData(newValue);
- originTrigger && originTrigger(newValue, ...args);
- };
+ childrenProps.error = this._errors;
+ childrenProps[trigger] = this.innerTriggerFunctionWithValueFirst;
}
- return React.cloneElement(children, props);
+ return React.cloneElement(children, childrenProps);
}
render() {
@@ -233,6 +266,8 @@ class FormItemInner extends PureComponent) => {
const {
label,
@@ -276,7 +311,6 @@ export default forwardRef((props: FormItemProps, ref: Ref) => {
useImperativeHandle(ref, () => ({
dom: formItemRef.current,
}));
-
return (
) => {
form: formInstance,
children,
onValuesChange,
+ onChange,
onSubmit,
onSubmitFailed,
disabled,
@@ -30,6 +31,7 @@ const Form = forwardRef((props: FormProps, ref: Ref
) => {
onValuesChange,
onSubmit,
onSubmitFailed,
+ onChange,
});
if (!initRef.current) {
diff --git a/packages/arcodesign/components/form/linked-container.tsx b/packages/arcodesign/components/form/linked-container.tsx
index a0ddb324..4bec9326 100644
--- a/packages/arcodesign/components/form/linked-container.tsx
+++ b/packages/arcodesign/components/form/linked-container.tsx
@@ -21,12 +21,15 @@ export function DefaultDatePickerLinkedContainer({
ts,
types,
}: {
- ts: number | [number, number];
+ ts?: number | [number, number];
types: string[];
}) {
const { prefixCls, locale } = useContext(GlobalContext);
const className = `${prefixCls}-form-picker-link-container`;
const dateTimeStr = useMemo(() => {
+ if (ts === undefined) {
+ return '';
+ }
if (typeof ts === 'number') {
return formatDateTimeStr(ts, types);
}
diff --git a/packages/arcodesign/components/form/style/index.less b/packages/arcodesign/components/form/style/index.less
index 06abe000..b9271db9 100644
--- a/packages/arcodesign/components/form/style/index.less
+++ b/packages/arcodesign/components/form/style/index.less
@@ -42,11 +42,16 @@
.@{prefix}-input-wrap, .@{prefix}-input {
padding: 0;
}
+
+ .@{prefix}-input-wrap.textarea {
+ padding: 0;
+ }
+
&-wrapper {
width: 100%;
flex: 1;
position: relative;
- .@{prefix}-input-wrap {
+ .@{prefix}-input-wrap.single-line {
.use-var(height, input-text-line-height);
}
}
diff --git a/packages/arcodesign/components/form/type.ts b/packages/arcodesign/components/form/type.ts
index 1a2575b3..5eb46ad2 100644
--- a/packages/arcodesign/components/form/type.ts
+++ b/packages/arcodesign/components/form/type.ts
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { Promise } from 'es6-promise';
export type FieldValue = any;
-export type FieldItem = Record;
+export type FieldItem = Record;
export type ILayout = 'horizontal' | 'vertical' | 'inline';
// 注意:自动识别form关联组件的依据,请勿轻易改变代码结构
@@ -57,6 +57,11 @@ export interface FormProps {
* @en Callback when the form item value changes
*/
onValuesChange?: Callbacks['onValuesChange'];
+ /**
+ * 表单项数据变化时的回调(仅用户操作表单时触发)
+ * @en Callback when the form item value changes (Only trigger when user operate form)
+ */
+ onChange?: Callbacks['onChange'];
/**
* 表单项数据变化时的回调
* @en Callback when the form is submitted
@@ -123,6 +128,11 @@ export interface Callbacks {
* @en Callback when the form item value changes
*/
onValuesChange?: (changedValues: FieldValue, values: FieldValue) => void;
+ /**
+ * 表单项数据变化时的回调(仅用户操作表单时触发)
+ * @en Callback when the form item value changes (Only trigger when user operate form)
+ */
+ onChange?: (changedValues: FieldValue, values: FieldValue) => void;
/**
* 表单项数据变化时的回调
* @en Callback when the form is submitted
@@ -143,6 +153,10 @@ export interface InternalHooks {
registerField: (name: string, self: any) => () => void;
setInitialValues: (values: FieldItem) => void;
setCallbacks: (callbacks: Callbacks) => void;
+ getInitialValue: (fieldName: string) => FieldValue;
+ setInitialValue: (fieldName: string, values: FieldItem) => void;
+ innerSetFieldsValue: (values: FieldItem) => boolean;
+ innerSetFieldValue: (name: string, value: FieldValue) => boolean;
}
export interface IFormInstance {
@@ -194,6 +208,11 @@ export type InternalFormInstance = IFormInstance & {
* @en Get internal methods
*/
getInternalHooks: () => InternalHooks;
+ /**
+ * 注册表单组件
+ * @en Register Form Item component
+ */
+ registerField: (name: string, self: any) => () => void;
};
export interface FormRef {
@@ -384,3 +403,10 @@ export interface IFormItemInnerProps {
*/
displayType?: FormInternalComponentType;
}
+
+export enum ValueChangeType {
+ /* form update */
+ Update,
+ /* form clear */
+ Reset,
+}
diff --git a/packages/arcodesign/components/form/useForm.ts b/packages/arcodesign/components/form/useForm.ts
index eabdb7a1..a8bf70b2 100644
--- a/packages/arcodesign/components/form/useForm.ts
+++ b/packages/arcodesign/components/form/useForm.ts
@@ -1,11 +1,20 @@
/* eslint-disable no-console */
import { ReactNode, useRef } from 'react';
import { Promise } from 'es6-promise';
-import { Callbacks, IFieldError, FieldItem, IFormInstance } from './type';
+import {
+ Callbacks,
+ IFieldError,
+ FieldItem,
+ IFormInstance,
+ FieldValue,
+ InternalFormInstance,
+ ValueChangeType,
+} from './type';
+import { deepClone } from './utils';
const defaultFunc: any = () => {};
-export const defaultFormDataMethods = {
+export const defaultFormDataMethods: InternalFormInstance = {
getFieldValue: name => name,
getFieldsValue: _names => {
return {};
@@ -29,6 +38,10 @@ export const defaultFormDataMethods = {
registerField: defaultFunc,
setInitialValues: defaultFunc,
setCallbacks: defaultFunc,
+ getInitialValue: defaultFunc,
+ setInitialValue: defaultFunc,
+ innerSetFieldsValue: defaultFunc,
+ innerSetFieldValue: defaultFunc,
};
},
};
@@ -43,7 +56,7 @@ class FormData {
private _callbacks: Callbacks = {};
- setFieldsValue = (values: FieldItem): boolean => {
+ private commonSetFieldsValue = (values: FieldItem, changeType?: ValueChangeType) => {
const oldValues: FieldItem = Object.keys(values).reduce(
(acc, key) => ({
...acc,
@@ -52,32 +65,78 @@ class FormData {
{},
);
this._formData = { ...this._formData, ...values };
+ this.notifyField(values, oldValues, changeType);
+ };
+
+ setFieldsValue = (values: FieldItem): boolean => {
+ this.commonSetFieldsValue(values);
const { onValuesChange } = this._callbacks;
- onValuesChange && onValuesChange(values, this._formData);
- this.notifyField(values, oldValues);
+ onValuesChange?.(values, this._formData);
return true;
};
- setFieldValue = (name: string, value: unknown): boolean => {
+ innerSetFieldsValue = (values: FieldItem, changeType?: ValueChangeType): boolean => {
+ this.commonSetFieldsValue(values, changeType);
+ const { onValuesChange, onChange } = this._callbacks;
+ onValuesChange?.(values, this._formData);
+ onChange?.(values, this._formData);
+ return true;
+ };
+
+ commonSetFieldValue = (name: string, value: unknown, changeType?: ValueChangeType) => {
const oldValues = { [name]: this.getFieldValue(name) };
this._formData = { ...this._formData, [name]: value };
+ this.notifyField({ [name]: value }, oldValues, changeType);
+ };
+
+ setFieldValue = (name: string, value: FieldValue): boolean => {
+ this.commonSetFieldValue(name, value);
const { onValuesChange } = this._callbacks;
onValuesChange &&
onValuesChange(
+ {
+ [name]: value,
+ },
+ this.getFieldsValue(),
+ );
+ return true;
+ };
+
+ innerSetFieldValue = (
+ name: string,
+ value: FieldValue,
+ changeType?: ValueChangeType,
+ ): boolean => {
+ this.commonSetFieldValue(name, value, changeType);
+ const { onValuesChange, onChange } = this._callbacks;
+ onValuesChange &&
+ onValuesChange(
+ {
+ [name]: value,
+ },
+ this._formData,
+ );
+ onChange &&
+ onChange(
{
[name]: value,
},
this._formData,
);
- this.notifyField({ [name]: value }, oldValues);
return true;
};
- notifyField = (values: FieldItem, oldValues: FieldItem): void => {
+ notifyField = (
+ values: FieldItem,
+ oldValues: FieldItem,
+ changeType: ValueChangeType = ValueChangeType.Update,
+ ): void => {
Object.keys(values).map((fieldName: string) => {
const fieldObj = this._fieldsList?.[fieldName] || null;
if (fieldObj) {
- fieldObj.onValueChange(values[fieldName], oldValues[fieldName]);
+ fieldObj.onValueChange(values[fieldName], oldValues[fieldName], {
+ changeType,
+ });
}
});
};
@@ -86,11 +145,11 @@ class FormData {
if (names) {
return names.map(name => this.getFieldValue(name));
}
- return this._formData;
+ return deepClone(this._formData);
};
getFieldValue = (name: string) => {
- return this._formData?.[name];
+ return deepClone(this._formData?.[name]);
};
getFieldError = (name: string): ReactNode[] => {
@@ -122,7 +181,7 @@ class FormData {
registerField = (name: string, self: any) => {
this._fieldsList[name] = self;
- const { initialValue } = (self as any).props;
+ const { initialValue } = self.props;
if (initialValue !== undefined && name) {
this._initialValues = {
...this._initialValues,
@@ -142,13 +201,36 @@ class FormData {
};
};
- setInitialValues = (initVal: Record) => {
- this._initialValues = { ...(initVal || {}) };
+ setInitialValues = (initVal: FieldItem) => {
+ this._initialValues = deepClone(initVal || {});
this.setFieldsValue(initVal);
};
+ setInitialValue = (fieldName: string, value: unknown) => {
+ if (!fieldName) {
+ return;
+ }
+ this._initialValues[fieldName] = value;
+ this.setFieldValue(fieldName, value);
+ };
+
+ getInitialValue = (fieldName: string) => {
+ return this._initialValues[fieldName];
+ };
+
resetFields = () => {
- this.setFieldsValue(this._initialValues);
+ const oldValues = { ...this._formData };
+ const initialValues = {};
+ Object.keys(this._formData).forEach((fieldName: string) => {
+ const fieldObj = this._fieldsList?.[fieldName] || null;
+ if (fieldObj) {
+ initialValues[fieldName] = fieldObj.getInitialValue();
+ }
+ });
+ const { onValuesChange } = this._callbacks;
+ onValuesChange && onValuesChange(initialValues, this._formData);
+ this._formData = initialValues;
+ this.notifyField(initialValues, oldValues, ValueChangeType.Reset);
};
validateFields = () => {
@@ -175,14 +257,14 @@ class FormData {
this.validateFields()
.then(result => {
const { onSubmit } = this._callbacks;
- onSubmit?.(this._formData, result);
+ onSubmit?.(this.getFieldsValue(), result);
})
.catch(e => {
const { onSubmitFailed } = this._callbacks;
if (!onSubmitFailed) {
return;
}
- onSubmitFailed(this._formData, e);
+ onSubmitFailed(this.getFieldsValue(), e);
});
};
@@ -212,6 +294,10 @@ class FormData {
registerField: this.registerField,
setInitialValues: this.setInitialValues,
setCallbacks: this.setCallbacks,
+ getInitialValue: this.getInitialValue,
+ setInitialValue: this.setInitialValue,
+ innerSetFieldsValue: this.innerSetFieldsValue,
+ innerSetFieldValue: this.innerSetFieldValue,
};
};
}
diff --git a/packages/arcodesign/components/form/utils.ts b/packages/arcodesign/components/form/utils.ts
index 2a7e8a8b..28d05072 100644
--- a/packages/arcodesign/components/form/utils.ts
+++ b/packages/arcodesign/components/form/utils.ts
@@ -1,4 +1,6 @@
-import { IRules, ValidatorError } from '@arco-design/mobile-utils';
+import { IRules, ValidatorError, isObject, isArray } from '@arco-design/mobile-utils';
+import cloneDeepWith from 'lodash/cloneDeepWith';
+import { FormInternalComponentType } from './type';
export const isFieldRequired = (rules: IRules[] = []) => {
return (rules || []).some(rule => rule?.required);
@@ -21,3 +23,27 @@ export const getErrorAndWarnings = (result: ValidatorError[]) => {
});
return { warnings, errors, errorTypes };
};
+
+export const getDefaultValueForInterComponent = (componentType: string) => {
+ switch (componentType) {
+ case FormInternalComponentType.Input:
+ case FormInternalComponentType.Textarea:
+ return undefined;
+ case FormInternalComponentType.CheckboxGroup:
+ return [];
+ case FormInternalComponentType.RadioGroup:
+ return null;
+ case FormInternalComponentType.Slider:
+ case FormInternalComponentType.Rate:
+ return 0;
+ default:
+ return undefined;
+ }
+};
+export function deepClone(value) {
+ return cloneDeepWith(value, val => {
+ if (!isObject(val) && !isArray(val)) {
+ return val;
+ }
+ });
+}
diff --git a/packages/arcodesign/components/input/__test__/__snapshots__/index.spec.js.snap b/packages/arcodesign/components/input/__test__/__snapshots__/index.spec.js.snap
index ddc6e494..69c9ab9f 100644
--- a/packages/arcodesign/components/input/__test__/__snapshots__/index.spec.js.snap
+++ b/packages/arcodesign/components/input/__test__/__snapshots__/index.spec.js.snap
@@ -7,7 +7,7 @@ exports[`input demo test input demo: disabled.md renders correctly 1`] = `
role="search"
>