Skip to content

Commit 72ca431

Browse files
Merge branch 'main' into fix-4402
2 parents bbae10b + a521990 commit 72ca431

File tree

14 files changed

+2490
-1524
lines changed

14 files changed

+2490
-1524
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19-
# 5.23.3
19+
# 5.24.0
20+
21+
## @rjsf/core
22+
23+
- Fixed issue with schema if/then/else conditions where switching to then/else subschemas did not reflect the actual validation errors in the onChange event, fixing [#4249](https://github.com/rjsf-team/react-jsonschema-form/issues/4249) and improving performance.
24+
25+
## @rjsf/utils
26+
27+
- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)
28+
- Fixed issue with assigning default values to formData with deeply nested required properties, fixing [#4399](https://github.com/rjsf-team/react-jsonschema-form/issues/4399)
29+
- Fix for AJV [$data](https://ajv.js.org/guide/combining-schemas.html#data-reference) reference in const property in schema treated as default/const value. The issue is mentioned in [#4361](https://github.com/rjsf-team/react-jsonschema-form/issues/4361).
2030

2131
## @rjsf/validator-ajv8
2232

packages/core/src/components/Form.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,9 @@ export default class Form<
416416
);
417417
}
418418
const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T;
419-
const _retrievedSchema = retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData);
419+
const _retrievedSchema = this.updateRetrievedSchema(
420+
retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData)
421+
);
420422

