diff --git a/.changeset/wild-donkeys-shave.md b/.changeset/wild-donkeys-shave.md new file mode 100644 index 0000000000..6594bbc125 --- /dev/null +++ b/.changeset/wild-donkeys-shave.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +Restrict form group membership to dot- and bracket-delimited field paths so fields that merely share a name prefix with a group are no longer treated as part of it. diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 80770f8897..609ef5fc4f 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1628,7 +1628,7 @@ export class FieldApi< const encompassingGroups = opts?.skipGroupValidation ? [] : Array.from(this.form.formGroupApis).filter((group) => - this.name.startsWith(group.name), + isFieldInGroup(group.name, this.name), ) // Attempt to sync validate first diff --git a/packages/form-core/src/FormGroupApi.ts b/packages/form-core/src/FormGroupApi.ts index 4790d5505c..0c36940155 100644 --- a/packages/form-core/src/FormGroupApi.ts +++ b/packages/form-core/src/FormGroupApi.ts @@ -1646,7 +1646,7 @@ export class FormGroupApi< if (!field.instance) continue // TODO: How to handle FormGroups? if (!(field.instance instanceof FieldApi)) continue - if (field.instance.name.startsWith(this.name)) { + if (isFieldInGroup(this.name, field.instance.name)) { relatedFields.push(field.instance) } } diff --git a/packages/form-core/tests/FormGroupApi.spec.ts b/packages/form-core/tests/FormGroupApi.spec.ts index 02d684fc1b..93bf03ba8a 100644 --- a/packages/form-core/tests/FormGroupApi.spec.ts +++ b/packages/form-core/tests/FormGroupApi.spec.ts @@ -1189,6 +1189,78 @@ describe('form group api', () => { expect(step1NameField.state.meta.errors).toEqual([]) }) + it('should not treat a field whose name merely shares a prefix as related', async () => { + const form = new FormApi({ + defaultValues: { + step1: { name: 'test' }, + step1Extra: '', + }, + }) + + const step1Group = new FormGroupApi({ + name: 'step1', + form, + }) + + const step1NameField = new FieldApi({ + name: 'step1.name', + form, + }) + + const step1ExtraField = new FieldApi({ + name: 'step1Extra', + form, + validators: { + onSubmit: () => 'Extra is invalid', + }, + }) + + form.mount() + step1Group.mount() + step1NameField.mount() + step1ExtraField.mount() + + await step1Group.handleSubmit() + + expect(step1NameField.state.meta.isTouched).toBe(true) + expect(step1ExtraField.state.meta.isTouched).toBe(false) + expect(step1ExtraField.state.meta.errors).toEqual([]) + expect(step1Group.areRelatedFieldsValid()).toBe(true) + }) + + it('should not cascade field validation into a group whose name merely shares a prefix', () => { + const form = new FormApi({ + defaultValues: { + step1: { name: 'test' }, + step1Extra: '', + }, + }) + + const onChange = vi.fn(() => undefined) + + const step1Group = new FormGroupApi({ + name: 'step1', + form, + validators: { + onChange, + }, + }) + + const step1ExtraField = new FieldApi({ + name: 'step1Extra', + form, + }) + + form.mount() + step1Group.mount() + step1ExtraField.mount() + + step1ExtraField.setMeta((prev) => ({ ...prev, isTouched: true })) + step1ExtraField.handleChange('changed') + + expect(onChange).not.toHaveBeenCalled() + }) + describe('isFieldsValid / isGroupValid / isValid', () => { it('should be true on a pristine, valid group', () => { const form = new FormApi({