diff --git a/packages/form-core/src/tests/FormApi.spec.ts b/packages/form-core/src/tests/FormApi.spec.ts index 2429111b6..aa414314f 100644 --- a/packages/form-core/src/tests/FormApi.spec.ts +++ b/packages/form-core/src/tests/FormApi.spec.ts @@ -923,6 +923,45 @@ describe('form api', () => { ]) }) + it('should validate optional object fields during submit', async () => { + const form = new FormApi({ + defaultValues: { + person: null, + } as { person: { firstName: string; lastName: string } | null }, + }) + + const field = new FieldApi({ + form, + name: 'person.firstName', + validators: { + onChange: ({ value }) => + value && value.length > 0 ? undefined : 'first name is required', + }, + }) + + const lastNameField = new FieldApi({ + form, + name: 'person.lastName', + validators: { + onChange: ({ value }) => + value && value.length > 0 ? undefined : 'last name is required', + }, + }) + + field.mount() + lastNameField.mount() + + await form.handleSubmit() + expect(form.state.isFieldsValid).toEqual(false) + expect(form.state.canSubmit).toEqual(false) + expect(form.state.fieldMeta['person.firstName'].errors).toEqual([ + 'first name is required', + ]) + expect(form.state.fieldMeta['person.lastName'].errors).toEqual([ + 'last name is required', + ]) + }) + it('should run all types of validation on fields during submit', async () => { const form = new FormApi({ defaultValues: { diff --git a/packages/form-core/src/tests/util-types.test-d.ts b/packages/form-core/src/tests/util-types.test-d.ts index c06e545dd..ae07663a2 100644 --- a/packages/form-core/src/tests/util-types.test-d.ts +++ b/packages/form-core/src/tests/util-types.test-d.ts @@ -75,6 +75,14 @@ type NestedKeysExample = DeepValue< > assertType(0 as never as NestedKeysExample) +type NestedNullableKeys = DeepValue< + { + meta: { mainUser: 'hello' } | null + }, + 'meta.mainUser' +> +assertType<'hello' | null>(0 as never as NestedNullableKeys) + type NestedArrayExample = DeepValue<{ users: User[] }, 'users[0].age'> assertType(0 as never as NestedArrayExample) diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index c443cf17e..29f238527 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -1,3 +1,6 @@ +type Nullable = T | null +type IsNullable = [null] extends [T] ? true : false + export type RequiredByKey = Omit & Required> @@ -96,6 +99,7 @@ export type DeepValue< TValue, // A string representing the path of the property we're trying to access TAccessor, + TNullable extends boolean = IsNullable, > = // If TValue is any it will recurse forever, this terminates the recursion unknown extends TValue @@ -122,7 +126,9 @@ export type DeepValue< : TAccessor extends `${infer TBefore}.${infer TAfter}` ? DeepValue, TAfter> : TAccessor extends string - ? TValue[TAccessor] + ? TNullable extends true + ? Nullable + : TValue[TAccessor] : never : // Do not allow `TValue` to be anything else never diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 29794a21f..7297defb3 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -23,6 +23,7 @@ export function functionalUpdate( export function getBy(obj: any, path: any) { const pathObj = makePathArray(path) return pathObj.reduce((current: any, pathPart: any) => { + if (current === null) return null if (typeof current !== 'undefined') { return current[pathPart] }