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 | diff --git a/examples/StateForm-context.tsx b/examples/StateForm-context.tsx new file mode 100644 index 00000000..389a20cc --- /dev/null +++ b/examples/StateForm-context.tsx @@ -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 ( + +

Form 1

+

Change me!

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

Form 2

+

Will follow Form 1 but not sync back

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

Form Context

+

Support global `validateMessages` config and communication between forms.

+ { + console.log('change from:', name, changedFields, forms); + if (name === 'first') { + forms.second.setFields(changedFields); + } + }} + > +
+ + +
+
+
+ ); +}; + +export default Demo; 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 new file mode 100644 index 00000000..e487b85b --- /dev/null +++ b/src/Form.tsx @@ -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, '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 = ( + { + 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(); + 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.tsx b/src/FormContext.tsx new file mode 100644 index 00000000..89d2c03d --- /dev/null +++ b/src/FormContext.tsx @@ -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({ + triggerFormChange: () => {}, + registerForm: () => () => {}, +}); + +const FormProvider: React.FunctionComponent = ({ + validateMessages, + onFormChange, + children, +}) => { + const formContext = React.useContext(FormContext); + + const formsRef = React.useRef({}); + + return ( + { + 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} + + ); +}; + +export { FormProvider }; + +export default FormContext; diff --git a/src/index.tsx b/src/index.tsx index 89896610..5ec866c6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,117 +1,16 @@ 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'; +import { FormProvider } from './FormContext'; -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 { + FormProvider: typeof FormProvider; Field: typeof Field; List: typeof List; useForm: typeof useForm; @@ -119,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 }; +export { FormInstance, Field, useForm, FormProvider }; export default RefStateForm; diff --git a/src/useForm.ts b/src/useForm.ts index 78826503..dbffb1fa 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -9,7 +9,6 @@ import { NamePath, NotifyInfo, Store, - ValidateFields, ValidateOptions, FormInstance, ValidateMessages, @@ -239,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();