Skip to content

Commit d465376

Browse files
authored
feat: Support FormProvider (#4)
* add context * add demo * clean up * trigger form onFieldsChange * update doc
1 parent b858f93 commit d465376

File tree

7 files changed

+314
-115
lines changed

7 files changed

+314
-115
lines changed

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ But you can still check the type definition [here](https://github.com/react-comp
7171
| fields | Control Form fields status. Only use when in Redux | [FieldData](#fielddata)[] | - |
7272
| form | Set form instance created by `useForm` | [FormInstance](#useform) | `Form.useForm()` |
7373
| initialValues | Initial value of Form | Object | - |
74+
| name | Config name with [FormProvider](#formprovider) | string | - |
7475
| validateMessages | Set validate message template | [ValidateMessages](#validatemessages) | - |
7576
| onFieldsChange | Trigger when any value of Field changed | (changedFields, allFields): void | - |
7677
| onValuesChange | Trigger when any value of Field changed | (changedValues, values): void | - |
@@ -133,6 +134,13 @@ class Demo extends React.Component {
133134
| setFieldsValue | Set fields value | (values) => void |
134135
| validateFields | Trigger fields to validate | (nameList?: [NamePath](#namepath)[], options?: ValidateOptions) => Promise |
135136

137+
## FormProvider
138+
139+
| Prop | Description | Type | Default |
140+
| ---------------- | ----------------------------------------- | ---------------------------------------- | ------- |
141+
| validateMessages | Config global `validateMessages` template | [ValidateMessages](#validatemessages) | - |
142+
| onFormChange | Trigger by named form fields change | (name, { changedFields, forms }) => void | - |
143+
136144
## Interface
137145

138146
### NamePath
@@ -177,8 +185,15 @@ class Demo extends React.Component {
177185

178186
### ValidateMessages
179187

180-
Please ref
181-
182-
| Prop | Type |
183-
| -------- | ---- |
184-
| required | |
188+
Validate Messages provides a list of error template.
189+
You can ref [here](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) for fully default templates.
190+
191+
| Prop | Description |
192+
| ------- | ------------------- |
193+
| enum | Rule `enum` prop |
194+
| len | Rule `len` prop |
195+
| max | Rule `max` prop |
196+
| min | Rule `min` prop |
197+
| name | Field name |
198+
| pattern | Rule `pattern` prop |
199+
| type | Rule `type` prop |

examples/StateForm-context.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* eslint-disable react/prop-types */
2+
3+
import React from 'react';
4+
import StateForm, { FormProvider } from '../src';
5+
import Input from './components/Input';
6+
import LabelField from './components/LabelField';
7+
import { ValidateMessages } from '../src/interface';
8+
9+
const myMessages: ValidateMessages = {
10+
required: '${name} 是必需品',
11+
};
12+
13+
const formStyle: React.CSSProperties = {
14+
padding: '10px 15px',
15+
flex: 'auto',
16+
};
17+
18+
const Form1 = () => {
19+
const [form] = StateForm.useForm();
20+
21+
return (
22+
<StateForm form={form} style={{ ...formStyle, border: '1px solid #000' }} name="first">
23+
<h4>Form 1</h4>
24+
<p>Change me!</p>
25+
<LabelField name="username" rules={[{ required: true }]}>
26+
<Input placeholder="username" />
27+
</LabelField>
28+
<LabelField name="password" rules={[{ required: true }]}>
29+
<Input placeholder="password" />
30+
</LabelField>
31+
32+
<button type="submit">Submit</button>
33+
</StateForm>
34+
);
35+
};
36+
37+
const Form2 = () => {
38+
const [form] = StateForm.useForm();
39+
40+
return (
41+
<StateForm form={form} style={{ ...formStyle, border: '1px solid #F00' }} name="second">
42+
<h4>Form 2</h4>
43+
<p>Will follow Form 1 but not sync back</p>
44+
<LabelField name="username" rules={[{ required: true }]}>
45+
<Input placeholder="username" />
46+
</LabelField>
47+
<LabelField name="password" rules={[{ required: true }]}>
48+
<Input placeholder="password" />
49+
</LabelField>
50+
51+
<button type="submit">Submit</button>
52+
</StateForm>
53+
);
54+
};
55+
56+
const Demo = () => {
57+
return (
58+
<div>
59+
<h3>Form Context</h3>
60+
<p>Support global `validateMessages` config and communication between forms.</p>
61+
<FormProvider
62+
validateMessages={myMessages}
63+
onFormChange={(name, { changedFields, forms }) => {
64+
console.log('change from:', name, changedFields, forms);
65+
if (name === 'first') {
66+
forms.second.setFields(changedFields);
67+
}
68+
}}
69+
>
70+
<div style={{ display: 'flex', width: '100%' }}>
71+
<Form1 />
72+
<Form2 />
73+
</div>
74+
</FormProvider>
75+
</div>
76+
);
77+
};
78+
79+
export default Demo;

examples/components/LabelField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const LabelField: React.FunctionComponent<LabelFieldProps> = ({
4040
: React.cloneElement(children as React.ReactElement, { ...control });
4141

4242
return (
43-
<div>
43+
<div style={{ position: 'relative' }}>
4444
<div style={{ display: 'flex', alignItems: 'center' }}>
4545
<label style={{ flex: 'none', width: 100 }}>{label || name}</label>
4646

src/Form.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as React from 'react';
2+
import {
3+
Store,
4+
FormInstance,
5+
FieldData,
6+
ValidateMessages,
7+
Callbacks,
8+
InternalFormInstance,
9+
} from './interface';
10+
import useForm from './useForm';
11+
import FieldContext, { HOOK_MARK } from './FieldContext';
12+
import FormContext, { FormContextProps } from './FormContext';
13+
14+
type BaseFormProps = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
15+
16+
export interface StateFormProps extends BaseFormProps {
17+
initialValues?: Store;
18+
form?: FormInstance;
19+
children?: (() => JSX.Element | React.ReactNode) | React.ReactNode;
20+
fields?: FieldData[];
21+
name?: string;
22+
validateMessages?: ValidateMessages;
23+
onValuesChange?: Callbacks['onValuesChange'];
24+
onFieldsChange?: Callbacks['onFieldsChange'];
25+
onFinish?: (values: Store) => void;
26+
}
27+
28+
const StateForm: React.FunctionComponent<StateFormProps> = (
29+
{
30+
name,
31+
initialValues,
32+
fields,
33+
form,
34+
children,
35+
validateMessages,
36+
onValuesChange,
37+
onFieldsChange,
38+
onFinish,
39+
...restProps
40+
}: StateFormProps,
41+
ref,
42+
) => {
43+
const formContext: FormContextProps = React.useContext(FormContext);
44+
45+
// We customize handle event since Context will makes all the consumer re-render:
46+
// https://reactjs.org/docs/context.html#contextprovider
47+
const [formInstance] = useForm(form);
48+
const {
49+
useSubscribe,
50+
setInitialValues,
51+
setCallbacks,
52+
setValidateMessages,
53+
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
54+
55+
// Pass ref with form instance
56+
React.useImperativeHandle(ref, () => formInstance);
57+
58+
// Register form into Context
59+
React.useEffect(() => {
60+
return formContext.registerForm(name, formInstance);
61+
}, [name]);
62+
63+
// Pass props to store
64+
setValidateMessages({
65+
...formContext.validateMessages,
66+
...validateMessages,
67+
});
68+
setCallbacks({
69+
onValuesChange,
70+
onFieldsChange: (changedFields: FieldData[], ...rest) => {
71+
formContext.triggerFormChange(name, changedFields);
72+
73+
if (onFieldsChange) {
74+
onFieldsChange(changedFields, ...rest);
75+
}
76+
},
77+
});
78+
79+
// Initial store value when first mount
80+
const mountRef = React.useRef(null);
81+
if (!mountRef.current) {
82+
mountRef.current = true;
83+
setInitialValues(initialValues);
84+
}
85+
86+
// Prepare children by `children` type
87+
let childrenNode = children;
88+
const childrenRenderProps = typeof children === 'function';
89+
if (childrenRenderProps) {
90+
const values = formInstance.getFieldsValue();
91+
childrenNode = (children as any)(values, formInstance);
92+
}
93+
94+
// Not use subscribe when using render props
95+
useSubscribe(!childrenRenderProps);
96+
97+
// Listen if fields provided. We use ref to save prev data here to avoid additional render
98+
const prevFieldsRef = React.useRef<FieldData[] | undefined>();
99+
if (prevFieldsRef.current !== fields) {
100+
formInstance.setFields(fields || []);
101+
}
102+
prevFieldsRef.current = fields;
103+
104+
return (
105+
<form
106+
{...restProps}
107+
onSubmit={event => {
108+
event.preventDefault();
109+
event.stopPropagation();
110+
111+
formInstance
112+
.validateFields()
113+
.then(values => {
114+
if (onFinish) {
115+
onFinish(values);
116+
}
117+
})
118+
// Do nothing about submit catch
119+
.catch(e => e);
120+
}}
121+
>
122+
<FieldContext.Provider value={formInstance as InternalFormInstance}>
123+
{childrenNode}
124+
</FieldContext.Provider>
125+
</form>
126+
);
127+
};
128+
129+
export default StateForm;

src/FormContext.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as React from 'react';
2+
import { ValidateMessages, FormInstance, FieldData } from './interface';
3+
4+
interface Forms {
5+
[name: string]: FormInstance;
6+
}
7+
8+
interface FormChangeInfo {
9+
changedFields: FieldData[];
10+
forms: Forms;
11+
}
12+
13+
export interface FormProviderProps {
14+
validateMessages?: ValidateMessages;
15+
onFormChange?: (name: string, info: FormChangeInfo) => void;
16+
}
17+
18+
export interface FormContextProps extends FormProviderProps {
19+
triggerFormChange: (name: string, changedFields: FieldData[]) => void;
20+
registerForm: (name: string, form: FormInstance) => () => void;
21+
}
22+
23+
const FormContext = React.createContext<FormContextProps>({
24+
triggerFormChange: () => {},
25+
registerForm: () => () => {},
26+
});
27+
28+
const FormProvider: React.FunctionComponent<FormProviderProps> = ({
29+
validateMessages,
30+
onFormChange,
31+
children,
32+
}) => {
33+
const formContext = React.useContext(FormContext);
34+
35+
const formsRef = React.useRef<Forms>({});
36+
37+
return (
38+
<FormContext.Provider
39+
value={{
40+
...formContext,
41+
validateMessages,
42+
43+
// =========================================================
44+
// = Global Form Control =
45+
// =========================================================
46+
triggerFormChange: (name, changedFields) => {
47+
if (onFormChange) {
48+
onFormChange(name, {
49+
changedFields,
50+
forms: formsRef.current,
51+
});
52+
}
53+
},
54+
registerForm: (name, form) => {
55+
if (name) {
56+
formsRef.current = {
57+
...formsRef.current,
58+
[name]: form,
59+
};
60+
}
61+
62+
return () => {
63+
const newForms = { ...formsRef.current };
64+
delete newForms[name];
65+
formsRef.current = newForms;
66+
};
67+
},
68+
}}
69+
>
70+
{children}
71+
</FormContext.Provider>
72+
);
73+
};
74+
75+
export { FormProvider };
76+
77+
export default FormContext;

0 commit comments

Comments
 (0)