From 3ffcb176474bcd11e186f7bcc29d689d6c8db113 Mon Sep 17 00:00:00 2001 From: Muhammad Amin Saffari Taheri Date: Mon, 20 May 2024 20:03:25 +0330 Subject: [PATCH 1/2] feat: add support for nullable objects. --- packages/form-core/src/tests/FormApi.spec.ts | 39 +++++++++++++++++++ .../form-core/src/tests/util-types.test-d.ts | 8 ++++ packages/form-core/src/util-types.ts | 8 +++- packages/form-core/src/utils.ts | 2 +- 4 files changed, 55 insertions(+), 2 deletions(-) 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..5235b7bdc 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -23,7 +23,7 @@ export function functionalUpdate( export function getBy(obj: any, path: any) { const pathObj = makePathArray(path) return pathObj.reduce((current: any, pathPart: any) => { - if (typeof current !== 'undefined') { + if (typeof current !== 'undefined' && current !== null) { return current[pathPart] } return undefined From 50a016dfcff9301fdd28c02f290d7c01d862da81 Mon Sep 17 00:00:00 2001 From: Muhammad Amin Saffari Taheri Date: Mon, 20 May 2024 21:41:35 +0330 Subject: [PATCH 2/2] feat: return null if the value was assigned null. --- packages/form-core/src/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 5235b7bdc..7297defb3 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -23,7 +23,8 @@ export function functionalUpdate( export function getBy(obj: any, path: any) { const pathObj = makePathArray(path) return pathObj.reduce((current: any, pathPart: any) => { - if (typeof current !== 'undefined' && current !== null) { + if (current === null) return null + if (typeof current !== 'undefined') { return current[pathPart] } return undefined