Skip to content

Commit 26a1f69

Browse files
committed
[core, form] Add mergeFormDataAndSchemaDefaults method to Merger
1 parent 67ff48c commit 26a1f69

27 files changed

+350
-113
lines changed

.changeset/violet-wombats-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sjsf/form": minor
3+
---
4+
5+
Add `mergeFormDataAndSchemaDefaults` method to `Merger`

packages/form/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@
8686
"./legacy-omit-extra-data": {
8787
"types": "./dist/legacy-omit-extra-data.d.ts",
8888
"default": "./dist/legacy-omit-extra-data.js"
89+
},
90+
"./get-default-form-state": {
91+
"types": "./dist/get-default-form-state.d.ts",
92+
"default": "./dist/get-default-form-state.js"
8993
}
9094
}
9195
}
Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import mergeAllOf, { type Options } from "json-schema-merge-allof";
22

33
import type { Merger } from "./merger.js";
4-
import type { Schema } from "./schema.js";
4+
import type { Schema, SchemaValue } from "./schema.js";
5+
import type { Validator } from "./validator.js";
6+
import { getDefaultFormState2 } from "./default-state.js";
57

6-
export const defaultMerger: Merger = {
7-
mergeAllOf(schema) {
8+
export class DefaultMerger implements Merger {
9+
constructor(
10+
protected readonly validator: Validator,
11+
protected readonly rootSchema: Schema
12+
) {}
13+
14+
mergeAllOf(schema: Schema): Schema {
815
return mergeAllOf(schema, {
916
deep: false,
1017
} as Options) as Schema;
11-
},
12-
};
18+
}
19+
20+
mergeFormDataAndSchemaDefaults(
21+
formData: SchemaValue | undefined,
22+
schema: Schema
23+
): SchemaValue | undefined {
24+
return getDefaultFormState2(
25+
this.validator,
26+
this,
27+
schema,
28+
formData,
29+
this.rootSchema
30+
);
31+
}
32+
}

packages/form/src/core/default-state.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ import { RECURSIVE_REF, RECURSIVE_REF_ALLOF } from "./fixtures/test-data.js";
2222
import type { Validator } from "./validator.js";
2323
import type { Schema } from "./schema.js";
2424
import { makeTestValidator } from "./test-validator.js";
25-
import { defaultMerger } from './default-merger.js';
25+
import { DefaultMerger } from './default-merger.js';
26+
import type { Merger } from './merger.js';
2627

2728
let testValidator: Validator;
29+
let defaultMerger: Merger
2830

2931
beforeEach(() => {
3032
testValidator = makeTestValidator();
33+
defaultMerger = new DefaultMerger(testValidator, {});
3134
});
3235

