fix(components): [form/form-item] initial value and reset func#23597
Conversation
|
👋 @l246804, seems like this is your first time contribution to element-plus. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorDocumentation mismatch: JSDoc doesn't match implementation behavior.
The implementation at line 112 replaces the entire
initialValuescache withcloneDeep(initModel). However, the JSDoc comment at line 289 states:"Only fields present in
initModelwill 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 initializinginitialValuessynchronously for robustness.The
initialValuesis set inonMounted, butresetFieldandsetInitialValuecould theoretically be called before mount in certain edge cases (e.g., via exposed refs during SSR or testing scenarios). WhilesetInitialValuehas a guard with a warning,resetFieldat line 81 will silently usegetProp(initialValues, prop)whereinitialValuesisundefined, potentially causing unexpected behavior.Consider either:
- Adding a guard to
resetFieldsimilar tosetInitialValue- Initializing synchronously:
let initialValues: any = props.model ? cloneDeep(props.model) : undefinedOption 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:removedFieldPropCacheshould be cleared after resetting removed fields.When
resetFields()is called without specific properties, removed fields are reset but their entries remain inremovedFieldPropCache. 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() }
There was a problem hiding this comment.
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 whenskipNormalizeis true.When
skipNormalize=true, the code castspropsdirectly tostring[]. However, the typeArrayable<FormItemProp>permits a single string value (e.g.,"foo"). If a caller mistakenly passes a single string withskipNormalize=true, iterating overprops as string[]would iterate over individual characters rather than treating it as a single-element array.Current usage in
form.vueis safe sincepropStringsis pre-computed asstring[], but the function signature allows incorrect usage.Consider either:
- Documenting that
skipNormalizerequirespropsto already bestring[], or- 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
commit: |
|
🧪 Playground Preview: https://element-plus.run/?pr=23597 |
There was a problem hiding this comment.
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 | 🟠 MajorImplementation doesn't match documentation.
The JSDoc comment at line 308 states "Only fields present in
initModelwill be updated", but the implementation at line 119 completely replacesinitialValueswithcloneDeep(initModel). This means fields NOT present ininitModelwill lose their initial values and reset toundefinedon subsequentresetFields()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,
There was a problem hiding this comment.
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 | 🟡 MinorClarify 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.
There was a problem hiding this comment.
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
setInitialValuesapplies to ALL registered fields, including those not ininitModel.
getProp(initModel, field.prop).valuereturnsundefinedfor props missing frominitModel, so every field's initial value is overwritten — matching the documentation atform.mdline 172. However, the JSDoc on line 294 still says "Only fields present ininitModelwill 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:removedFieldsretains fullFormItemContextreferences indefinitely.Entries are only cleaned up inside
resetFields(line 121) when a currently-active field matches the samepropString. 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 uniquepropStringvalues and unlikely to matter for typical forms, but worth being aware of.Also applies to: 74-80
There was a problem hiding this comment.
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 cleaningremovedFieldsinaddFieldto avoid stale entries.When a field is re-mounted with the same
propString,addFieldpushes the new instance intofieldsbut the old instance remains inremovedFields. This stale entry is harmless (it's cleaned up inresetFieldsat line 122, andremoveFieldoverwrites it on the next unmount), but it meanssetInitialValueswill unnecessarily callsetInitialValueon the stale context in the interim.♻️ Optional cleanup
const addField: FormContext['addField'] = (field) => { fields.push(field) + if (field.prop) { + removedFields.delete(field.propString) + } }
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/components/form/src/form.vue (1)
117-117:resetField()is async but its return value (Promise) is silently discarded byforEach.The static analysis tool correctly flags this.
field.resetField()returns aPromisethat is not awaited, so any error thrown insideresetFieldwould 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...ofloop 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 fromaddField.When a field is first mounted,
setInitialValue(fieldValue.value)is called (line 411), which doescloneDeep(value). Then inaddField(form.vue line 77),cloneDeep(field.fieldValue)stores another deep clone into the Map. So the Map gets a clone, and the localinitialValuegets 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,
addFieldcallsfield.setInitialValue(initialValues.get(...))(form.vue line 75) which clones the Map value. The priorsetInitialValuecall on line 411 is then immediately overwritten. This is harmless but slightly wasteful. If performance on mount/unmount cycles matters, thesetInitialValuecall on line 411 could be deferred to afteraddFieldonly 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.
Co-authored-by: btea <[email protected]>
7bbb494 to
b0395f0
Compare
|
@l246804 Thanks for your contribution! ❤️ |

Please make sure these boxes are checked before submitting your PR, thank you!
devbranch.修复 #23013 (comment)
针对 PR 方案 3 的实现
Summary by CodeRabbit
Bug Fixes
Documentation
setInitialValuesmethod behavior and field reset functionality.