421423
const getCurrentErrors = (): ValidationData<T> => {
422424
// If the `props.noValidate` option is set or the schema has changed, we reset the error state.
@@ -459,6 +461,7 @@ export default class Form<
459461
errors = currentErrors.errors;
460462
errorSchema = currentErrors.errorSchema;
461463
}
464+
462465
if (props.extraErrors) {
463466
const merged = validationDataMerge({ errorSchema, errors }, props.extraErrors);
464467
errorSchema = merged.errorSchema;
@@ -649,11 +652,13 @@ export default class Form<
649652
*/
650653
onChange = (formData: T | undefined, newErrorSchema?: ErrorSchema<T>, id?: string) => {
651654
const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props;
652-
const { schemaUtils, schema, retrievedSchema } = this.state;
655+
const { schemaUtils, schema } = this.state;
653656

657+
let retrievedSchema = this.state.retrievedSchema;
654658
if (isObject(formData) || Array.isArray(formData)) {
655-
const newState = this.getStateFromProps(this.props, formData, retrievedSchema);
659+
const newState = this.getStateFromProps(this.props, formData);
656660
formData = newState.formData;
661+
retrievedSchema = newState.retrievedSchema;
657662
}
658663

659664
const mustValidate = !noValidate && liveValidate;
@@ -703,6 +708,20 @@ export default class Form<
703708
this.setState(state as FormState<T, S, F>, () => onChange && onChange({ ...this.state, ...state }, id));
704709
};
705710

711+
/**
712+
* If the retrievedSchema has changed the new retrievedSchema is returned.
713+
* Otherwise, the old retrievedSchema is returned to persist reference.
714+
* - This ensures that AJV retrieves the schema from the cache when it has not changed,
715+
* avoiding the performance cost of recompiling the schema.
716+
*
717+
* @param retrievedSchema The new retrieved schema.
718+
* @returns The new retrieved schema if it has changed, else the old retrieved schema.
719+
*/
720+
private updateRetrievedSchema(retrievedSchema: S) {
721+
const isTheSame = deepEquals(retrievedSchema, this.state?.retrievedSchema);
722+
return isTheSame ? this.state.retrievedSchema : retrievedSchema;
723+
}
724+
706725
/**
707726
* Callback function to handle reset form data.
708727
* - Reset all fields with default values.

packages/core/test/ObjectField.test.jsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,99 @@ describe('ObjectField', () => {
227227
});
228228
});
229229

230+
it('Check schema with if/then/else conditions and activate the then/else subschemas, the onChange event should reflect the actual validation errors', () => {
231+
const schema = {
232+
type: 'object',
233+
_const: 'test',
234+
required: ['checkbox'],
235+
properties: {
236+
checkbox: {
237+
type: 'boolean',
238+
},
239+
},
240+
if: {
241+
required: ['checkbox'],
242+
properties: {
243+
checkbox: {
244+
const: true,
245+
},
246+
},
247+
},
248+
then: {
249+
required: ['text'],
250+
properties: {
251+
text: {
252+
type: 'string',
253+
},
254+
},
255+
},
256+
};
257+
258+
const { node, onChange } = createFormComponent({
259+
schema,
260+
formData: {
261+
checkbox: true,
262+
},
263+
liveValidate: true,
264+
});
265+
266+
// Uncheck the checkbox
267+
fireEvent.click(node.querySelector('input[type=checkbox]'));
268+
269+
sinon.assert.calledWithMatch(
270+
onChange.lastCall,
271+
{
272+
formData: { checkbox: false },
273+
errorSchema: {},
274+
errors: [],
275+
},
276+
'root_checkbox'
277+
);
278+
});
279+
280+
it('should validate AJV $data reference ', () => {
281+
const schema = {
282+
type: 'object',
283+
properties: {
284+
email: {
285+
type: 'string',
286+
title: 'E-mail',
287+
format: 'email',
288+
},
289+
emailConfirm: {
290+
type: 'string',
291+
const: {
292+
$data: '/email',
293+
},
294+
title: 'Confirm e-mail',
295+
format: 'email',
296+
},
297+
},
298+
};
299+
const { node, rerender } = createFormComponent({
300+
schema,
301+
formData: {
302+
303+
emailConfirm: '[email protected]',
304+
},
305+
liveValidate: true,
306+
});
307+
308+
const errorMessages = node.querySelectorAll('#root_emailConfirm__error');
309+
expect(errorMessages).to.have.length(1);
310+
311+
rerender({
312+
schema,
313+
formData: {
314+
315+
emailConfirm: '[email protected]',
316+
},
317+
liveValidate: true,
318+
});
319+
320+
expect(node.querySelectorAll('#root_foo__error')).to.have.length(0);
321+
});
322+
230323
it('Check that when formData changes, the form should re-validate', () => {
231324
const { node, rerender } = createFormComponent({
232325
schema,

packages/playground/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ const esV8Validator = customizeValidator({}, localize_es);
2020
const AJV8_2019 = customizeValidator({ AjvClass: Ajv2019 });
2121
const AJV8_2020 = customizeValidator({ AjvClass: Ajv2020 });
2222
const AJV8_DISC = customizeValidator({ ajvOptionsOverrides: { discriminator: true } });
23+
const AJV8_DATA_REF = customizeValidator({ ajvOptionsOverrides: { $data: true } });
2324

2425
const validators: PlaygroundProps['validators'] = {
2526
AJV8: v8Validator,
27+
'AJV8 $data reference': AJV8_DATA_REF,
2628
'AJV8 (discriminator)': AJV8_DISC,
2729
AJV8_es: esV8Validator,
2830
AJV8_2019,

packages/playground/src/components/Header.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export default function Header({
306306
uiSchema,
307307
theme,
308308
liveSettings,
309+
validator,
309310
})
310311
);
311312

@@ -314,7 +315,7 @@ export default function Header({
314315
setShareURL(null);
315316
console.error(error);
316317
}
317-
}, [formData, liveSettings, schema, theme, uiSchema, setShareURL]);
318+
}, [formData, liveSettings, schema, theme, uiSchema, validator, setShareURL]);
318319

319320
return (
320321
<div className='page-header'>

packages/playground/src/components/Playground.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
6969
theme: dataTheme = theme,
7070
extraErrors,
7171
liveSettings,
72+
validator,
7273
...rest
7374
} = data;
7475

@@ -85,6 +86,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
8586
setTheme(theTheme);
8687
setShowForm(true);
8788
setLiveSettings(liveSettings);
89+
setValidator(validator);
8890
setOtherFormProps({ fields, templates, ...rest });
8991
},
9092
[theme, onThemeSelected, themes]
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
import { FormProps } from '@rjsf/core';
22

3-
export type Sample = Omit<FormProps, 'validator'>;
3+
export interface Sample extends Omit<FormProps, 'validator'> {
4+
validator: string;
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CONST_KEY, getSchemaType, isObject } from './';
2+
import { RJSFSchema, StrictRJSFSchema } from './types';
3+
import { JSONSchema7Type } from 'json-schema';
4+
import isString from 'lodash/isString';
5+
6+
/**
7+
* Checks if the schema const property value is an AJV $data reference
8+
* and the current schema is not an object or array
9+
*
10+
* @param schema - The schema to check if the const is an AJV $data reference
11+
* @returns - true if the schema const property value is an AJV $data reference otherwise false.
12+
*/
13+
export default function constIsAjvDataReference<S extends StrictRJSFSchema = RJSFSchema>(schema: S): boolean {
14+
const schemaConst = schema[CONST_KEY] as JSONSchema7Type & { $data: string };
15+
const schemaType = getSchemaType<S>(schema);
16+
return isObject(schemaConst) && isString(schemaConst?.$data) && schemaType !== 'object' && schemaType !== 'array';
17+
}

packages/utils/src/mergeDefaultsWithFormData.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import get from 'lodash/get';
22

33
import isObject from './isObject';
44
import { GenericObjectType } from '../src';
5+
import isNil from 'lodash/isNil';
56

67
/** Merges the `defaults` object of type `T` into the `formData` of type `T`
78
*
@@ -19,47 +20,78 @@ import { GenericObjectType } from '../src';
1920
* @param [formData] - The form data into which the defaults will be merged
2021
* @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData
2122
* @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
24+
* doesn't exist in the default, we take it from formData and in the case where the value is set to undefined in formData.
25+
* This is useful when we have already merged formData with defaults and want to add an additional field from formData
26+
* that does not exist in defaults.
2227
* @returns - The resulting merged form data with defaults
2328
*/
2429
export default function mergeDefaultsWithFormData<T = any>(
2530
defaults?: T,
2631
formData?: T,
2732
mergeExtraArrayDefaults = false,
28-
defaultSupercedesUndefined = false
33+
defaultSupercedesUndefined = false,
34+
overrideFormDataWithDefaults = false
2935
): T | undefined {
3036
if (Array.isArray(formData)) {
3137
const defaultsArray = Array.isArray(defaults) ? defaults : [];
32-
const mapped = formData.map((value, idx) => {
33-
if (defaultsArray[idx]) {
38+
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;
42+
43+
const mapped = overrideArray.map((value, idx) => {
44+
if (overrideOppositeArray[idx]) {
3445
return mergeDefaultsWithFormData<any>(
3546
defaultsArray[idx],
36-
value,
47+
formData[idx],
3748
mergeExtraArrayDefaults,
38-
defaultSupercedesUndefined
49+
defaultSupercedesUndefined,
50+
overrideFormDataWithDefaults
3951
);
4052
}
4153
return value;
4254
});
55+
4356
// Merge any extra defaults when mergeExtraArrayDefaults is true
44-
if (mergeExtraArrayDefaults && mapped.length < defaultsArray.length) {
45-
mapped.push(...defaultsArray.slice(mapped.length));
57+
// Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array
58+
if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) {
59+
mapped.push(...overrideOppositeArray.slice(mapped.length));
4660
}
4761
return mapped as unknown as T;
4862
}
4963
if (isObject(formData)) {
5064
const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object.
5165
return Object.keys(formData as GenericObjectType).reduce((acc, key) => {
66+
const keyValue = get(formData, key);
67+
const keyExistsInDefaults = isObject(defaults) && key in (defaults as GenericObjectType);
68+
const keyExistsInFormData = key in (formData as GenericObjectType);
5269
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
5370
defaults ? get(defaults, key) : {},
54-
get(formData, key),
71+
keyValue,
5572
mergeExtraArrayDefaults,
56-
defaultSupercedesUndefined
73+
defaultSupercedesUndefined,
74+
// overrideFormDataWithDefaults can be true only when the key value exists in defaults
75+
// Or if the key value doesn't exist in formData
76+
overrideFormDataWithDefaults && (keyExistsInDefaults || !keyExistsInFormData)
5777
);
5878
return acc;
5979
}, acc);
6080
}
61-
if (defaultSupercedesUndefined && formData === undefined) {
81+
82+
/**
83+
* If the defaultSupercedesUndefined flag is true
84+
* And formData is set to undefined or null and defaults are defined
85+
* Or if formData is a number and is NaN return defaults
86+
* Or if overrideFormDataWithDefaults flag is true and formData is set to not undefined/null return defaults
87+
*/
88+
if (
89+
(defaultSupercedesUndefined &&
90+
((!isNil(defaults) && isNil(formData)) || (typeof formData === 'number' && isNaN(formData)))) ||
91+
(overrideFormDataWithDefaults && !isNil(formData))
92+
) {
6293
return defaults;
6394
}
95+
6496
return formData;
6597
}

0 commit comments

Comments
 (0)