Skip to content

Commit f359660

Browse files
committed
feat: allow default value overrides form data
1 parent c967820 commit f359660

File tree

8 files changed

+201
-31
lines changed

8 files changed

+201
-31
lines changed

packages/docs/docs/api-reference/form-props.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ NOTE: If there is a default for a field and the `formData` is unspecified, the d
269269
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
270270
| `useFormDataIfPresent` | Legacy behavior - Do not merge defaults if there is a value for a field in `formData` even if that value is explicitly set to `undefined` |
271271
| `useDefaultIfFormDataUndefined` | If the value of a field within the `formData` is `undefined`, then use the default value instead |
272+
| `useDefault` | Always use the default value instead of form data |
273+
274+
|
272275

273276
## experimental_customMergeAllOf
274277

packages/docs/docs/api-reference/utility-functions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ Merges the `defaults` object of type `T` into the `formData` of type `T`
661661
When merging defaults and form data, we want to merge in this specific way:
662662

663663
- objects are deeply merged
664-
- arrays are merged in such a way that:
664+
- arrays are either replaced (when `defaultSupercedes` is true) or merged in such a way that:
665665
- when the array is set in form data, only array entries set in form data are deeply merged; additional entries from the defaults are ignored unless `mergeExtraArrayDefaults` is true, in which case the extras are appended onto the end of the form data
666666
- when the array is not set in form data, the default is copied over
667667
- scalars are overwritten/set by form data
@@ -672,6 +672,7 @@ When merging defaults and form data, we want to merge in this specific way:
672672
- [formData]: T | undefined - The form data into which the defaults will be merged
673673
- [mergeExtraArrayDefaults=false]: boolean - If true, any additional default array entries are appended onto the formData
674674
- [defaultSupercedesUndefined=false]: boolean - If true, an explicit undefined value will be overwritten by the default value
675+
- [defaultSupercedes=false]: boolean - If true, a value will be overwritten by the default value
675676

676677
#### Returns
677678

packages/playground/src/components/Header.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ const liveSettingsSelectSchema: RJSFSchema = {
201201
title: 'Use default for undefined field value',
202202
enum: ['useDefaultIfFormDataUndefined'],
203203
},
204+
{
205+
type: 'string',
206+
title: 'Always use default for field value',
207+
enum: ['useDefault'],
208+
},
204209
],
205210
},
206211
},

