Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wild-donkeys-shave.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/form-core/src/FormGroupApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
72 changes: 72 additions & 0 deletions packages/form-core/tests/FormGroupApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading