diff --git a/examples/V7/parseFormatInputValues.tsx b/examples/V7/parseFormatInputValues.tsx index 5bfd50497f4..d315f276694 100644 --- a/examples/V7/parseFormatInputValues.tsx +++ b/examples/V7/parseFormatInputValues.tsx @@ -24,7 +24,7 @@ export default function App() {
diff --git a/src/__tests__/logic/validateField.test.tsx b/src/__tests__/logic/validateField.test.tsx index 35ba933ec66..dbdfca19efe 100644 --- a/src/__tests__/logic/validateField.test.tsx +++ b/src/__tests__/logic/validateField.test.tsx @@ -57,6 +57,22 @@ describe('validateField', () => { }, }); + expect( + await validateField( + { + _f: { + valueAsNumber: true, + mount: true, + name: 'test', + ref: { name: 'test' }, + required: 'required', + }, + }, + 2, + false, + ), + ).toEqual({}); + expect( await validateField( { diff --git a/src/__tests__/type.test.tsx b/src/__tests__/type.test.tsx index 43656a8ba4d..72425b2927e 100644 --- a/src/__tests__/type.test.tsx +++ b/src/__tests__/type.test.tsx @@ -60,7 +60,7 @@ test('should not throw type error with optional array fields', () => { {fields.map((field, index) => (
- +
))} {fieldArray.fields.map((item) => { diff --git a/src/__tests__/useForm.test.tsx b/src/__tests__/useForm.test.tsx index 2add4232bbe..86dfdd2b549 100644 --- a/src/__tests__/useForm.test.tsx +++ b/src/__tests__/useForm.test.tsx @@ -1553,7 +1553,7 @@ describe('useForm', () => { mode: 'onChange', resolver: async (values) => { if (!values.test) { - const result = { + return { values: {}, errors: { test: { @@ -1561,7 +1561,6 @@ describe('useForm', () => { }, }, }; - return result; } return { @@ -1720,7 +1719,10 @@ describe('useForm', () => { it('should update defaultValues async', async () => { const App = () => { - const { register } = useForm({ + const { + register, + formState: { isLoading }, + } = useForm({ defaultValues: async () => { await sleep(100); @@ -1733,17 +1735,26 @@ describe('useForm', () => { return ( +

{isLoading ? 'loading...' : 'done'}

); }; render(); + await waitFor(() => { + screen.getByText('loading...'); + }); + await waitFor(() => { expect((screen.getByRole('textbox') as HTMLInputElement).value).toEqual( 'test', ); }); + + await waitFor(() => { + screen.getByText('done'); + }); }); it('should update async default values for controlled components', async () => { diff --git a/src/__tests__/useWatch.test.tsx b/src/__tests__/useWatch.test.tsx index 13dbea707ac..1123dba6ead 100644 --- a/src/__tests__/useWatch.test.tsx +++ b/src/__tests__/useWatch.test.tsx @@ -92,6 +92,35 @@ describe('useWatch', () => { expect(result.current).toEqual(['test', 'test1']); }); + it('should return own default value for single input', () => { + const { result } = renderHook(() => { + const { control } = useForm<{ test: string; test1: string }>({}); + return useWatch({ + control, + name: 'test', + defaultValue: 'test', + }); + }); + + expect(result.current).toEqual('test'); + }); + + it('should return own default value for array of inputs', () => { + const { result } = renderHook(() => { + const { control } = useForm<{ test: string; test1: string }>({}); + return useWatch({ + control, + name: ['test', 'test1'], + defaultValue: { + test: 'test', + test1: 'test1', + }, + }); + }); + + expect(result.current).toEqual(['test', 'test1']); + }); + it('should return default value when name is undefined', () => { const { result } = renderHook(() => { const { control } = useForm<{ test: string; test1: string }>({ diff --git a/src/__typetest__/path/eager.test-d.ts b/src/__typetest__/path/eager.test-d.ts index 4306bd0bc53..f53078cbf7a 100644 --- a/src/__typetest__/path/eager.test-d.ts +++ b/src/__typetest__/path/eager.test-d.ts @@ -23,6 +23,25 @@ import { _, Depth3Type } from '../__fixtures__'; const actual = _ as Path<{ foo: string[] }>; expectType<'foo' | `foo.${number}`>(actual); } + + /** it should be able to avoid self-referencing/recursion, not crashing on self-referencing types. */ { + type Foo = { foo: Foo }; + const actual = _ as Path; + expectType<'foo'>(actual); + } + + /** it should not erroneously match subtypes as traversed */ { + type Foo = + | { + foo?: Foo; + bar?: { + baz: 1; + }; + } + | {}; + const actual = _ as Path; + expectType<'foo' | 'bar' | 'bar.baz'>(actual); + } } /** {@link ArrayPath} */ { @@ -42,6 +61,25 @@ import { _, Depth3Type } from '../__fixtures__'; const actual = _ as ArrayPath<{ foo: string[][][] }>; expectType<'foo' | `foo.${number}`>(actual); } + + /** it should be able to avoid self-referencing/recursion, not crashing on self-referencing types. */ { + type Foo = { foo: Foo[] }; + const actual = _ as ArrayPath; + expectType<'foo'>(actual); + } + + /** it should not erroneously match subtypes as traversed */ { + type Foo = + | { + bar?: { + baz?: 1; + fooArr?: Foo[]; + }; + } + | {}; + const actual = _ as ArrayPath; + expectType<'bar.fooArr'>(actual); + } } /** {@link PathValue} */ { diff --git a/src/logic/createFormControl.ts b/src/logic/createFormControl.ts index ed11b568d76..ad75af5cb13 100644 --- a/src/logic/createFormControl.ts +++ b/src/logic/createFormControl.ts @@ -104,6 +104,7 @@ export function createFormControl< let _formState: FormState = { submitCount: 0, isDirty: false, + isLoading: true, isValidating: false, isSubmitted: false, isSubmitting: false, @@ -1245,6 +1246,9 @@ export function createFormControl< if (isFunction(_options.defaultValues)) { _options.defaultValues().then((values) => { reset(values, _options.resetOptions); + _subjects.state.next({ + isLoading: false, + }); }); } diff --git a/src/logic/validateField.ts b/src/logic/validateField.ts index 2591f66f8dd..13e66e4bc1d 100644 --- a/src/logic/validateField.ts +++ b/src/logic/validateField.ts @@ -17,6 +17,7 @@ import isObject from '../utils/isObject'; import isRadioInput from '../utils/isRadioInput'; import isRegex from '../utils/isRegex'; import isString from '../utils/isString'; +import isUndefined from '../utils/isUndefined'; import appendErrors from './appendErrors'; import getCheckboxValue from './getCheckboxValue'; @@ -61,7 +62,9 @@ export default async ( const isCheckBox = isCheckBoxInput(ref); const isRadioOrCheckbox = isRadio || isCheckBox; const isEmpty = - ((valueAsNumber || isFileInput(ref)) && !ref.value) || + ((valueAsNumber || isFileInput(ref)) && + isUndefined(ref.value) && + isUndefined(inputValue)) || inputValue === '' || (Array.isArray(inputValue) && !inputValue.length); const appendErrorsCurry = appendErrors.bind( diff --git a/src/types/form.ts b/src/types/form.ts index f6918217539..ee27bb6b8e6 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -130,6 +130,7 @@ export type ReadFormState = { [K in keyof FormStateProxy]: boolean | 'all' }; export type FormState = { isDirty: boolean; + isLoading: boolean; isSubmitted: boolean; isSubmitSuccessful: boolean; isSubmitting: boolean; diff --git a/src/types/path/eager.ts b/src/types/path/eager.ts index aff596df7f6..2ec8a040841 100644 --- a/src/types/path/eager.ts +++ b/src/types/path/eager.ts @@ -1,21 +1,58 @@ import { FieldValues } from '../fields'; import { BrowserNativeObject, + IsAny, + IsEqual, Primitive, UnPackAsyncDefaultValues, } from '../utils'; import { ArrayKey, IsTuple, TupleKeys } from './common'; +/** + * Helper function to break apart T1 and check if any are equal to T2 + * + * See {@link IsEqual} + */ +type AnyIsEqual = T1 extends T2 + ? IsEqual extends true + ? true + : never + : never; + /** * Helper type for recursively constructing paths through a type. + * This actually constructs the strings and recurses into nested + * object types. + * * See {@link Path} */ -type PathImpl = V extends +type PathImpl = V extends | Primitive | BrowserNativeObject ? `${K}` - : `${K}` | `${K}.${Path}`; + : // Check so that we don't recurse into the same type + // by ensuring that the types are mutually assignable + // mutually required to avoid false positives of subtypes + true extends AnyIsEqual + ? `${K}` + : `${K}` | `${K}.${PathInternal}`; + +/** + * Helper type for recursively constructing paths through a type. + * This obsucres the internal type param TraversedTypes from exported contract. + * + * See {@link Path} + */ +type PathInternal = T extends ReadonlyArray + ? IsTuple extends true + ? { + [K in TupleKeys]-?: PathImpl; + }[TupleKeys] + : PathImpl + : { + [K in keyof T]-?: PathImpl; + }[keyof T]; /** * Type which eagerly collects all paths through a type @@ -25,15 +62,9 @@ type PathImpl = V extends * Path<{foo: {bar: string}}> = 'foo' | 'foo.bar' * ``` */ -export type Path = T extends ReadonlyArray - ? IsTuple extends true - ? { - [K in TupleKeys]-?: PathImpl; - }[TupleKeys] - : PathImpl - : { - [K in keyof T]-?: PathImpl; - }[keyof T]; +// We want to explode the union type and process each individually +// so assignable types don't leak onto the stack from the base. +export type Path = T extends any ? PathInternal : never; /** * See {@link Path} @@ -44,36 +75,60 @@ export type FieldPath = Path< /** * Helper type for recursively constructing paths through a type. + * This actually constructs the strings and recurses into nested + * object types. + * * See {@link ArrayPath} */ -type ArrayPathImpl = V extends +type ArrayPathImpl = V extends | Primitive | BrowserNativeObject - ? never + ? IsAny extends true + ? string + : never : V extends ReadonlyArray ? U extends Primitive | BrowserNativeObject + ? IsAny extends true + ? string + : never + : // Check so that we don't recurse into the same type + // by ensuring that the types are mutually assignable + // mutually required to avoid false positives of subtypes + true extends AnyIsEqual ? never - : `${K}` | `${K}.${ArrayPath}` - : `${K}.${ArrayPath}`; + : `${K}` | `${K}.${ArrayPathInternal}` + : true extends AnyIsEqual + ? never + : `${K}.${ArrayPathInternal}`; + +/** + * Helper type for recursively constructing paths through a type. + * This obsucres the internal type param TraversedTypes from exported contract. + * + * See {@link ArrayPath} + */ +type ArrayPathInternal = T extends ReadonlyArray + ? IsTuple extends true + ? { + [K in TupleKeys]-?: ArrayPathImpl; + }[TupleKeys] + : ArrayPathImpl + : { + [K in keyof T]-?: ArrayPathImpl; + }[keyof T]; /** * Type which eagerly collects all paths through a type which point to an array * type. - * @typeParam T - type which should be introspected + * @typeParam T - type which should be introspected. * @example * ``` * Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz' * ``` */ -export type ArrayPath = T extends ReadonlyArray - ? IsTuple extends true - ? { - [K in TupleKeys]-?: ArrayPathImpl; - }[TupleKeys] - : ArrayPathImpl - : { - [K in keyof T]-?: ArrayPathImpl; - }[keyof T]; +// We want to explode the union type and process each individually +// so assignable types don't leak onto the stack from the base. +export type ArrayPath = T extends any ? ArrayPathInternal : never; /** * See {@link ArrayPath} diff --git a/src/types/utils.ts b/src/types/utils.ts index 073357414aa..edd688b7ed1 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -77,6 +77,26 @@ export type IsAny = 0 extends 1 & T ? true : false; */ export type IsNever = [T] extends [never] ? true : false; +/** + * Checks whether T1 can be exactly (mutually) assigned to T2 + * @typeParam T1 - type to check + * @typeParam T2 - type to check against + * ``` + * IsEqual = true + * IsEqual<'foo', 'foo'> = true + * IsEqual = false + * IsEqual = false + * IsEqual = false + * IsEqual<'foo', string> = false + * IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean + * ``` + */ +export type IsEqual = T1 extends T2 + ? (() => G extends T1 ? 1 : 2) extends () => G extends T2 ? 1 : 2 + ? true + : false + : false; + export type DeepMap = IsAny extends true ? any : T extends BrowserNativeObject | NestedValue diff --git a/src/types/validator.ts b/src/types/validator.ts index d1a62067d31..741a09b66e8 100644 --- a/src/types/validator.ts +++ b/src/types/validator.ts @@ -15,7 +15,7 @@ export type ValidationValueMessage< message: Message; }; -export type ValidateResult = Message | Message[] | boolean | undefined; +export type ValidateResult = Message | boolean | undefined; export type Validate = ( value: TFieldValue, diff --git a/src/useForm.ts b/src/useForm.ts index 5a2f0f24ed6..c22591a99c3 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -49,6 +49,7 @@ export function useForm< const [formState, updateFormState] = React.useState>({ isDirty: false, isValidating: false, + isLoading: true, isSubmitted: false, isSubmitting: false, isSubmitSuccessful: false, diff --git a/src/useFormState.ts b/src/useFormState.ts index ee1bc10a0ae..c73cdc3fe10 100644 --- a/src/useFormState.ts +++ b/src/useFormState.ts @@ -51,6 +51,7 @@ function useFormState( const _mounted = React.useRef(true); const _localProxyFormState = React.useRef({ isDirty: false, + isLoading: false, dirtyFields: false, touchedFields: false, isValidating: false, diff --git a/src/useWatch.ts b/src/useWatch.ts index 355f348ff9c..2881af8cc00 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -181,9 +181,10 @@ export function useWatch( }); const [value, updateValue] = React.useState( - isUndefined(defaultValue) - ? control._getWatch(name as InternalFieldName) - : defaultValue, + control._getWatch( + name as InternalFieldName, + defaultValue as DeepPartialSkipArrayKey, + ), ); React.useEffect(() => control._removeUnmounted()); diff --git a/src/utils/unset.ts b/src/utils/unset.ts index 2f777db3ba3..766309b1d02 100644 --- a/src/utils/unset.ts +++ b/src/utils/unset.ts @@ -24,41 +24,28 @@ function isEmptyArray(obj: unknown[]) { return true; } -export default function unset(object: any, path: string) { - const updatePath = isKey(path) ? [path] : stringToPath(path); - const childObject = - updatePath.length == 1 ? object : baseGet(object, updatePath); - const key = updatePath[updatePath.length - 1]; - let previousObjRef; +export default function unset(object: any, path: string | (string | number)[]) { + const paths = Array.isArray(path) + ? path + : isKey(path) + ? [path] + : stringToPath(path); + + const childObject = paths.length === 1 ? object : baseGet(object, paths); + + const index = paths.length - 1; + const key = paths[index]; if (childObject) { delete childObject[key]; } - for (let k = 0; k < updatePath.slice(0, -1).length; k++) { - let index = -1; - let objectRef; - const currentPaths = updatePath.slice(0, -(k + 1)); - const currentPathsLength = currentPaths.length - 1; - - if (k > 0) { - previousObjRef = object; - } - - while (++index < currentPaths.length) { - const item = currentPaths[index]; - objectRef = objectRef ? objectRef[item] : object[item]; - - if ( - currentPathsLength === index && - ((isObject(objectRef) && isEmptyObject(objectRef)) || - (Array.isArray(objectRef) && isEmptyArray(objectRef))) - ) { - previousObjRef ? delete previousObjRef[item] : delete object[item]; - } - - previousObjRef = objectRef; - } + if ( + index !== 0 && + ((isObject(childObject) && isEmptyObject(childObject)) || + (Array.isArray(childObject) && isEmptyArray(childObject))) + ) { + unset(object, paths.slice(0, -1)); } return object;