diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f39152a5c..62d7ba6cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,11 @@ should change the heading of the (upcoming) version to include a major version b --> -# 5.23.3 +# 5.24.0 + +## @rjsf/core + +- 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. ## @rjsf/utils diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 7bc39f6c83..c077f4ddd7 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -416,7 +416,9 @@ export default class Form< ); } const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T; - const _retrievedSchema = retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData); + const _retrievedSchema = this.updateRetrievedSchema( + retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData) + ); const getCurrentErrors = (): ValidationData => { // 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< errors = currentErrors.errors; errorSchema = currentErrors.errorSchema; } + if (props.extraErrors) { const merged = validationDataMerge({ errorSchema, errors }, props.extraErrors); errorSchema = merged.errorSchema; @@ -649,11 +652,13 @@ export default class Form< */ onChange = (formData: T | undefined, newErrorSchema?: ErrorSchema, id?: string) => { const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props; - const { schemaUtils, schema, retrievedSchema } = this.state; + const { schemaUtils, schema } = this.state; + let retrievedSchema = this.state.retrievedSchema; if (isObject(formData) || Array.isArray(formData)) { - const newState = this.getStateFromProps(this.props, formData, retrievedSchema); + const newState = this.getStateFromProps(this.props, formData); formData = newState.formData; + retrievedSchema = newState.retrievedSchema; } const mustValidate = !noValidate && liveValidate; @@ -703,6 +708,20 @@ export default class Form< this.setState(state as FormState, () => onChange && onChange({ ...this.state, ...state }, id)); }; + /** + * If the retrievedSchema has changed the new retrievedSchema is returned. + * Otherwise, the old retrievedSchema is returned to persist reference. + * - This ensures that AJV retrieves the schema from the cache when it has not changed, + * avoiding the performance cost of recompiling the schema. + * + * @param retrievedSchema The new retrieved schema. + * @returns The new retrieved schema if it has changed, else the old retrieved schema. + */ + private updateRetrievedSchema(retrievedSchema: S) { + const isTheSame = deepEquals(retrievedSchema, this.state?.retrievedSchema); + return isTheSame ? this.state.retrievedSchema : retrievedSchema; + } + /** * Callback function to handle reset form data. * - Reset all fields with default values. diff --git a/packages/core/test/ObjectField.test.jsx b/packages/core/test/ObjectField.test.jsx index fc0cfd08ab..1dd8f88a85 100644 --- a/packages/core/test/ObjectField.test.jsx +++ b/packages/core/test/ObjectField.test.jsx @@ -227,6 +227,56 @@ describe('ObjectField', () => { }); }); + it('Check schema with if/then/else conditions and activate the then/else subschemas, the onChange event should reflect the actual validation errors', () => { + const schema = { + type: 'object', + _const: 'test', + required: ['checkbox'], + properties: { + checkbox: { + type: 'boolean', + }, + }, + if: { + required: ['checkbox'], + properties: { + checkbox: { + const: true, + }, + }, + }, + then: { + required: ['text'], + properties: { + text: { + type: 'string', + }, + }, + }, + }; + + const { node, onChange } = createFormComponent({ + schema, + formData: { + checkbox: true, + }, + liveValidate: true, + }); + + // Uncheck the checkbox + fireEvent.click(node.querySelector('input[type=checkbox]')); + + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { checkbox: false }, + errorSchema: {}, + errors: [], + }, + 'root_checkbox' + ); + }); + it('Check that when formData changes, the form should re-validate', () => { const { node, rerender } = createFormComponent({ schema,