packages/utils/src/mergeDefaultsWithFormData.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import isNil from 'lodash/isNil';
2020
* @param [formData] - The form data into which the defaults will be merged
2121
* @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData
2222
* @param [defaultSupercedesUndefined=false] - If true, an explicit undefined value will be overwritten by the default value
23-
* @param [overrideFormDataWithDefaults=false] - If true, the default value will overwrite the form data value. If the value
23+
* @param [overrideFormDataWithDefaultsStrategy='noop'] - If not 'noop', the default value will overwrite the form data value. Values can be either replaced or merged, if the value
2424
* doesn't exist in the default, we take it from formData and in the case where the value is set to undefined in formData.
2525
* This is useful when we have already merged formData with defaults and want to add an additional field from formData
2626
* that does not exist in defaults.
@@ -31,14 +31,14 @@ export default function mergeDefaultsWithFormData<T = any>(
3131
formData?: T,
3232
mergeExtraArrayDefaults = false,
3333
defaultSupercedesUndefined = false,
34-
overrideFormDataWithDefaults = false,
34+
overrideFormDataWithDefaultsStrategy: 'noop' | 'replace' | 'merge' = 'noop',
3535
): T | undefined {
3636
if (Array.isArray(formData)) {
3737
const defaultsArray = Array.isArray(defaults) ? defaults : [];
3838

39-
// If overrideFormDataWithDefaults is true, we want to override the formData with the defaults
40-
const overrideArray = overrideFormDataWithDefaults ? defaultsArray : formData;
41-
const overrideOppositeArray = overrideFormDataWithDefaults ? formData : defaultsArray;
39+
// If overrideFormDataWithDefaultsStrategy is not noop, we want to override the formData with the defaults
40+
const overrideArray = overrideFormDataWithDefaultsStrategy !== 'noop' ? defaultsArray : formData;
41+
const overrideOppositeArray = overrideFormDataWithDefaultsStrategy !== 'noop' ? formData : defaultsArray;
4242

4343
const mapped = overrideArray.map((value, idx) => {
4444
// We want to explicitly make sure that the value is NOT undefined since null, 0 and empty space are valid values
@@ -48,33 +48,43 @@ export default function mergeDefaultsWithFormData<T = any>(
4848
formData[idx],
4949
mergeExtraArrayDefaults,
5050
defaultSupercedesUndefined,
51-
overrideFormDataWithDefaults,
51+
overrideFormDataWithDefaultsStrategy,
5252
);
5353
}
5454
return value;
5555
});
5656

5757
// Merge any extra defaults when mergeExtraArrayDefaults is true
58-
// Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array
59-
if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) {
58+
// Or when overrideFormDataWithDefaults is not noop and the default array is shorter than the formData array
59+
if (
60+
(mergeExtraArrayDefaults || overrideFormDataWithDefaultsStrategy === 'merge') &&
61+
mapped.length < overrideOppositeArray.length
62+
) {
6063
mapped.push(...overrideOppositeArray.slice(mapped.length));
6164
}
6265
return mapped as unknown as T;
6366
}
6467
if (isObject(formData)) {
68+
const iterationSource = overrideFormDataWithDefaultsStrategy === 'replace' ? (defaults ?? {}) : formData;
6569
const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object.
66-
return Object.keys(formData as GenericObjectType).reduce((acc, key) => {
70+
return Object.keys(iterationSource as GenericObjectType).reduce((acc, key) => {
6771
const keyValue = get(formData, key);
6872
const keyExistsInDefaults = isObject(defaults) && key in (defaults as GenericObjectType);
6973
const keyExistsInFormData = key in (formData as GenericObjectType);
74+
// overrideFormDataWithDefaultsStrategy can be 'merge' only when the key value exists in defaults
75+
// Or if the key value doesn't exist in formData
76+
const keyOverrideDefaultStrategy =
77+
overrideFormDataWithDefaultsStrategy === 'replace'
78+
? 'replace'
79+
: keyExistsInDefaults || !keyExistsInFormData
80+
? overrideFormDataWithDefaultsStrategy
81+
: 'noop';
7082
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
7183
defaults ? get(defaults, key) : {},
7284
keyValue,
7385
mergeExtraArrayDefaults,
7486
defaultSupercedesUndefined,
75-
// overrideFormDataWithDefaults can be true only when the key value exists in defaults
76-
// Or if the key value doesn't exist in formData
77-
overrideFormDataWithDefaults && (keyExistsInDefaults || !keyExistsInFormData),
87+
keyOverrideDefaultStrategy,
7888
);
7989
return acc;
8090
}, acc);
@@ -89,10 +99,10 @@ export default function mergeDefaultsWithFormData<T = any>(
8999
if (
90100
(defaultSupercedesUndefined &&
91101
((!isNil(defaults) && isNil(formData)) || (typeof formData === 'number' && isNaN(formData)))) ||
92-
(overrideFormDataWithDefaults && !isNil(formData))
102+
(overrideFormDataWithDefaultsStrategy === 'merge' && !isNil(formData))
93103
) {
94104
return defaults;
95105
}
96106

97-
return formData;
107+
return overrideFormDataWithDefaultsStrategy === 'replace' ? defaults : formData;
98108
}

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
363363
matchingFormData as T,
364364
mergeExtraDefaults,
365365
true,
366+
experimental_defaultFormStateBehavior?.mergeDefaultsIntoFormData === 'useDefault' ? 'replace' : 'noop',
366367
) as T;
367368
}
368369
}
@@ -732,7 +733,7 @@ export default function getDefaultFormState<
732733
formData,
733734
true, // set to true to add any additional default array entries.
734735
defaultSupercedesUndefined,
735-
true, // set to true to override formData with defaults if they exist.
736+
'merge', // set to 'merge' to override formData with defaults if they exist.
736737
);
737738
return result;
738739
}

