Skip to content

fix(components): [form/form-item] initial value and reset func#23597

Merged
btea merged 18 commits into
element-plus:devfrom
l246804:fix/resetFields
Feb 27, 2026
Merged

fix(components): [form/form-item] initial value and reset func#23597
btea merged 18 commits into
element-plus:devfrom
l246804:fix/resetFields

Conversation

@l246804
Copy link
Copy Markdown
Contributor

@l246804 l246804 commented Feb 3, 2026

Please make sure these boxes are checked before submitting your PR, thank you!

  • Make sure you follow contributing guide English | (中文 | Español | Français).
  • Make sure you are merging your commits to dev branch.
  • Add some descriptions and refer to relative issues for your PR.

修复 #23013 (comment)

针对 PR 方案 3 的实现

FormItem 只要挂载就记录其 prop,卸载只是其表现形式上不再接受任何操作,但是在重置时是可以保证将相关 prop 重置到指定数据的,比较符合开发直觉以及开发习惯。

Summary by CodeRabbit

  • Bug Fixes

    • Corrected form reset behavior when using partial initial value updates. Fields not included in the update now reset to undefined instead of retaining previous values.
  • Documentation

    • Updated Form component documentation to clarify setInitialValues method behavior and field reset functionality.

@pull-request-triage
Copy link
Copy Markdown

👋 @l246804, seems like this is your first time contribution to element-plus.
Please make sure that you have read our guidelines and code of conduct before making a contribution.

@pull-request-triage pull-request-triage Bot added 1st contribution Their very first contribution Needs Review labels Feb 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 3, 2026

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request modifies form initial value handling by switching from shallow to deep cloning, introducing internal Map-based tracking of per-field initial values, and updating the setInitialValues method to synchronize all fields and reset unmapped fields to undefined during form reset operations.

Changes

Cohort / File(s) Summary
Form Item Deep Cloning
packages/components/form/src/form-item.vue
Replaced shallow clone with cloneDeep for field value handling during initialization and reset operations; updated imports accordingly.
Form Initial Values Management
packages/components/form/src/form.vue
Introduced initialValues Map to track per-field initial values; modified setInitialValues to pre-populate all tracked fields and reset unmapped fields to undefined; enhanced addField and resetFields to maintain synchronization between the model and internal initial value state.
Form Test Expectations
packages/components/form/__tests__/form.test.tsx
Updated test assertions to reflect new behavior where partial initial value updates result in unmapped address field becoming undefined on reset instead of preserving mounted value.
Form API Documentation
docs/en-US/component/form.md
Updated setInitialValues documentation to clarify that all initialized fields are updated and unmapped fields reset to undefined; adjusted table formatting.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #23013: Modifies form initial-value APIs and reset behavior across the same components and functions.

Suggested reviewers

  • btea
  • rzzf

Poem

🐰 Deep cloning all the values, not just scratching at the surface,
A Map to track their origins serves a grander purpose,
When fields take leave, their ghosts now fade away,
Resets run cleaner in the form's ballet! 📋✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description includes all required checklist items (marked complete), provides a specific issue reference, and explains the implementation approach being followed from a related PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: fixing initial value handling and reset functionality for form and form-item components, which aligns with the substantial changes across both components and their test files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/components/form/src/form.vue (1)

100-113: ⚠️ Potential issue | 🟡 Minor

Documentation mismatch: JSDoc doesn't match implementation behavior.

The implementation at line 112 replaces the entire initialValues cache with cloneDeep(initModel). However, the JSDoc comment at line 289 states:

"Only fields present in initModel will be updated."

This documentation is now incorrect. Either update the documentation to reflect the new behavior, or change the implementation to merge values as previously documented.

Option A: Fix documentation
   /**
-   * `@description` Set initial values for form fields. When `resetFields` is called, fields will reset to these values. Only fields present in `initModel` will be updated.
+   * `@description` Set initial values for form fields. When `resetFields` is called, fields will reset to these values. This replaces all initial values; fields not present in `initModel` will reset to `undefined`.
    */
   setInitialValues,
