Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ But you can still check the type definition [here](https://github.com/react-comp
| fields | Control Form fields status. Only use when in Redux | [FieldData](#fielddata)[] | - |
| form | Set form instance created by `useForm` | [FormInstance](#useform) | `Form.useForm()` |
| initialValues | Initial value of Form | Object | - |
| name | Config name with [FormProvider](#formprovider) | string | - |
| validateMessages | Set validate message template | [ValidateMessages](#validatemessages) | - |
| onFieldsChange | Trigger when any value of Field changed | (changedFields, allFields): void | - |
| onValuesChange | Trigger when any value of Field changed | (changedValues, values): void | - |
Expand Down Expand Up @@ -133,6 +134,13 @@ class Demo extends React.Component {
| setFieldsValue | Set fields value | (values) => void |
| validateFields | Trigger fields to validate | (nameList?: [NamePath](#namepath)[], options?: ValidateOptions) => Promise |

## FormProvider

| Prop | Description | Type | Default |
| ---------------- | ----------------------------------------- | ---------------------------------------- | ------- |
| validateMessages | Config global `validateMessages` template | [ValidateMessages](#validatemessages) | - |
| onFormChange | Trigger by named form fields change | (name, { changedFields, forms }) => void | - |

## Interface

### NamePath
Expand Down Expand Up @@ -177,8 +185,15 @@ class Demo extends React.Component {

### ValidateMessages

Please ref

| Prop | Type |
| -------- | ---- |
| required | |
Validate Messages provides a list of error template.
You can ref [here](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) for fully default templates.

| Prop | Description |
| ------- | ------------------- |
| enum | Rule `enum` prop |
| len | Rule `len` prop |
| max | Rule `max` prop |
| min | Rule `min` prop |
| name | Field name |
| pattern | Rule `pattern` prop |
| type | Rule `type` prop |
79 changes: 79 additions & 0 deletions examples/StateForm-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* eslint-disable react/prop-types */

import React from 'react';
import StateForm, { FormProvider } from '../src';
import Input from './components/Input';
import LabelField from './components/LabelField';
import { ValidateMessages } from '../src/interface';

const myMessages: ValidateMessages = {
required: '${name} 是必需品',
};

const formStyle: React.CSSProperties = {
padding: '10px 15px',
flex: 'auto',
};

const Form1 = () => {
const [form] = StateForm.useForm();

return (
<StateForm form={form} style={{ ...formStyle, border: '1px solid #000' }} name="first">
<h4>Form 1</h4>
<p>Change me!</p>
<LabelField name="username" rules={[{ required: true }]}>
<Input placeholder="username" />
</LabelField>
<LabelField name="password" rules={[{ required: true }]}>
<Input placeholder="password" />
</LabelField>

<button type="submit">Submit</button>
</StateForm>
);
};

const Form2 = () => {
const [form] = StateForm.useForm();

return (
<StateForm form={form} style={{ ...formStyle, border: '1px solid #F00' }} name="second">
<h4>Form 2</h4>
<p>Will follow Form 1 but not sync back</p>
<LabelField name="username" rules={[{ required: true }]}>
<Input placeholder="username" />
</LabelField>
<LabelField name="password" rules={[{ required: true }]}>
<Input placeholder="password" />
</LabelField>

<button type="submit">Submit</button>
</StateForm>
);
};

const Demo = () => {
return (
<div>
<h3>Form Context</h3>
<p>Support global `validateMessages` config and communication between forms.</p>
<FormProvider
validateMessages={myMessages}
onFormChange={(name, { changedFields, forms }) => {
console.log('change from:', name, changedFields, forms);
if (name === 'first') {
forms.second.setFields(changedFields);
}
}}
>
<div style={{ display: 'flex', width: '100%' }}>
<Form1 />
<Form2 />
</div>
</FormProvider>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule 我们现在是 Field level 的,不太好支持。

</div>
);
};

export default Demo;
2 changes: 1 addition & 1 deletion examples/components/LabelField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const LabelField: React.FunctionComponent<LabelFieldProps> = ({
: React.cloneElement(children as React.ReactElement, { ...control });

return (
<div>
<div style={{ position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<label style={{ flex: 'none', width: 100 }}>{label || name}</label>

Expand Down
129 changes: 129 additions & 0 deletions src/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as React from 'react';
import {
Store,
FormInstance,
FieldData,
ValidateMessages,
Callbacks,
InternalFormInstance,
} from './interface';
import useForm from './useForm';
import FieldContext, { HOOK_MARK } from './FieldContext';
import FormContext, { FormContextProps } from './FormContext';

type BaseFormProps = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;

export interface StateFormProps extends BaseFormProps {
initialValues?: Store;
form?: FormInstance;
children?: (() => JSX.Element | React.ReactNode) | React.ReactNode;
fields?: FieldData[];
name?: string;
validateMessages?: ValidateMessages;
onValuesChange?: Callbacks['onValuesChange'];
onFieldsChange?: Callbacks['onFieldsChange'];
onFinish?: (values: Store) => void;
}

const StateForm: React.FunctionComponent<StateFormProps> = (
{
name,
initialValues,
fields,
form,
children,
validateMessages,
onValuesChange,
onFieldsChange,
onFinish,
...restProps
}: StateFormProps,
ref,
) => {
const formContext: FormContextProps = React.useContext(FormContext);

// We customize handle event since Context will makes all the consumer re-render:
// https://reactjs.org/docs/context.html#contextprovider
const [formInstance] = useForm(form);
const {
useSubscribe,
setInitialValues,
setCallbacks,
setValidateMessages,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);

// Pass ref with form instance
React.useImperativeHandle(ref, () => formInstance);

// Register form into Context
React.useEffect(() => {
return formContext.registerForm(name, formInstance);
}, [name]);

// Pass props to store
setValidateMessages({
...formContext.validateMessages,
...validateMessages,
});
setCallbacks({
onValuesChange,
onFieldsChange: (changedFields: FieldData[], ...rest) => {
formContext.triggerFormChange(name, changedFields);

if (onFieldsChange) {
onFieldsChange(changedFields, ...rest);
}
},
});

// Initial store value when first mount
const mountRef = React.useRef(null);
if (!mountRef.current) {
mountRef.current = true;
setInitialValues(initialValues);
}

// Prepare children by `children` type
let childrenNode = children;
const childrenRenderProps = typeof children === 'function';
if (childrenRenderProps) {
const values = formInstance.getFieldsValue();
childrenNode = (children as any)(values, formInstance);
}

// Not use subscribe when using render props
useSubscribe(!childrenRenderProps);

// Listen if fields provided. We use ref to save prev data here to avoid additional render
const prevFieldsRef = React.useRef<FieldData[] | undefined>();
if (prevFieldsRef.current !== fields) {
formInstance.setFields(fields || []);
}
prevFieldsRef.current = fields;

return (
<form
{...restProps}
onSubmit={event => {
event.preventDefault();
event.stopPropagation();

formInstance
.validateFields()
.then(values => {
if (onFinish) {
onFinish(values);
}
})
// Do nothing about submit catch
.catch(e => e);
}}
>
<FieldContext.Provider value={formInstance as InternalFormInstance}>
{childrenNode}
</FieldContext.Provider>
</form>
);
};

export default StateForm;
77 changes: 77 additions & 0 deletions src/FormContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import { ValidateMessages, FormInstance, FieldData } from './interface';

interface Forms {
[name: string]: FormInstance;
}

interface FormChangeInfo {
changedFields: FieldData[];
forms: Forms;
}

export interface FormProviderProps {
validateMessages?: ValidateMessages;
onFormChange?: (name: string, info: FormChangeInfo) => void;
}

export interface FormContextProps extends FormProviderProps {
triggerFormChange: (name: string, changedFields: FieldData[]) => void;
registerForm: (name: string, form: FormInstance) => () => void;
}

const FormContext = React.createContext<FormContextProps>({
triggerFormChange: () => {},
registerForm: () => () => {},
});

const FormProvider: React.FunctionComponent<FormProviderProps> = ({
validateMessages,
onFormChange,
children,
}) => {
const formContext = React.useContext(FormContext);

const formsRef = React.useRef<Forms>({});

return (
<FormContext.Provider
value={{
...formContext,
validateMessages,

// =========================================================
// = Global Form Control =
// =========================================================
triggerFormChange: (name, changedFields) => {
if (onFormChange) {
onFormChange(name, {
changedFields,
forms: formsRef.current,
});
}
},
registerForm: (name, form) => {
if (name) {
formsRef.current = {
...formsRef.current,
[name]: form,
};
}

return () => {
const newForms = { ...formsRef.current };
delete newForms[name];
formsRef.current = newForms;
};
},
}}
>
{children}
</FormContext.Provider>
);
};

export { FormProvider };

export default FormContext;
Loading