packages/utils/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ export type Experimental_DefaultFormStateBehavior = {
9898
* even if that value is explicitly set to `undefined`
9999
* - `useDefaultIfFormDataUndefined`: - If the value of a field within the `formData` is `undefined`, then use the
100100
* default value instead
101+
* - `useDefault`: - Always use the default value
101102
*/
102-
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined';
103+
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined' | 'useDefault';
103104
/** Optional enumerated flag controlling how const values are merged into the form data as defaults when dealing with
104105
* undefined values, defaulting to `always`. The defaulting behavior for this flag will always be controlled by the
105106
* `emptyObjectField` flag value. For instance, if `populateRequiredDefaults` is set and the const value is not

packages/utils/test/mergeDefaultsWithFormData.test.ts

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,13 @@ describe('mergeDefaultsWithFormData()', () => {
141141
expect(mergeDefaultsWithFormData(obj1, obj2)?.a).toBeInstanceOf(File);
142142
});
143143

144-
describe('test with overrideFormDataWithDefaults set to true', () => {
144+
describe('test with overrideFormDataWithDefaults set to `merge`', () => {
145145
it('should return data in formData when no defaults', () => {
146-
expect(mergeDefaultsWithFormData(undefined, [2], undefined, undefined, true)).toEqual([2]);
146+
expect(mergeDefaultsWithFormData(undefined, [2], undefined, undefined, 'merge')).toEqual([2]);
147147
});
148148

149149
it('should return formData when formData is undefined', () => {
150-
expect(mergeDefaultsWithFormData({}, undefined, undefined, undefined, true)).toEqual(undefined);
150+
expect(mergeDefaultsWithFormData({}, undefined, undefined, undefined, 'merge')).toEqual(undefined);
151151
});
152152

153153
it('should deeply merge and return formData when formData is undefined and defaultSupercedesUndefined false', () => {
@@ -170,7 +170,7 @@ describe('mergeDefaultsWithFormData()', () => {
170170
},
171171
undefined,
172172
undefined,
173-
true,
173+
'merge',
174174
),
175175
).toEqual({
176176
arrayWithDefaults: [null],
@@ -183,42 +183,42 @@ describe('mergeDefaultsWithFormData()', () => {
183183
});
184184

185185
it('should return default when formData is undefined and defaultSupercedesUndefined true', () => {
186-
expect(mergeDefaultsWithFormData({}, undefined, undefined, true, true)).toEqual({});
186+
expect(mergeDefaultsWithFormData({}, undefined, undefined, true, 'merge')).toEqual({});
187187
});
188188

189189
it('should return default when formData is null and defaultSupercedesUndefined true', () => {
190-
expect(mergeDefaultsWithFormData({}, null, undefined, true, true)).toEqual({});
190+
expect(mergeDefaultsWithFormData({}, null, undefined, true, 'merge')).toEqual({});
191191
});
192192

193193
it('should merge two one-level deep objects', () => {
194-
expect(mergeDefaultsWithFormData({ a: 1 }, { b: 2 }, undefined, undefined, true)).toEqual({
194+
expect(mergeDefaultsWithFormData({ a: 1 }, { b: 2 }, undefined, undefined, 'merge')).toEqual({
195195
a: 1,
196196
b: 2,
197197
});
198198
});
199199

200200
it('should override the first object with the values from the second', () => {
201-
expect(mergeDefaultsWithFormData({ a: 1 }, { a: 2 }, undefined, undefined, true)).toEqual({ a: 1 });
201+
expect(mergeDefaultsWithFormData({ a: 1 }, { a: 2 }, undefined, undefined, 'merge')).toEqual({ a: 1 });
202202
});
203203

204204
it('should override non-existing values of the first object with the values from the second', () => {
205205
expect(
206-
mergeDefaultsWithFormData({ a: { b: undefined } }, { a: { b: { c: 1 } } }, undefined, undefined, true),
206+
mergeDefaultsWithFormData({ a: { b: undefined } }, { a: { b: { c: 1 } } }, undefined, undefined, 'merge'),
207207
).toEqual({
208208
a: { b: { c: 1 } },
209209
});
210210
});
211211

212212
it('should merge arrays using entries from second', () => {
213-
expect(mergeDefaultsWithFormData([1, 2, 3], [4, 5], undefined, undefined, true)).toEqual([1, 2, 3]);
213+
expect(mergeDefaultsWithFormData([1, 2, 3], [4, 5], undefined, undefined, 'merge')).toEqual([1, 2, 3]);
214214
});
215215

216216
it('should merge arrays using entries from second and extra from the first', () => {
217-
expect(mergeDefaultsWithFormData([1, 2], [4, 5, 6], undefined, undefined, true)).toEqual([1, 2, 6]);
217+
expect(mergeDefaultsWithFormData([1, 2], [4, 5, 6], undefined, undefined, 'merge')).toEqual([1, 2, 6]);
218218
});
219219

220220
it('should deeply merge arrays with overlapping entries', () => {
221-
expect(mergeDefaultsWithFormData([{ a: 1 }], [{ b: 2 }, { c: 3 }], undefined, undefined, true)).toEqual([
221+
expect(mergeDefaultsWithFormData([{ a: 1 }], [{ b: 2 }, { c: 3 }], undefined, undefined, 'merge')).toEqual([
222222
{ a: 1, b: 2 },
223223
{ c: 3 },
224224
]);
@@ -256,7 +256,7 @@ describe('mergeDefaultsWithFormData()', () => {
256256
},
257257
c: 2,
258258
};
259-
expect(mergeDefaultsWithFormData<any>(obj1, obj2, undefined, undefined, true)).toEqual(expected);
259+
expect(mergeDefaultsWithFormData<any>(obj1, obj2, undefined, undefined, 'merge')).toEqual(expected);
260260
});
261261

262262
it('should recursively merge deeply nested objects, including extra array data', () => {
@@ -293,7 +293,7 @@ describe('mergeDefaultsWithFormData()', () => {
293293
c: 2,
294294
d: 4,
295295
};
296-
expect(mergeDefaultsWithFormData<any>(obj1, obj2, undefined, undefined, true)).toEqual(expected);
296+
expect(mergeDefaultsWithFormData<any>(obj1, obj2, undefined, undefined, 'merge')).toEqual(expected);
297297
});
298298

299299
it('should recursively merge File objects', () => {
@@ -307,4 +307,94 @@ describe('mergeDefaultsWithFormData()', () => {
307307
expect(mergeDefaultsWithFormData(obj1, obj2)?.a).toBeInstanceOf(File);
308308
});
309309
});
310+
311+
describe('test with overrideFormDataWithDefaults set to `replace`', () => {
312+
it('should return empty array even when no defaults', () => {
313+
expect(mergeDefaultsWithFormData(undefined, [2], undefined, undefined, 'replace')).toEqual([]);
314+
});
315+
316+
it('should return default when formData is undefined', () => {
317+
expect(mergeDefaultsWithFormData({}, undefined, undefined, undefined, 'replace')).toEqual({});
318+
});
319+
320+
it('should return default when formData is undefined and defaultSupercedesUndefined true', () => {
321+
expect(mergeDefaultsWithFormData({}, undefined, undefined, true, 'replace')).toEqual({});
322+
});
323+
324+
it('should return default when formData is null and defaultSupercedesUndefined true', () => {
325+
expect(mergeDefaultsWithFormData({}, null, undefined, true, 'replace')).toEqual({});
326+
});
327+
328+
it('should not merge two one-level deep objects', () => {
329+
expect(mergeDefaultsWithFormData({ a: 1 }, { b: 2 }, undefined, undefined, 'replace')).toEqual({
330+
a: 1,
331+
});
332+
});
333+
334+
it('should not override the first object with the values from the second', () => {
335+
expect(mergeDefaultsWithFormData({ a: 1 }, { a: 2 }, undefined, undefined, 'replace')).toEqual({ a: 1 });
336+
});
337+
338+
it('should not return undefined from defaults', () => {
339+
expect(
340+
mergeDefaultsWithFormData({ a: { b: undefined } }, { a: { b: { c: 1 } } }, undefined, undefined, 'replace'),
341+
).toEqual({
342+
a: { b: {} },
343+
});
344+
});
345+
346+
it('should not merge arrays using entries from second', () => {
347+
expect(mergeDefaultsWithFormData([1, 2, 3], [4, 5], undefined, undefined, 'replace')).toEqual([1, 2, 3]);
348+
});
349+
350+
it('should not deeply merge arrays with overlapping entries', () => {
351+
expect(mergeDefaultsWithFormData([{ a: 1 }], [{ b: 2 }, { c: 3 }], undefined, undefined, 'replace')).toEqual([
352+
{ a: 1 },
353+
]);
354+
});
355+
356+
it('should replace objects', () => {
357+
const obj1 = {
358+
a: 1,
359+
b: {
360+
c: 3,
361+
d: [1, 2, 3],
362+
e: { f: { g: 1 } },
363+
h: [{ i: 1 }, { i: 2 }],
364+
},
365+
c: 2,
366+
};
367+
const obj2 = {
368+
a: 1,
369+
b: {
370+
d: [3],
371+
e: { f: { h: 2 } },
372+
g: 1,
373+
h: [{ i: 3 }, { i: 4 }, { i: 5 }],
374+
},
375+
c: 3,
376+
d: 4,
377+
};
378+
expect(mergeDefaultsWithFormData<any>(obj1, obj2, undefined, undefined, 'replace')).toEqual({
379+
a: 1,
380+
b: {
381+
c: 3,
382+
d: [1, 2, 3],
383+
e: { f: { g: 1 } },
384+
h: [{ i: 1 }, { i: 2 }],
385+
},
386+
c: 2,
387+
});
388+
});
389+
390+
it('should replace arrays', () => {
391+
expect(mergeDefaultsWithFormData([1, 2], [4, 5, 6], undefined, undefined, 'replace')).toEqual([1, 2]);
392+
});
393+
394+
it('should replace objects', () => {
395+
expect(mergeDefaultsWithFormData({ a: { b: 1 } }, { a: { b: 2 } }, undefined, undefined, 'replace')).toEqual({
396+
a: { b: 1 },
397+
});
398+
});
399+
});
310400
});

0 commit comments

Comments
 (0)