Option B: Preserve merge behavior
 const setInitialValues: FormContext['setInitialValues'] = (initModel) => {
   if (!props.model) {
     debugWarn(COMPONENT_NAME, 'model is required for setInitialValues to work.')
     return
   }
   if (!initModel) {
     debugWarn(
       COMPONENT_NAME,
       'initModel is required for setInitialValues to work.'
     )
     return
   }
-  initialValues = cloneDeep(initModel)
+  if (!initialValues) {
+    initialValues = cloneDeep(initModel)
+  } else {
+    Object.keys(initModel).forEach((key) => {
+      setInitialValue(key, initModel[key])
+    })
+  }
 }
🤖 Fix all issues with AI agents
In `@packages/components/form/__tests__/form.test.tsx`:
- Around line 1140-1141: The test shows partial setInitialValues now clears
unspecified fields because setInitialValues in form.vue replaces the entire
initialValues cache; change setInitialValues to merge the incoming initModel
into the existing initialValues (e.g., shallow/deep merge) instead of replacing
it so fields not present in initModel keep their previous mounted values; update
the implementation at the setInitialValues function/handler to merge (using
cloneDeep or equivalent if deep objects are expected) and adjust or add a unit
test confirming partial updates preserve unspecified fields.
🧹 Nitpick comments (2)
packages/components/form/src/form.vue (2)

46-53: Consider initializing initialValues synchronously for robustness.

The initialValues is set in onMounted, but resetField and setInitialValue could theoretically be called before mount in certain edge cases (e.g., via exposed refs during SSR or testing scenarios). While setInitialValue has a guard with a warning, resetField at line 81 will silently use getProp(initialValues, prop) where initialValues is undefined, potentially causing unexpected behavior.

Consider either:

  1. Adding a guard to resetField similar to setInitialValue
  2. Initializing synchronously: let initialValues: any = props.model ? cloneDeep(props.model) : undefined
Option 1: Add guard to resetField
 const resetField: FormContext['resetField'] = (prop) => {
   if (!props.model) return
+  if (!initialValues) {
+    debugWarn(COMPONENT_NAME, 'initialValues is not initialized yet.')
+    return
+  }

   const computedValue = getProp(props.model, prop)
   computedValue.value = cloneDeep(getProp(initialValues, prop).value)
 }

129-136: Memory consideration: removedFieldPropCache should be cleared after resetting removed fields.

When resetFields() is called without specific properties, removed fields are reset but their entries remain in removedFieldPropCache. This means the cache grows unboundedly in dynamic forms where fields are frequently added and removed.

Clear cache after resetting removed fields
   // then reset the removed fields if no specific properties are provided
   if (properties.length === 0) {
     removedFieldPropCache.forEach((prop) => {
       if (has(initialValues, prop)) {
         resetField(prop)
       }
     })
+    removedFieldPropCache.clear()
   }

