From 17afcb987ca4d1cc66407c11332ba26fa77ce7af Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 11 Jun 2019 20:04:57 +0800 Subject: [PATCH 1/5] add context --- examples/StateForm-context.tsx | 62 ++++++++++++++++++ src/Form.tsx | 109 ++++++++++++++++++++++++++++++++ src/FormContext.ts | 8 +++ src/index.tsx | 111 ++------------------------------- 4 files changed, 183 insertions(+), 107 deletions(-) create mode 100644 examples/StateForm-context.tsx create mode 100644 src/Form.tsx create mode 100644 src/FormContext.ts diff --git a/examples/StateForm-context.tsx b/examples/StateForm-context.tsx new file mode 100644 index 00000000..4f95cb9c --- /dev/null +++ b/examples/StateForm-context.tsx @@ -0,0 +1,62 @@ +/* eslint-disable react/prop-types */ + +import React from 'react'; +import StateForm, { FormInstance } from '../src'; +import Input from './components/Input'; +import LabelField from './components/LabelField'; + +const formStyle: React.CSSProperties = { + padding: '10px 15px', + flex: 'auto', +}; + +const Form1 = () => { + const [form] = StateForm.useForm(); + + return ( + +

Form 1

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

Form 2

+ + + + + + + + +
+ ); +}; + +const Demo = () => { + return ( +
+

Form Context

+

Support global `validateMessages` config and communication between forms.

+
+ + +
+
+ ); +}; + +export default Demo; diff --git a/src/Form.tsx b/src/Form.tsx new file mode 100644 index 00000000..e4744e22 --- /dev/null +++ b/src/Form.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { + Store, + FormInstance, + FieldData, + ValidateMessages, + Callbacks, + InternalFormInstance, +} from './interface'; +import useForm from './useForm'; +import FieldContext, { HOOK_MARK } from './FieldContext'; + +type BaseFormProps = Omit, 'onSubmit'>; + +export interface StateFormProps extends BaseFormProps { + initialValues?: Store; + form?: FormInstance; + children?: (() => JSX.Element | React.ReactNode) | React.ReactNode; + fields?: FieldData[]; + validateMessages?: ValidateMessages; + onValuesChange?: Callbacks['onValuesChange']; + onFieldsChange?: Callbacks['onFieldsChange']; + onFinish?: (values: Store) => void; +} + +const StateForm: React.FunctionComponent = ( + { + initialValues, + fields, + form, + children, + validateMessages, + onValuesChange, + onFieldsChange, + onFinish, + ...restProps + }: StateFormProps, + ref, +) => { + // 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 props to store + setValidateMessages(validateMessages); + setCallbacks({ + onValuesChange, + onFieldsChange, + }); + + // 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); + + // Pass ref with form instance + React.useImperativeHandle(ref, () => formInstance); + + // Listen if fields provided. We use ref to save prev data here to avoid additional render + const prevFieldsRef = React.useRef(); + if (prevFieldsRef.current !== fields) { + formInstance.setFields(fields || []); + } + prevFieldsRef.current = fields; + + return ( +
{ + event.preventDefault(); + event.stopPropagation(); + + formInstance + .validateFields() + .then(values => { + if (onFinish) { + onFinish(values); + } + }) + // Do nothing about submit catch + .catch(e => e); + }} + > + + {childrenNode} + +
+ ); +}; + +export default StateForm; diff --git a/src/FormContext.ts b/src/FormContext.ts new file mode 100644 index 00000000..22f7711a --- /dev/null +++ b/src/FormContext.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { ValidateMessages } from './interface'; + +interface FormContextProps { + validateMessages?: ValidateMessages; +} + +const Context = React.createContext({}); diff --git a/src/index.tsx b/src/index.tsx index 89896610..a5661c24 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,114 +1,11 @@ import * as React from 'react'; -import { - Callbacks, - FieldData, - FormInstance, - Store, - ValidateMessages, - InternalFormInstance, -} from './interface'; -import FieldContext, { HOOK_MARK } from './FieldContext'; +import { FormInstance } from './interface'; import Field from './Field'; import List from './List'; import useForm from './useForm'; +import FieldForm, { StateFormProps } from './Form'; -type BaseFormProps = Omit, 'onSubmit'>; - -export interface StateFormProps extends BaseFormProps { - initialValues?: Store; - form?: FormInstance; - children?: (() => JSX.Element | React.ReactNode) | React.ReactNode; - fields?: FieldData[]; - validateMessages?: ValidateMessages; - onValuesChange?: Callbacks['onValuesChange']; - onFieldsChange?: Callbacks['onFieldsChange']; - onFinish?: (values: Store) => void; -} - -const StateForm: React.FunctionComponent = ( - { - initialValues, - fields, - form, - children, - validateMessages, - onValuesChange, - onFieldsChange, - onFinish, - ...restProps - }: StateFormProps, - ref, -) => { - // 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 props to store - setValidateMessages(validateMessages); - setCallbacks({ - onValuesChange, - onFieldsChange, - }); - - // 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); - - // Pass ref with form instance - React.useImperativeHandle(ref, () => formInstance); - - // Listen if fields provided. We use ref to save prev data here to avoid additional render - const prevFieldsRef = React.useRef(); - if (prevFieldsRef.current !== fields) { - formInstance.setFields(fields || []); - } - prevFieldsRef.current = fields; - - return ( -
{ - event.preventDefault(); - event.stopPropagation(); - - formInstance - .validateFields() - .then(values => { - if (onFinish) { - onFinish(values); - } - }) - // Do nothing about submit catch - .catch(e => e); - }} - > - - {childrenNode} - -
- ); -}; - -const InternalStateForm = React.forwardRef(StateForm); +const InternalStateForm = React.forwardRef(FieldForm); type InternalStateForm = typeof InternalStateForm; interface RefStateForm extends InternalStateForm { @@ -123,6 +20,6 @@ RefStateForm.Field = Field; RefStateForm.List = List; RefStateForm.useForm = useForm; -export { FormInstance, Field }; +export { FormInstance, Field, useForm }; export default RefStateForm; From 71f47e5e03d278fd160132fe14475832500442f1 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 11 Jun 2019 22:38:37 +0800 Subject: [PATCH 2/5] add demo --- examples/StateForm-context.tsx | 31 +++++++++++---- src/Form.tsx | 32 +++++++++++++--- src/FormContext.ts | 8 ---- src/FormContext.tsx | 69 ++++++++++++++++++++++++++++++++++ src/index.tsx | 5 ++- src/useForm.ts | 1 - 6 files changed, 124 insertions(+), 22 deletions(-) delete mode 100644 src/FormContext.ts create mode 100644 src/FormContext.tsx diff --git a/examples/StateForm-context.tsx b/examples/StateForm-context.tsx index 4f95cb9c..e990043d 100644 --- a/examples/StateForm-context.tsx +++ b/examples/StateForm-context.tsx @@ -1,9 +1,14 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import StateForm, { FormInstance } from '../src'; +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', @@ -14,8 +19,9 @@ const Form1 = () => { const [form] = StateForm.useForm(); return ( - +

Form 1

+

Change me!

@@ -32,8 +38,9 @@ const Form2 = () => { const [form] = StateForm.useForm(); return ( - +

Form 2

+

Will follow Form 1 but not sync back

@@ -51,10 +58,20 @@ const Demo = () => {

Form Context

Support global `validateMessages` config and communication between forms.

-
- - -
+ { + console.log('change from:', name, forms); + if (name === 'first') { + forms.second.setFieldsValue(forms.first.getFieldsValue()); + } + }} + > +
+ + +
+
); }; diff --git a/src/Form.tsx b/src/Form.tsx index e4744e22..3fc1e622 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -9,6 +9,7 @@ import { } from './interface'; import useForm from './useForm'; import FieldContext, { HOOK_MARK } from './FieldContext'; +import FormContext, { FormContextProps } from './FormContext'; type BaseFormProps = Omit, 'onSubmit'>; @@ -17,6 +18,7 @@ export interface StateFormProps extends BaseFormProps { form?: FormInstance; children?: (() => JSX.Element | React.ReactNode) | React.ReactNode; fields?: FieldData[]; + name?: string; validateMessages?: ValidateMessages; onValuesChange?: Callbacks['onValuesChange']; onFieldsChange?: Callbacks['onFieldsChange']; @@ -25,6 +27,7 @@ export interface StateFormProps extends BaseFormProps { const StateForm: React.FunctionComponent = ( { + name, initialValues, fields, form, @@ -37,6 +40,8 @@ const StateForm: React.FunctionComponent = ( }: 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); @@ -47,10 +52,29 @@ const StateForm: React.FunctionComponent = ( 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(validateMessages); + setValidateMessages({ + ...formContext.validateMessages, + ...validateMessages, + }); setCallbacks({ - onValuesChange, + onValuesChange: (...args) => { + if (name) { + formContext.triggerFormChange(name, formInstance); + } + + if (onValuesChange) { + onValuesChange(...args); + } + }, onFieldsChange, }); @@ -68,12 +92,10 @@ const StateForm: React.FunctionComponent = ( const values = formInstance.getFieldsValue(); childrenNode = (children as any)(values, formInstance); } + // Not use subscribe when using render props useSubscribe(!childrenRenderProps); - // Pass ref with form instance - React.useImperativeHandle(ref, () => formInstance); - // Listen if fields provided. We use ref to save prev data here to avoid additional render const prevFieldsRef = React.useRef(); if (prevFieldsRef.current !== fields) { diff --git a/src/FormContext.ts b/src/FormContext.ts deleted file mode 100644 index 22f7711a..00000000 --- a/src/FormContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react'; -import { ValidateMessages } from './interface'; - -interface FormContextProps { - validateMessages?: ValidateMessages; -} - -const Context = React.createContext({}); diff --git a/src/FormContext.tsx b/src/FormContext.tsx new file mode 100644 index 00000000..87759b82 --- /dev/null +++ b/src/FormContext.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { ValidateMessages, FormInstance } from './interface'; + +interface Forms { + [name: string]: FormInstance; +} + +export interface FormProviderProps { + validateMessages?: ValidateMessages; + onFormChange?: (name: string, forms: Forms) => void; +} + +export interface FormContextProps extends FormProviderProps { + triggerFormChange: (name: string, form: FormInstance) => void; + registerForm: (name: string, form: FormInstance) => () => void; +} + +const FormContext = React.createContext({ + triggerFormChange: () => {}, + registerForm: () => () => {}, +}); + +const FormProvider: React.FunctionComponent = ({ + validateMessages, + onFormChange, + children, +}) => { + const formContext = React.useContext(FormContext); + + const formsRef = React.useRef({}); + + return ( + { + if (onFormChange) { + onFormChange(name, 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} + + ); +}; + +export { FormProvider }; + +export default FormContext; diff --git a/src/index.tsx b/src/index.tsx index a5661c24..5ec866c6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,11 +4,13 @@ import Field from './Field'; import List from './List'; import useForm from './useForm'; import FieldForm, { StateFormProps } from './Form'; +import { FormProvider } from './FormContext'; const InternalStateForm = React.forwardRef(FieldForm); type InternalStateForm = typeof InternalStateForm; interface RefStateForm extends InternalStateForm { + FormProvider: typeof FormProvider; Field: typeof Field; List: typeof List; useForm: typeof useForm; @@ -16,10 +18,11 @@ interface RefStateForm extends InternalStateForm { const RefStateForm: RefStateForm = InternalStateForm as any; +RefStateForm.FormProvider = FormProvider; RefStateForm.Field = Field; RefStateForm.List = List; RefStateForm.useForm = useForm; -export { FormInstance, Field, useForm }; +export { FormInstance, Field, useForm, FormProvider }; export default RefStateForm; diff --git a/src/useForm.ts b/src/useForm.ts index 78826503..81f8100f 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -9,7 +9,6 @@ import { NamePath, NotifyInfo, Store, - ValidateFields, ValidateOptions, FormInstance, ValidateMessages, From 2fd7c1a8af700f0633428e80b78aa4d19088e9c6 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 11 Jun 2019 22:39:58 +0800 Subject: [PATCH 3/5] clean up --- src/Form.tsx | 2 +- src/FormContext.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Form.tsx b/src/Form.tsx index 3fc1e622..1f30fa39 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -68,7 +68,7 @@ const StateForm: React.FunctionComponent = ( setCallbacks({ onValuesChange: (...args) => { if (name) { - formContext.triggerFormChange(name, formInstance); + formContext.triggerFormChange(name); } if (onValuesChange) { diff --git a/src/FormContext.tsx b/src/FormContext.tsx index 87759b82..f986be11 100644 --- a/src/FormContext.tsx +++ b/src/FormContext.tsx @@ -11,7 +11,7 @@ export interface FormProviderProps { } export interface FormContextProps extends FormProviderProps { - triggerFormChange: (name: string, form: FormInstance) => void; + triggerFormChange: (name: string) => void; registerForm: (name: string, form: FormInstance) => () => void; } @@ -38,7 +38,7 @@ const FormProvider: React.FunctionComponent = ({ // ========================================================= // = Global Form Control = // ========================================================= - triggerFormChange: (name, form) => { + triggerFormChange: name => { if (onFormChange) { onFormChange(name, formsRef.current); } From 0ba87e2753802639cef97b1443b2f7630922038a Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 12 Jun 2019 14:19:00 +0800 Subject: [PATCH 4/5] trigger form onFieldsChange --- examples/StateForm-context.tsx | 10 +++++----- examples/components/LabelField.tsx | 2 +- src/Form.tsx | 12 +++++------- src/FormContext.tsx | 18 +++++++++++++----- src/useForm.ts | 2 +- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/examples/StateForm-context.tsx b/examples/StateForm-context.tsx index e990043d..389a20cc 100644 --- a/examples/StateForm-context.tsx +++ b/examples/StateForm-context.tsx @@ -23,7 +23,7 @@ const Form1 = () => {

Form 1

Change me!

- + @@ -42,7 +42,7 @@ const Form2 = () => {

Form 2

Will follow Form 1 but not sync back

- + @@ -60,10 +60,10 @@ const Demo = () => {

Support global `validateMessages` config and communication between forms.

{ - console.log('change from:', name, forms); + onFormChange={(name, { changedFields, forms }) => { + console.log('change from:', name, changedFields, forms); if (name === 'first') { - forms.second.setFieldsValue(forms.first.getFieldsValue()); + forms.second.setFields(changedFields); } }} > diff --git a/examples/components/LabelField.tsx b/examples/components/LabelField.tsx index f10ade3e..56716e22 100644 --- a/examples/components/LabelField.tsx +++ b/examples/components/LabelField.tsx @@ -40,7 +40,7 @@ const LabelField: React.FunctionComponent = ({ : React.cloneElement(children as React.ReactElement, { ...control }); return ( -
+
diff --git a/src/Form.tsx b/src/Form.tsx index 1f30fa39..e487b85b 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -66,16 +66,14 @@ const StateForm: React.FunctionComponent = ( ...validateMessages, }); setCallbacks({ - onValuesChange: (...args) => { - if (name) { - formContext.triggerFormChange(name); - } + onValuesChange, + onFieldsChange: (changedFields: FieldData[], ...rest) => { + formContext.triggerFormChange(name, changedFields); - if (onValuesChange) { - onValuesChange(...args); + if (onFieldsChange) { + onFieldsChange(changedFields, ...rest); } }, - onFieldsChange, }); // Initial store value when first mount diff --git a/src/FormContext.tsx b/src/FormContext.tsx index f986be11..89d2c03d 100644 --- a/src/FormContext.tsx +++ b/src/FormContext.tsx @@ -1,17 +1,22 @@ import * as React from 'react'; -import { ValidateMessages, FormInstance } from './interface'; +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, forms: Forms) => void; + onFormChange?: (name: string, info: FormChangeInfo) => void; } export interface FormContextProps extends FormProviderProps { - triggerFormChange: (name: string) => void; + triggerFormChange: (name: string, changedFields: FieldData[]) => void; registerForm: (name: string, form: FormInstance) => () => void; } @@ -38,9 +43,12 @@ const FormProvider: React.FunctionComponent = ({ // ========================================================= // = Global Form Control = // ========================================================= - triggerFormChange: name => { + triggerFormChange: (name, changedFields) => { if (onFormChange) { - onFormChange(name, formsRef.current); + onFormChange(name, { + changedFields, + forms: formsRef.current, + }); } }, registerForm: (name, form) => { diff --git a/src/useForm.ts b/src/useForm.ts index 81f8100f..dbffb1fa 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -238,7 +238,7 @@ export class FormStore { let fields: FieldData[]; if (!namePathList) { - this.getFieldEntities(true).map( + fields = this.getFieldEntities(true).map( (field: FieldEntity): FieldData => { const namePath = field.getNamePath(); const meta = field.getMeta(); From 76db5a030f3e2c0bfa7ebf1af49e5ed26ae6680c Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 12 Jun 2019 14:33:23 +0800 Subject: [PATCH 5/5] update doc --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bbf26b39..d7a615da 100644 --- a/README.md +++ b/README.md @@ -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 | - | @@ -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 @@ -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 |