diff --git a/docs/demo/scopedForm.md b/docs/demo/scopedForm.md new file mode 100644 index 00000000..f3db51fc --- /dev/null +++ b/docs/demo/scopedForm.md @@ -0,0 +1,3 @@ +## scopedForm + + diff --git a/docs/examples/scopedForm.tsx b/docs/examples/scopedForm.tsx new file mode 100644 index 00000000..216dac4f --- /dev/null +++ b/docs/examples/scopedForm.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import Form from 'rc-field-form'; +import Input from './components/Input'; +import { isEqual } from 'lodash'; + +const ChildrenContent = (props: { name: number }) => { + + const { name } = props; + + const scopedForm = Form.useFormInstance({ scoped: true }); + const college = Form.useWatch([name, 'college'], scopedForm); + const location = Form.useWatch([name, 'location'], { scoped: true }); + const [, forceUpdate] = React.useState({}); + + React.useEffect(() => { + scopedForm.setFieldValue([name, 'nonexistent'], 'nonexistent'); + }, [scopedForm, name]); + + return ( +
+
+ + + + {college} +
+
+ + + + {location} +
+
+ + + + Checked +
+
+ + { + () => { + if (scopedForm.getFieldValue([name, 'field0'])) { + return ( + + + + ); + } + return null; + } + } + +
+
+ +
+
+ {`scopedForm.getFieldsValue({strict: true }):`} + {`${JSON.stringify(scopedForm.getFieldsValue({ strict: true }))}`} +
+
+ scopedForm.getFieldsValue(): + {`${JSON.stringify(scopedForm.getFieldsValue())}`} +
+
+ {`scopedForm.getFieldValue([name, 'location']):`} + {`${JSON.stringify(scopedForm.getFieldValue([name, 'location']))}`} +
+
+ {`scopedForm.getFieldValue([name, 'nonexistent']):`} + {`${JSON.stringify(scopedForm.getFieldValue([name, 'nonexistent']))}`} +
+
+ {`scopedForm.getFieldsValue({ strict: true, filter: meta => isEqual(meta.name, [name, 'location']) }):`} + {`${JSON.stringify(scopedForm.getFieldsValue({ strict: true, filter: meta => isEqual(meta.name, [name, 'location']) }))}`} +
+
+ {`scopedForm.getFieldsValue(true, meta => isEqual(meta.name, [name, 'location'])):`} + {`${JSON.stringify(scopedForm.getFieldsValue(true, meta => isEqual(meta.name, [name, 'location'])))}`} +
+
+ {`scopedForm.isFieldsTouched(true):`} + {`${JSON.stringify(scopedForm.isFieldsTouched(true))}`} +
+
+ {`scopedForm.isFieldsTouched():`} + {`${JSON.stringify(scopedForm.isFieldsTouched())}`} +
+
+ ); +}; + +export default () => { + const [form] = Form.useForm(); + console.log('rootForm', form); + + return ( +
+
+ <> + + + + + + + + { + (fields, { add }) => ( +
+

Colleges

+ { + fields.map(field => { + return ( + + ); + }) + } + +
+ ) + } +
+ +
+
+ ); +}; diff --git a/src/FieldContext.ts b/src/FieldContext.ts index dbe08038..941f4f81 100644 --- a/src/FieldContext.ts +++ b/src/FieldContext.ts @@ -25,6 +25,7 @@ const Context = React.createContext({ setFieldsValue: warningFunc, validateFields: warningFunc, submit: warningFunc, + getScopeName: warningFunc, getInternalHooks: () => { warningFunc(); @@ -42,6 +43,7 @@ const Context = React.createContext({ setValidateMessages: warningFunc, setPreserve: warningFunc, getInitialValue: warningFunc, + getFieldEntities: warningFunc, }; }, }); diff --git a/src/Form.tsx b/src/Form.tsx index 370185cb..614b251b 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -13,6 +13,7 @@ import type { FormContextProps } from './FormContext'; import FormContext from './FormContext'; import { isSimilar } from './utils/valueUtil'; import ListContext from './ListContext'; +import FormInstanceContext from './FormInstanceContext'; type BaseFormProps = Omit, 'onSubmit' | 'children'>; @@ -148,9 +149,11 @@ const Form: React.ForwardRefRenderFunction = ( ); const wrapperNode = ( - - {childrenNode} - + + + {childrenNode} + + ); if (Component === false) { diff --git a/src/FormInstanceContext.tsx b/src/FormInstanceContext.tsx new file mode 100644 index 00000000..a51e98bc --- /dev/null +++ b/src/FormInstanceContext.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import type { InternalFormInstance } from './interface'; + +const FormInstanceContext = React.createContext(undefined); + +export default FormInstanceContext; diff --git a/src/index.tsx b/src/index.tsx index 3c6c6e5d..7631b3a5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FormInstance } from './interface'; +import type { FormInstance } from './interface'; import Field from './Field'; import List from './List'; import useForm from './useForm'; @@ -9,6 +9,7 @@ import { FormProvider } from './FormContext'; import FieldContext from './FieldContext'; import ListContext from './ListContext'; import useWatch from './useWatch'; +import useFormInstance from './useFormInstance'; const InternalForm = React.forwardRef(FieldForm) as ( props: FormProps & { ref?: React.Ref> }, @@ -21,6 +22,7 @@ interface RefFormType extends InternalFormType { List: typeof List; useForm: typeof useForm; useWatch: typeof useWatch; + useFormInstance: typeof useFormInstance; } const RefForm: RefFormType = InternalForm as RefFormType; @@ -30,8 +32,9 @@ RefForm.Field = Field; RefForm.List = List; RefForm.useForm = useForm; RefForm.useWatch = useWatch; +RefForm.useFormInstance = useFormInstance; -export { Field, List, useForm, FormProvider, FieldContext, ListContext, useWatch }; +export { Field, List, useForm, FormProvider, FieldContext, ListContext, useWatch, useFormInstance }; export type { FormProps, FormInstance }; diff --git a/src/interface.ts b/src/interface.ts index 723998a6..b9777532 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -217,6 +217,12 @@ export type WatchCallBack = ( export interface WatchOptions
{ form?: Form; preserve?: boolean; + scoped?: boolean; +} + +export interface FormInstanceOptions { + form?: Form; + scoped?: boolean; } export interface InternalHooks { @@ -232,6 +238,7 @@ export interface InternalHooks { setValidateMessages: (validateMessages: ValidateMessages) => void; setPreserve: (preserve?: boolean) => void; getInitialValue: (namePath: InternalNamePath) => StoreValue; + getFieldEntities: (prue: boolean) => FieldEntity[]; } /** Only return partial when type is not any */ @@ -271,6 +278,7 @@ export interface FormInstance { // New API submit: () => void; + getScopeName: () => InternalNamePath | undefined; } export type InternalFormInstance = Omit & { diff --git a/src/useForm.ts b/src/useForm.ts index 2fdd233c..3d1f1205 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -96,6 +96,7 @@ export class FormStore { setFieldsValue: this.setFieldsValue, validateFields: this.validateFields, submit: this.submit, + getScopeName: () => undefined, _init: true, getInternalHooks: this.getInternalHooks, @@ -119,6 +120,7 @@ export class FormStore { setPreserve: this.setPreserve, getInitialValue: this.getInitialValue, registerWatch: this.registerWatch, + getFieldEntities: this.getFieldEntities, }; } diff --git a/src/useFormInstance.ts b/src/useFormInstance.ts new file mode 100644 index 00000000..4b16b781 --- /dev/null +++ b/src/useFormInstance.ts @@ -0,0 +1,273 @@ +import * as React from 'react'; +import type { + FieldData, + FieldError, + FilterFunc, + FormInstance, + FormInstanceOptions, + InternalFormInstance, + InternalNamePath, + Meta, + NamePath, +} from './interface'; +import FieldContext, { HOOK_MARK } from './FieldContext'; +import { getNamePath, getValue, setValue, matchNamePath } from './utils/valueUtil'; +import FormInstanceContext from './FormInstanceContext'; + +class ScopedFormStore { + + private form: InternalFormInstance; + + private getScopeName: FormInstance['getScopeName'] = () => undefined; + + private get scopeName() { + return this.getScopeName(); + } + + constructor(form: InternalFormInstance, getScopeName: FormInstance['getScopeName']) { + this.form = form; + this.getScopeName = getScopeName; + } + + private scopedNamePath = (name?: NamePath) => { + return [ + ...getNamePath(this.scopeName), + ...getNamePath(name), + ]; + } + + private scopedNameList = (nameList?: NamePath[]) => { + if (nameList) { + return nameList?.map(this.scopedNamePath); + } + return [this.scopeName]; + } + + private dropScopeName = (name: InternalNamePath) => { + return name.slice(this.scopeName.length); + }; + + private preCheck = any>(fn: T, originFn: T) => { + return ((...args: Parameters) => { + if (this.scopeName) { + return fn(...args); + } + return originFn(...args); + }) as T; + }; + + getForm() { + return { + ...this.form, + getFieldValue: this.preCheck( + this.getFieldValue, + this.form.getFieldValue, + ), + getFieldsValue: this.preCheck( + this.getFieldsValue, + this.form.getFieldsValue, + ), + getFieldError: this.preCheck( + this.getFieldError, + this.form.getFieldError, + ), + getFieldWarning: this.preCheck( + this.getFieldWarning, + this.form.getFieldWarning, + ), + getFieldsError: this.preCheck( + this.getFieldsError, + this.form.getFieldsError, + ), + isFieldsTouched: this.preCheck( + this.isFieldsTouched, + this.form.isFieldsTouched, + ), + isFieldTouched: this.preCheck( + this.isFieldTouched, + this.form.isFieldTouched, + ), + isFieldValidating: this.preCheck( + this.isFieldValidating, + this.form.isFieldValidating, + ), + isFieldsValidating: this.preCheck( + this.isFieldsValidating, + this.form.isFieldsValidating, + ), + resetFields: this.preCheck( + this.resetFields, + this.form.resetFields, + ), + setFields: this.preCheck( + this.setFields, + this.form.setFields, + ), + setFieldValue: this.preCheck( + this.setFieldValue, + this.form.setFieldValue, + ), + setFieldsValue: this.preCheck( + this.setFieldsValue, + this.form.setFieldsValue, + ), + validateFields: this.preCheck( + this.validateFields, + this.form.validateFields, + ), + getScopeName: this.getScopeName, + } as InternalFormInstance; + } + + private getFieldValue = (name: NamePath) => { + return this.form.getFieldValue( + this.scopedNamePath(name), + ); + } + + private getFieldsValue = ( + nameList?: any, + filterFunc?: any, + ) => { + if (nameList === true && !filterFunc) { + return getValue( + this.form.getFieldsValue(true), + this.scopeName, + ); + } + + const mergedFilterFunc = (filter?: FilterFunc): FilterFunc => { + return (meta: Meta) => { + if (meta) { + return matchNamePath(meta.name, this.scopeName, true) + && (!filter || filter({ ...meta, name: this.dropScopeName(meta.name) })); + } + return !filter || filter(meta); + }; + }; + + if (nameList && typeof nameList === 'object' && !Array.isArray(nameList)) { + return getValue( + this.form.getFieldsValue({ + ...nameList, + filter: mergedFilterFunc(nameList.filter), + }), + this.scopeName, + ); + } + + return getValue( + this.form.getFieldsValue( + Array.isArray(nameList) ? this.scopedNameList(nameList) : nameList, + mergedFilterFunc(filterFunc), + ), + this.scopeName, + ); + } + + getFieldError = (name: NamePath) => this.form.getFieldError(this.scopedNamePath(name)); + + getFieldWarning = (name: NamePath) => this.form.getFieldWarning(this.scopedNamePath(name)); + + getFieldsError = (nameList?: NamePath) => { + const fieldErrors = nameList + ? this.form.getFieldsError(this.scopedNameList(nameList)) + : this.form.getFieldsError().filter(field => matchNamePath(field.name, this.scopeName, true)); + return fieldErrors.map(field => ({ ...field, name: this.dropScopeName(field.name) })); + }; + + isFieldsTouched = (...args: any[]) => { + const [arg0, arg1] = args; + + // this first param is array; eg: isFieldsTouched([['field0'], ['field1']]) + if (Array.isArray(arg0)) { + return this.form.isFieldsTouched(this.scopedNameList(arg0), arg1); + } + + // the params are only true; eg: isFieldsTouched(true) + if (args.length === 1 && arg0 === true) { + const internalHooks = this.form.getInternalHooks(HOOK_MARK); + const fieldEntities = internalHooks.getFieldEntities(true); + return fieldEntities.every(entity => { + return !matchNamePath(entity.getNamePath(), this.scopeName, true) || entity.isFieldTouched() || entity.isList(); + }); + } + + // no params; eg: isFieldsTouched() + return this.form.isFieldsTouched([this.scopeName], false); + }; + + isFieldTouched = (name: NamePath) => this.form.isFieldTouched(this.scopedNamePath(name)); + + isFieldValidating = (name: NamePath) => this.form.isFieldValidating(this.scopedNamePath(name)); + + isFieldsValidating = (nameList?: NamePath[]) => this.form.isFieldsValidating(this.scopedNameList(nameList)); + + resetFields = (nameList?: NamePath[]) => this.form.resetFields(this.scopedNameList(nameList)); + + setFields = (fields: FieldData[]) => this.form.setFields(fields.map(field => ({ ...field, name: this.scopedNamePath(field.name) }))); + + setFieldValue = (name: NamePath, value: any) => this.form.setFieldValue(this.scopedNamePath(name), value); + + setFieldsValue = (values: any) => { + return this.form.setFieldsValue( + setValue(this.form.getFieldsValue(true), this.scopeName, values), + ); + }; + + validateFields = (arg1?: any, arg2?: any) => { + + const promiseWrap = async (promise: Promise) => { + return promise.then(res => getValue(res, this.scopeName)).catch(err => { + if (Array.isArray(err.errorFields)) { + return Promise.reject({ + ...err, + errorFields: (err.errorFields as FieldError[]).map(field => ({ ...field, name: this.dropScopeName(field.name) })), + values: getValue(err.values, this.scopeName), + }); + } + return Promise.reject(err); + }); + }; + // this first param is array; eg: validateFields([['field0'], ['field1']]) + if (Array.isArray(arg1)) { + return promiseWrap( + this.form.validateFields(this.scopedNameList(arg1), arg2), + ); + } + // the first param is object, or no params; eg: validateFields() or validateFields({ validateOnly: true, dirty: true }) + return promiseWrap( + this.form.validateFields([this.scopeName], { ...arg1, recursive: true }), + ); + }; + +} + +function useFormInstance(options?: FormInstanceOptions): FormInstance { + const { form, scoped } = options || {}; + const fieldContext = React.useContext(FieldContext); + const formInstance = React.useContext(FormInstanceContext); + const mergedForm = (form || formInstance) as InternalFormInstance; + + const prefixNameRef = React.useRef(fieldContext.prefixName); + prefixNameRef.current = fieldContext.prefixName; + + const scopedformRef = React.useRef(); + + if (!scoped) { + return mergedForm; + } + + if (!scopedformRef.current) { + const scopedFormStore = new ScopedFormStore( + mergedForm, + () => prefixNameRef.current, + ); + scopedformRef.current = scopedFormStore.getForm(); + } + + return scopedformRef.current; + +} + +export default useFormInstance; diff --git a/src/useWatch.ts b/src/useWatch.ts index 127c04d5..d097a919 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -1,6 +1,6 @@ import warning from 'rc-util/lib/warning'; -import { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import FieldContext, { HOOK_MARK } from './FieldContext'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { HOOK_MARK } from './FieldContext'; import type { FormInstance, InternalFormInstance, @@ -11,6 +11,7 @@ import type { } from './interface'; import { isFormInstance } from './utils/typeUtil'; import { getNamePath, getValue } from './utils/valueUtil'; +import useFormInstance from './useFormInstance'; type ReturnPromise = T extends Promise ? ValueType : never; type GetGeneric = ReturnPromise>; @@ -111,8 +112,10 @@ function useWatch( const valueStrRef = useRef(valueStr); valueStrRef.current = valueStr; - const fieldContext = useContext(FieldContext); - const formInstance = (form as InternalFormInstance) || fieldContext; + const formInstance = useFormInstance({ + form, + scoped: options.scoped, + }) as InternalFormInstance; const isValidForm = formInstance && formInstance._init; // Warning if not exist form instance @@ -136,7 +139,7 @@ function useWatch( return; } - const { getFieldsValue, getInternalHooks } = formInstance; + const { getFieldsValue, getInternalHooks, getScopeName } = formInstance; const { registerWatch } = getInternalHooks(HOOK_MARK); const getWatchValue = (values: any, allValues: any) => { @@ -147,7 +150,11 @@ function useWatch( }; const cancelRegister = registerWatch((values, allValues) => { - const newValue = getWatchValue(values, allValues); + const scopeName = getScopeName() ?? []; + const newValue = getWatchValue( + getValue(values, scopeName), + getValue(allValues, scopeName), + ); const nextValueStr = stringify(newValue); // Compare stringify in case it's nest object diff --git a/tests/scopedForm.test.tsx b/tests/scopedForm.test.tsx new file mode 100644 index 00000000..fe93809a --- /dev/null +++ b/tests/scopedForm.test.tsx @@ -0,0 +1,474 @@ +import React, { forwardRef } from 'react'; +import type { ForwardedRef, PropsWithChildren } from 'react'; +import { act, renderHook } from '@testing-library/react'; +import Form from '../src'; +import type { FormInstance } from '../src'; +import { isEqual } from 'lodash'; +import timeout from './common/timeout'; + +const CreateForm = forwardRef((props: PropsWithChildren, ref: ForwardedRef) => { + const { children } = props; + return ( + + + + + + + + + + + + { + (fields, { add }) => ( + <> + { + fields.map(field => { + return ( + + + + + + + + + + + + ); + }) + } + + {children} + + ) + } + + + ); +}); + +describe('Form.useFormInstance', () => { + + it('useFormInstance returns undefined', () => { + const { result } = renderHook(() => Form.useFormInstance()); + + expect(result.current).toBeUndefined(); + }); + + it('useFormInstance returns FormInstance', () => { + const formRef = React.createRef(); + const { result } = renderHook(() => Form.useFormInstance(), { + wrapper({ children }) { + return ( +
{children}
+ ); + }, + }); + + expect(result.current).toBeTruthy(); + expect(formRef.current).toBe(result.current); + }); + + it('useFormInstance({ scoped: true }) returns new FormInstance', () => { + const formRef = React.createRef(); + const { result } = renderHook(() => Form.useFormInstance({ scoped: true }), { + wrapper({ children }) { + return ( +
{children}
+ ); + }, + }); + + expect(result.current).toBeTruthy(); + expect(formRef.current).not.toBe(result.current); + }); + + it('useFormInstance({ scoped: true }) works fine in top level', () => { + const formRef = React.createRef(); + const { result } = renderHook(() => Form.useFormInstance({ scoped: true }), { + wrapper({ children }) { + return ( +
+ + + + + + + {children} +
+ ); + }, + }); + + expect(result.current.getScopeName()).toBeUndefined(); + expect(formRef.current.getScopeName()).toBeUndefined(); + + expect(result.current.getFieldsValue()).toEqual(formRef.current.getFieldsValue()); + expect(result.current.getFieldsValue(true)).toBe(formRef.current.getFieldsValue(true)); + expect(result.current.getFieldsValue(['field0'])).toEqual(formRef.current.getFieldsValue(['field0'])); + expect(result.current.getFieldValue(['field0'])).toEqual(formRef.current.getFieldValue(['field0'])); + + }); + + const renderScopedForm = () => { + const formRef = React.createRef(); + const { result } = renderHook(() => Form.useFormInstance({ scoped: true }), { + wrapper({ children }) { + return ( + {children} + ); + }, + }); + return { formRef, result }; + }; + + it('useFormInstance({ scoped: true }).getFieldsValue works fine in sub level', async () => { + + const { result } = renderScopedForm(); + + expect(result.current.getScopeName()).toEqual(['list']); + + act(() => { + result.current.setFieldValue([0, 'nonexistent'], 'nonexistent'); + }); + + // getFieldsValue + expect(result.current.getFieldsValue({ strict: true })).toEqual( + [ + { + listfield0: 'listfield0', + }, + ] + ); + expect(result.current.getFieldsValue(true)).toBe(result.current.getFieldsValue(true)); + expect(result.current.getFieldsValue(true)).toEqual( + [ + { + listfield0: 'listfield0', + nonexistent: 'nonexistent', + }, + ] + ); + expect(result.current.getFieldsValue({ + strict: true, + filter: (meta) => { + return isEqual(meta.name, [0, 'listfield0']); + }, + })).toEqual([{ listfield0: 'listfield0' }]); + expect(result.current.getFieldsValue([ + [0, 'listfield0'], + [0, 'listfield1'], + ], (meta) => { + return isEqual(meta.name, [0, 'listfield0']); + })).toEqual([{ listfield0: 'listfield0' }]); + + const mock = jest.fn(); + const values = result.current.getFieldsValue([ + [0, 'listfield0'], + [0, 'notexistfield'], + ], (meta) => { + if (!meta) { + mock(); + } + return true; + }); + expect(mock).toHaveBeenCalled(); + expect(values).toEqual([{ listfield0: 'listfield0' }]); + + }); + + it('useFormInstance({ scoped: true }).(get|set)FieldValue works fine in sub level', async () => { + const { result } = renderScopedForm(); + expect(result.current.getFieldValue([0, 'listfield0'])).toBe('listfield0'); + expect(result.current.getFieldValue([0, 'listfield1'])).toBeUndefined(); + act(() => { + result.current.setFieldValue([0, 'listfield1'], 'modifiedlistfield1'); + result.current.setFieldValue([0, 'nonexistent'], 'nonexistent'); + }); + expect(result.current.getFieldValue([0, 'listfield1'])).toBe('modifiedlistfield1'); + expect(result.current.getFieldValue([0, 'nonexistent'])).toBe('nonexistent'); + }); + + it('useFormInstance({ scoped: true }).setFieldsValue() works fine in sub level', () => { + const { result } = renderScopedForm(); + act(() => { + result.current.setFieldsValue([{ + listfield1: 'listfield1', + }]); + }); + expect(result.current.getFieldsValue()).toEqual([{ listfield1: 'listfield1' }]); + }); + + it('useFormInstance({ scoped: true }).getFieldError() works fine in sub level', async () => { + const { result, formRef } = renderScopedForm(); + + await act(async () => { + result.current.setFieldValue([0, 'listfield0'], 'Capital'); + await result.current.validateFields().catch(e => e); + }); + expect(result.current.getFieldError([0, 'listfield1'])).toEqual(['listfield1 is required']); + expect(result.current.getFieldWarning([0, 'listfield0'])).toEqual(['Capital letters are not recommended']); + const fieldsError = result.current.getFieldsError(); + expect(fieldsError.find(field => isEqual(field.name, [0, 'listfield0']))?.warnings).toEqual(['Capital letters are not recommended']); + expect(fieldsError.find(field => isEqual(field.name, [0, 'listfield1']))?.errors).toEqual(['listfield1 is required']); + expect(formRef.current.getFieldError(['field0'])).toEqual([]); + + await act(async () => { + result.current.setFieldsValue([]); + await result.current.validateFields().catch(e => e); + }); + expect(result.current.getFieldError([])).toEqual(['At least one is required']); + + }); + + it('useFormInstance({ scoped: true }).validateFields() works fine in sub level', async () => { + const { result, formRef } = renderScopedForm(); + let values: any; + let errors: any; + act(() => { + result.current.setFieldsValue([ + { + listfield0: 'Capital', + }, + {}, + ]); + }); + await act(async () => { + try { + values = await result.current.validateFields(); + } catch (e) { + errors = e; + } + }); + expect(values).toBeUndefined(); + expect(errors?.errorFields).toEqual([ + { name: [0, 'listfield1'], errors: ['listfield1 is required'], warnings: [] }, + { name: [1, 'listfield1'], errors: ['listfield1 is required'], warnings: [] }, + ]); + expect(errors?.values).toEqual([{ listfield0: 'Capital', }, {}]); + expect(formRef.current.getFieldError('field0')).toEqual([]); + + await act(async () => { + await formRef.current.validateFields().catch(e => e); + }); + expect(formRef.current.getFieldError('field2')).toEqual(['field2 is required']); + + act(() => { + formRef.current.resetFields(); + }); + await act(async () => { + await result.current.validateFields([ + [0, 'listfield0'] + ]).catch(e => e); + }); + expect(result.current.getFieldError([0, 'listfield1'])).toEqual([]); + + await act(async () => { + await result.current.validateFields([ + [0, 'listfield1'] + ]).catch(e => e); + }); + expect(result.current.getFieldError([0, 'listfield1'])).toEqual(['listfield1 is required']); + + }); + + it('useFormInstance({ scoped: true }).isFieldTouched() works fine in sub level', () => { + const { result } = renderScopedForm(); + act(() => { + result.current.setFields([ + { + name: [0, 'listfield0'], + touched: true, + }, + ]); + }); + expect(result.current.isFieldTouched([0, 'listfield0'])).toBe(true); + expect(result.current.isFieldTouched([0, 'listfield1'])).toBe(false); + expect(result.current.isFieldsTouched([0])).toBe(true); + expect( + result.current.isFieldsTouched([ + [0, 'listfield0'], + [0, 'listfield1'], + ]), + ).toBe(true); + expect( + result.current.isFieldsTouched([ + [0, 'listfield0'], + [0, 'listfield1'], + ], true), + ).toBe(false); + expect(result.current.isFieldsTouched(true)).toBe(false); + expect(result.current.isFieldsTouched()).toBe(true); + + act(() => { + result.current.setFields([ + { + name: [0, 'listfield1'], + touched: true, + }, + { + name: [0, 'listfield2'], + touched: true, + }, + ]); + }); + expect(result.current.isFieldsTouched(true)).toBe(true); + + }); + + it('useFormInstance({ scoped: true }).isFieldValidating() works fine in sub level', async () => { + const { result, formRef } = renderScopedForm(); + + act(() => { + formRef.current.validateFields(['field2']).catch(e => e); + }); + + expect(formRef.current.isFieldsValidating()).toBe(true); + expect(result.current.isFieldsValidating()).toBe(false); + + await timeout(); + + act(() => { + result.current.validateFields().catch(e => e); + }); + + expect(result.current.isFieldValidating([0, 'listfield0'])).toBe(true); + expect(result.current.isFieldValidating([0, 'listfield2'])).toBe(false); + + expect(result.current.isFieldsValidating()).toBe(true); + expect(result.current.isFieldsValidating([ + [0, 'listfield0'], + [0, 'listfield1'], + [0, 'listfield2'], + ])).toBe(true); + expect(result.current.isFieldsValidating([ + [0, 'listfield2'], + ])).toBe(false); + + }); + + it('useFormInstance({ scoped: true }).resetFields() works fine in sub level', () => { + const { result, formRef } = renderScopedForm(); + + act(() => { + formRef.current.setFields([ + { + name: 'field2', + touched: true, + }, + { + name: ['list', 0, 'listfield0'], + touched: true, + }, + { + name: ['list', 0, 'listfield1'], + touched: true, + }, + ]); + }); + expect(formRef.current.isFieldTouched('field2')).toBe(true); + expect(result.current.isFieldsTouched()).toBe(true); + expect(result.current.isFieldsTouched(true)).toBe(false); + expect(result.current.isFieldsTouched([ + [0, 'listfield0'], + ])).toBe(true); + + act(() => { + result.current.resetFields([ + [0, 'listfield0'], + ]); + }); + expect(formRef.current.isFieldTouched('field2')).toBe(true); + expect(result.current.isFieldsTouched([ + [0, 'listfield0'], + ])).toBe(false); + + act(() => { + result.current.resetFields(); + }); + expect(formRef.current.isFieldTouched('field2')).toBe(true); + expect(result.current.isFieldsTouched()).toBe(false); + }); + +}); + +describe('Form.useWatch', () => { + it('Form.useWatch({ scoped: true })', () => { + const formRef = React.createRef(); + const { result } = renderHook(() => Form.useWatch([0, 'listfield0'], { scoped: true }), { + wrapper({ children }) { + return ( + {children} + ); + }, + }); + + expect(result.current).toBe('listfield0'); + act(() => { + formRef.current.setFieldValue(['list', 0, 'listfield0'], 'modified'); + }); + expect(result.current).toBe('modified'); + + }); +});