Comment thread packages/components/form/__tests__/form.test.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/components/form/src/form.vue`:
- Around line 83-88: The resetField implementation uses getProp(initialValues,
prop) without ensuring initialValues is defined; update resetField to guard like
setInitialValue does by returning early if initialValues is falsy, then proceed
to read and clone the initial value; reference the resetField function,
initialValues variable, and getProp to locate and apply the guard to prevent
accessing undefined.
🧹 Nitpick comments (1)
packages/components/form/src/utils.ts (1)

50-62: Type safety concern when skipNormalize is true.

When skipNormalize=true, the code casts props directly to string[]. However, the type Arrayable<FormItemProp> permits a single string value (e.g., "foo"). If a caller mistakenly passes a single string with skipNormalize=true, iterating over props as string[] would iterate over individual characters rather than treating it as a single-element array.

Current usage in form.vue is safe since propStrings is pre-computed as string[], but the function signature allows incorrect usage.

Consider either:

  1. Documenting that skipNormalize requires props to already be string[], or
  2. Adding a minimal guard:
💡 Optional: Add type guard for skipNormalize path
 export const filterFields = (
   fields: FormItemContext[],
   props: Arrayable<FormItemProp>,
   skipNormalize?: boolean
 ) => {
   const normalized = skipNormalize
-    ? (props as string[])
+    ? (isArray(props) ? props : [props]) as string[]
     : ensureArray(props).map((prop) => (isArray(prop) ? prop.join('.') : prop))
   return normalized.length > 0

Comment thread packages/components/form/src/form.vue Outdated
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 3, 2026

Open in StackBlitz

pnpm add https://pkg.pr.new/element-plus@23597
npm i https://pkg.pr.new/element-plus@23597
yarn add https://pkg.pr.new/[email protected]

commit: 4200b4c

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 3, 2026

🧪 Playground Preview: https://element-plus.run/?pr=23597
Please comment the example via this playground if needed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/components/form/src/form.vue (1)

107-120: ⚠️ Potential issue | 🟠 Major

Implementation doesn't match documentation.

The JSDoc comment at line 308 states "Only fields present in initModel will be updated", but the implementation at line 119 completely replaces initialValues with cloneDeep(initModel). This means fields NOT present in initModel will lose their initial values and reset to undefined on subsequent resetFields() calls.

Either update the implementation to merge values, or correct the documentation to reflect the replacement behavior.

🛠️ Option A: Merge implementation to match docs
-  initialValues = cloneDeep(initModel)
+  Object.keys(initModel).forEach((key) => {
+    const computedValue = getProp(initialValues, key)
+    computedValue.value = cloneDeep(getProp(initModel, key).value)
+  })
📝 Option B: Update docs to match implementation
   /**
-   * `@description` Set initial values for form fields. When `resetFields` is called, fields will reset to these values. Only fields present in `initModel` will be updated.
+   * `@description` Replace initial values for form fields. When `resetFields` is called, fields will reset to these values. This completely replaces all previous initial values.
    */
   setInitialValues,

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/components/form/src/form.vue (1)

107-120: ⚠️ Potential issue | 🟡 Minor

Clarify partial updates vs full replace in setInitialValues.

This replaces the entire cache, so missing keys are effectively cleared and later reset to undefined. If the intended API is “update only provided fields,” consider merging with existing values; otherwise, please update the docs to reflect full replacement.

💡 Optional merge-based fix
-  initialValues = cloneDeep(initModel)
+  initialValues = cloneDeep({
+    ...(initialValues ?? {}),
+    ...initModel,
+  })
🤖 Fix all issues with AI agents
In `@packages/components/form/src/form.vue`:
- Around line 52-59: The code initializes initialValues inside onMounted which
leaves a timing gap where setInitialValue may be called before mount and no-op;
move the initialization into setup by setting initialValues =
cloneDeep(props.model) immediately (replace the onMounted assignment) so
initialValues is available during initial render; keep removedFieldPropCache and
onMounted only if other mount-only logic remains, and ensure functions like
setInitialValue and any refs read initialValues directly after setup.

Comment thread packages/components/form/src/form.vue Outdated
Comment thread packages/components/form/src/form-item.vue Outdated
@l246804 l246804 requested review from btea and kooriookami February 6, 2026 02:40
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/components/form/src/form.vue (1)

82-101: ⚠️ Potential issue | 🟡 Minor

setInitialValues applies to ALL registered fields, including those not in initModel.

getProp(initModel, field.prop).value returns undefined for props missing from initModel, so every field's initial value is overwritten — matching the documentation at form.md line 172. However, the JSDoc on line 294 still says "Only fields present in initModel will be updated", which is now stale after this behavior change.

📝 Update the JSDoc to match the actual behavior
   /**
-   * `@description` Set initial values for form fields. When `resetFields` is called, fields will reset to these values. Only fields present in `initModel` will be updated.
+   * `@description` Set initial values for form fields. When `resetFields` is called, fields will reset to these values. All initialized fields (including unmounted ones) will be updated. Fields not defined in `initModel` will be reset to `undefined`.
    */
   setInitialValues,
🧹 Nitpick comments (1)
packages/components/form/src/form.vue (1)

50-50: removedFields retains full FormItemContext references indefinitely.

Entries are only cleaned up inside resetFields (line 121) when a currently-active field matches the same propString. For long-lived forms with highly dynamic fields (e.g., a list that adds/removes items with unique props), stale contexts (including their reactive refs) accumulate. In practice this is bounded by unique propString values and unlikely to matter for typical forms, but worth being aware of.

Also applies to: 74-80

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/components/form/src/form.vue`:
- Around line 95-102: The implementation currently calls
field.setInitialValue(...) for every FormItemContext in removedFields and fields
even when getProp(initModel, field.prop) returns undefined; change the
eachCallback so it first calls getProp(initModel, field.prop) and only calls
field.setInitialValue(...) when that result is defined (e.g., prop !==
undefined) so fields not present in initModel are not assigned an undefined
initial value; update references in the loop over removedFields and fields to
use this guarded check and keep function names eachCallback, getProp,
setInitialValue, removedFields, fields, and FormItemContext to locate the code.
🧹 Nitpick comments (1)
packages/components/form/src/form.vue (1)

