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/upset-lemons-spend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

Added options to reset/replace/delete fields for FieldGroup
83 changes: 82 additions & 1 deletion packages/form-core/src/FieldGroupApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ export interface FieldGroupOptions<
onSubmitMeta?: TSubmitMeta
}

function isFieldInStringGroup(fieldName: string, groupPath: string) {
return (
fieldName !== groupPath &&
fieldName.startsWith(groupPath) &&
(fieldName[groupPath.length] === '.' || fieldName[groupPath.length] === '[')
)
}

export class FieldGroupApi<
in out TFormData,
in out TFieldGroupData,
Expand Down Expand Up @@ -395,6 +403,36 @@ export class FieldGroupApi<
return this.form.deleteField(this.getFormFieldName(field))
}

/**
* Delete all fields that belong to this field group.
*/
deleteAllFields = () => {
if (typeof this.fieldsMap === 'string') {
const groupPath = this.fieldsMap.toString()
const currentValue = this.form.getFieldValue(groupPath)

const fieldsToDelete = Object.keys(this.form.fieldInfo).filter(
(fieldName) => isFieldInStringGroup(fieldName, groupPath),
)

fieldsToDelete.forEach((field) => {
this.form.deleteField(field)
})

const emptyValue = Array.isArray(currentValue) ? [] : {}
this.form.setFieldValue(groupPath, emptyValue as never, {
dontUpdateMeta: true,
})

return
}

const fieldsMap = this.fieldsMap as FieldsMap<TFormData, TFieldGroupData>
for (const key in fieldsMap) {
this.deleteField(key)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Pushes a value into an array field.
*/
Expand Down Expand Up @@ -457,6 +495,20 @@ export class FieldGroupApi<
)
}

/**
* Replaces all field values in this field group with the provided values.
*/
replaceAllFields = (fields: TFieldGroupData) => {
for (const fieldName of Object.keys(
fields as object,
) as (keyof TFieldGroupData)[]) {
this.setFieldValue(
fieldName as unknown as DeepKeys<TFieldGroupData>,
fields[fieldName] as never,
)
}
}
Comment on lines +501 to +510
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

replaceAllFields does a shallow per-key write; surplus existing keys remain.

Because each provided key is written via setFieldValue, any keys that exist in the current form subtree but aren't present on the incoming fields object are not cleared. The name "replaceAllFields" implies a full replacement. For the string-group case, a single this.form.setFieldValue(this.fieldsMap, fields) would give true replace semantics; for the mapped case, keys outside fieldsMap are inherently out of scope, which is fine.

At minimum, please clarify the intended semantics in the JSDoc (partial overwrite vs. full replace) so callers don't get surprised. If full replace is intended, switch the string-group path to a single setFieldValue on the group root.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/form-core/src/FieldGroupApi.ts` around lines 486 - 495, The current
replaceAllFields implementation iterates keys and calls setFieldValue per key,
which leaves any existing keys not present in the incoming fields object
untouched; clarify or change behavior: update the JSDoc for replaceAllFields to
state whether it performs a partial overwrite (only provided keys are updated)
or a full replacement (missing keys are cleared), and if full replace is
intended then for the string-group path call
this.form.setFieldValue(this.fieldsMap, fields) once to replace the whole
subtree instead of per-key writes; keep the mapped-case behavior as-is (keys
outside fieldsMap are out of scope) and reference replaceAllFields,
setFieldValue, this.form.setFieldValue, and this.fieldsMap in the change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one sounds like a miss. At least I can't make much sense of it. Focus on the other errors for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LeCarbonator, understood — I'll set this aside and focus on the other issues in the PR. Thanks for the clarification!


/**
* Removes a value from an array field at the specified index.
*/
Expand Down Expand Up @@ -512,12 +564,41 @@ export class FieldGroupApi<
}

/**
* Resets the field value and meta to default state
* Resets the field value and meta to default state.
*/
resetField = <TField extends DeepKeys<TFieldGroupData>>(field: TField) => {
return this.form.resetField(this.getFormFieldName(field))
}

/**
* Resets all field values and meta within this field group.
*/
resetAllFields = () => {
if (typeof this.fieldsMap === 'string') {
const groupPath = this.fieldsMap.toString()

const fieldsToReset = Object.keys(this.form.fieldInfo).filter(
(fieldName) => isFieldInStringGroup(fieldName, groupPath),
)

fieldsToReset.forEach((field) => this.form.resetField(field))

if (this.form.options.defaultValues !== undefined) {
const resetValue = getBy(this.form.options.defaultValues, groupPath)
this.form.setFieldValue(groupPath, resetValue as never, {
dontUpdateMeta: true,
})
}
return
}

const fieldsMap = this.fieldsMap as FieldsMap<TFormData, TFieldGroupData>

for (const key in fieldsMap) {
this.resetField(key)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

validateAllFields = (cause: ValidationCause) =>
this.form.validateAllFields(cause)
}
Loading
Loading