3336
describe("getDefaultFormState2()", () => {
@@ -2952,6 +2955,39 @@ describe("getDefaultFormState2()", () => {
29522955
);
29532956
});
29542957
});
2958+
describe('object with defaults and undefined in formData, testing mergeDefaultsIntoFormData', () => {
2959+
let schema: Schema;
2960+
let defaultedFormData: any;
2961+
beforeAll(() => {
2962+
schema = {
2963+
type: 'object',
2964+
properties: {
2965+
field: {
2966+
type: 'string',
2967+
default: 'foo',
2968+
},
2969+
},
2970+
required: ['field'],
2971+
};
2972+
defaultedFormData = { field: 'foo' };
2973+
});
2974+
it('returns field value of default when formData is empty', () => {
2975+
const formData = {};
2976+
expect(getDefaultFormState2(testValidator, defaultMerger, schema, formData)).toEqual(defaultedFormData);
2977+
});
2978+
it('returns field value of undefined when formData has undefined for field', () => {
2979+
const formData = { field: undefined };
2980+
expect(getDefaultFormState2(testValidator, defaultMerger, schema, formData)).toEqual(formData);
2981+
});
2982+
it('returns field value of default when formData has undefined for field and `useDefaultIfFormDataUndefined`', () => {
2983+
const formData = { field: undefined };
2984+
expect(
2985+
getDefaultFormState2(testValidator, defaultMerger, schema, formData, undefined, undefined, {
2986+
mergeDefaultsIntoFormData: 'useDefaultIfFormDataUndefined',
2987+
})
2988+
).toEqual(defaultedFormData);
2989+
});
2990+
});
29552991
it("should return undefined defaults for a required array property with minItems", () => {
29562992
const schema: Schema = {
29572993
type: "object",

packages/form/src/core/default-state.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import { getSimpleSchemaType } from "./type.js";
2626
import { isMultiSelect2 } from "./is-select.js";
2727
import { getClosestMatchingOption2 } from "./matching.js";
28-
import { defaultMerger } from './default-merger.js';
28+
import { DefaultMerger } from './default-merger.js';
2929
import type { Merger } from './merger.js';
3030

3131
export function getDefaultValueForType(type: SchemaType) {
@@ -59,7 +59,7 @@ export function getDefaultFormState(
5959
rootSchema?: Schema,
6060
includeUndefinedValues: boolean | "excludeObjectChildren" = false,
6161
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior,
62-
merger: Merger = defaultMerger
62+
merger: Merger = new DefaultMerger(validator, rootSchema ?? theSchema)
6363
): SchemaValue | undefined {
6464
return getDefaultFormState2(
6565
validator,
@@ -100,7 +100,8 @@ export function getDefaultFormState2(
100100
return mergeDefaultsWithFormData(
101101
defaults,
102102
formData,
103-
experimental_defaultFormStateBehavior?.arrayMinItems?.mergeExtraDefaults
103+
experimental_defaultFormStateBehavior?.arrayMinItems?.mergeExtraDefaults,
104+
experimental_defaultFormStateBehavior?.mergeDefaultsIntoFormData === "useDefaultIfFormDataUndefined"
104105
);
105106
}
106107
return formData;
@@ -159,6 +160,15 @@ type Experimental_DefaultFormStateBehavior = {
159160
* Optional flag to compute the default form state using allOf and if/then/else schemas. Defaults to `skipDefaults'.
160161
*/
161162
allOf?: "populateDefaults" | "skipDefaults";
163+
/** Optional enumerated flag controlling how the defaults are merged into the form data when dealing with undefined
164+
* values, defaulting to `useFormDataIfPresent`.
165+
* NOTE: If there is a default for a field and the `formData` is unspecified, the default ALWAYS merges.
166+
* - `useFormDataIfPresent`: Legacy behavior - Do not merge defaults if there is a value for a field in `formData`,
167+
* even if that value is explicitly set to `undefined`
168+
* - `useDefaultIfFormDataUndefined`: - If the value of a field within the `formData` is `undefined`, then use the
169+
* default value instead
170+
*/
171+
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined';
162172
};
163173

164174
interface ComputeDefaultsProps {
@@ -178,7 +188,7 @@ export function computeDefaults<T extends SchemaValue>(
178188
validator: Validator,
179189
rawSchema: Schema,
180190
computeDefaultsProps?: ComputeDefaultsProps,
181-
merger: Merger = defaultMerger
191+
merger: Merger = new DefaultMerger(validator, rawSchema)
182192
): SchemaValue | undefined {
183193
return computeDefaults2<T>(
184194
validator,

packages/form/src/core/is-file-schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the Apache License, Version 2.0.
33
// Modifications made by Roman Krasilnikov.
44

5-
import { defaultMerger } from './default-merger.js';
5+
import { DefaultMerger } from './default-merger.js';
66
import type { Merger } from './merger.js';
77
import { retrieveSchema2 } from "./resolve.js";
88
import { DATA_URL_FORMAT, isNormalArrayItems, type Schema } from "./schema.js";
@@ -19,7 +19,7 @@ export function isFilesArray(
1919
validator: Validator,
2020
schema: Schema,
2121
rootSchema?: Schema,
22-
merger: Merger = defaultMerger
22+
merger: Merger = new DefaultMerger(validator, rootSchema ?? schema)
2323
) {
2424
return isFilesArray2(validator, merger, schema, rootSchema);
2525
}

packages/form/src/core/is-select.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import type { Schema } from "./schema.js";
88
import type { Validator } from "./validator.js";
99
import { isMultiSelect2, isSelect2 } from "./is-select.js";
1010
import { makeTestValidator } from "./test-validator.js";
11-
import { defaultMerger } from './default-merger.js';
11+
import { DefaultMerger } from './default-merger.js';
12+
import type { Merger } from './merger.js';
1213

1314
let testValidator: Validator;
15+
let defaultMerger: Merger
1416

1517
beforeEach(() => {
1618
testValidator = makeTestValidator();
19+
defaultMerger = new DefaultMerger(testValidator, {});
1720
});
1821

1922
describe("isSelect2()", () => {

packages/form/src/core/is-select.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Validator } from "./validator.js";
77
import { retrieveSchema2 } from "./resolve.js";
88
import { isSchemaOfConstantValue } from "./constant-schema.js";
99
import type { Merger } from "./merger.js";
10-
import { defaultMerger } from "./default-merger.js";
10+
import { DefaultMerger } from "./default-merger.js";
1111

1212
/**
1313
* @deprecated use `isSelect2`
@@ -16,7 +16,7 @@ export function isSelect(
1616
validator: Validator,
1717
theSchema: Schema,
1818
rootSchema: Schema,
19-
merger: Merger = defaultMerger
19+
merger: Merger = new DefaultMerger(validator, rootSchema)
2020
) {
2121
return isSelect2(validator, merger, theSchema, rootSchema);
2222
}
@@ -48,7 +48,7 @@ export function isMultiSelect(
4848
validator: Validator,
4949
schema: Schema,
5050
rootSchema: Schema,
51-
merger: Merger = defaultMerger
51+
merger: Merger = new DefaultMerger(validator, rootSchema)
5252
) {
5353
return isMultiSelect2(validator, merger, schema, rootSchema);
5454
}

packages/form/src/core/matching.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ import { calculateIndexScore2, getClosestMatchingOption2 } from "./matching.js";
1717
import { beforeEach, describe, expect, it } from "vitest";
1818
import type { Validator } from "./validator.js";
1919
import { makeTestValidator } from "./test-validator.js";
20-
import { defaultMerger } from "./default-merger.js";
20+
import { DefaultMerger } from "./default-merger.js";
21+
import type { Merger } from './merger.js';
2122

2223
const firstOption = oneOfSchema.definitions!.first_option_def as Schema;
2324
const secondOption = oneOfSchema.definitions!.second_option_def as Schema;
2425

2526
let testValidator: Validator;
27+
let defaultMerger: Merger
2628

2729
beforeEach(() => {
2830
testValidator = makeTestValidator();
31+
defaultMerger = new DefaultMerger(testValidator, oneOfSchema);
2932
});
3033

3134
describe("calculateIndexScore", () => {

packages/form/src/core/matching.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the Apache License, Version 2.0.
33
// Modifications made by Roman Krasilnikov.
44

5-
import { defaultMerger } from './default-merger.js';
5+
import { DefaultMerger } from './default-merger.js';
66
import {
77
getDiscriminatorFieldFromSchema,
88
getOptionMatchingSimpleDiscriminator,
@@ -130,7 +130,7 @@ export function calculateIndexScore(
130130
rootSchema: Schema,
131131
schema?: Schema,
132132
formData?: SchemaValue,
133-
merger: Merger = defaultMerger
133+
merger: Merger = new DefaultMerger(validator, rootSchema)
134134
): number {
135135
return calculateIndexScore2(validator, merger, rootSchema, schema, formData);
136136
}
@@ -232,7 +232,7 @@ export function getClosestMatchingOption(
232232
options: Schema[],
233233
selectedOption = -1,
234234
discriminatorField?: string,
235-
merger: Merger = defaultMerger
235+
merger: Merger = new DefaultMerger(validator, rootSchema)
236236
): number {
237237
return getClosestMatchingOption2(
238238
validator,

0 commit comments

Comments
 (0)