70-79: Consider cleaning removedFields in addField to avoid stale entries.

When a field is re-mounted with the same propString, addField pushes the new instance into fields but the old instance remains in removedFields. This stale entry is harmless (it's cleaned up in resetFields at line 122, and removeField overwrites it on the next unmount), but it means setInitialValues will unnecessarily call setInitialValue on the stale context in the interim.

♻️ Optional cleanup
 const addField: FormContext['addField'] = (field) => {
   fields.push(field)
+  if (field.prop) {
+    removedFields.delete(field.propString)
+  }
 }

Comment thread packages/components/form/src/form.vue Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/components/form/src/form.vue (1)

117-117: resetField() is async but its return value (Promise) is silently discarded by forEach.

The static analysis tool correctly flags this. field.resetField() returns a Promise that is not awaited, so any error thrown inside resetField would become an unhandled rejection. The subsequent lines (119–133) don't depend on the resets completing, so this isn't a correctness bug in the current flow, but it silently swallows potential errors.

Consider using a for...of loop or collecting and awaiting the promises if error propagation matters:

♻️ Suggested approach
-  filterFields(fields, properties).forEach((field) => field.resetField())
+  for (const field of filterFields(fields, properties)) {
+    field.resetField()
+  }

This avoids the linter warning while keeping the fire-and-forget semantics explicit.

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

In `@packages/components/form/src/form.vue` at line 117, The call
filterFields(fields, properties).forEach((field) => field.resetField()) discards
the Promise returned by field.resetField(), risking unhandled rejections; update
this to either (a) await each reset if you want errors to propagate (use a
for...of loop and await field.resetField()), or (b) keep fire-and-forget but
handle errors explicitly by collecting promises and using
Promise.allSettled(filterFields(...).map(f => f.resetField())) (or Promise.all
with try/catch) so resetField() promises are not silently ignored; locate the
call by the symbols filterFields and resetField in form.vue and implement one of
these patterns.
packages/components/form/src/form-item.vue (1)

369-371: Double deep-clone when called from addField.

When a field is first mounted, setInitialValue(fieldValue.value) is called (line 411), which does cloneDeep(value). Then in addField (form.vue line 77), cloneDeep(field.fieldValue) stores another deep clone into the Map. So the Map gets a clone, and the local initialValue gets a separate clone — both from the same source. This is correct (they should be independent snapshots), just noting the two clones are intentional.

On re-mount, addField calls field.setInitialValue(initialValues.get(...)) (form.vue line 75) which clones the Map value. The prior setInitialValue call on line 411 is then immediately overwritten. This is harmless but slightly wasteful. If performance on mount/unmount cycles matters, the setInitialValue call on line 411 could be deferred to after addField only when the Map doesn't have a cached value. That said, this is a negligible concern for typical form sizes.

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

In `@packages/components/form/src/form-item.vue` around lines 369 - 371, The
current mount flow causes an extra deep-clone: field calls
setInitialValue(fieldValue.value) which cloneDeeps into local initialValue, and
addField then cloneDeeps field.fieldValue into the form's initialValues Map, or
on remount addField later overwrites the local initialValue with a clone from
the Map. Fix by deferring the field-side clone: in addField (the function that
registers fields and writes into the initialValues Map) ensure the Map is
populated first and then call field.setInitialValue(initialValues.get(key) ??
field.fieldValue) so setInitialValue only clones when there is no cached value;
alternatively populate the Map before invoking field.setInitialValue so the
field receives the already-stored snapshot and avoids an immediate redundant
clone. Use identifiers addField, initialValues, field.setInitialValue,
field.fieldValue, and the local initialValue in FormItem to implement this
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/components/form/src/form.vue`:
- Around line 100-108: The docstring for setInitialValues is incorrect: the
implementation (loop over initialValues.keys() using getProp(initModel,
key).value and the fields.forEach calling field.setInitialValue(...)) will
overwrite all tracked keys with values from initModel, setting missing keys to
undefined; update the docstring for setInitialValues to state that all tracked
fields are updated from initModel (and that keys absent in initModel will be set
to undefined), and mention behavior for fields with a prop calling
setInitialValue, so the comment matches the actual behavior of initialValues,
getProp, and field.setInitialValue.

---

Nitpick comments:
In `@packages/components/form/src/form-item.vue`:
- Around line 369-371: The current mount flow causes an extra deep-clone: field
calls setInitialValue(fieldValue.value) which cloneDeeps into local
initialValue, and addField then cloneDeeps field.fieldValue into the form's
initialValues Map, or on remount addField later overwrites the local
initialValue with a clone from the Map. Fix by deferring the field-side clone:
in addField (the function that registers fields and writes into the
initialValues Map) ensure the Map is populated first and then call
field.setInitialValue(initialValues.get(key) ?? field.fieldValue) so
setInitialValue only clones when there is no cached value; alternatively
populate the Map before invoking field.setInitialValue so the field receives the
already-stored snapshot and avoids an immediate redundant clone. Use identifiers
addField, initialValues, field.setInitialValue, field.fieldValue, and the local
initialValue in FormItem to implement this change.

In `@packages/components/form/src/form.vue`:
- Line 117: The call filterFields(fields, properties).forEach((field) =>
field.resetField()) discards the Promise returned by field.resetField(), risking
unhandled rejections; update this to either (a) await each reset if you want
errors to propagate (use a for...of loop and await field.resetField()), or (b)
keep fire-and-forget but handle errors explicitly by collecting promises and
using Promise.allSettled(filterFields(...).map(f => f.resetField())) (or
Promise.all with try/catch) so resetField() promises are not silently ignored;
locate the call by the symbols filterFields and resetField in form.vue and
implement one of these patterns.

@l246804 l246804 requested a review from Dsaquel February 24, 2026 06:22
Comment thread docs/en-US/component/form.md Outdated
Co-authored-by: btea <[email protected]>
@btea btea changed the title fix(components): [form][form-item] initial value and reset func fix(components): [form/form-item] initial value and reset func Feb 27, 2026
@btea btea merged commit 91b46a7 into element-plus:dev Feb 27, 2026
15 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

@l246804 Thanks for your contribution! ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants