Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
should change the heading of the (upcoming) version to include a major version bump.

-->
# 5.24.4

## @rjsf/utils

- fixed issue with customValidate errors are not cleared when the form is valid [4365](https://github.com/rjsf-team/react-jsonschema-form/pull/4365) due to regression

# 5.24.3

## @rjsf/utils
Expand Down
47 changes: 43 additions & 4 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
ValidatorType,
Experimental_DefaultFormStateBehavior,
Experimental_CustomMergeAllOf,
createErrorHandler,
unwrapErrorHandler,
} from '@rjsf/utils';
import _forEach from 'lodash/forEach';
import _get from 'lodash/get';
Expand Down Expand Up @@ -519,6 +521,22 @@ export default class Form<
return shouldRender(this, nextProps, nextState);
}

/** Gets the previously raised customValidate errors.
*
* @returns the previous customValidate errors
*/
private getPreviousCustomValidateErrors(): ErrorSchema<T> {
const { customValidate, uiSchema } = this.props;
const prevFormData = this.state.formData as T;
let customValidateErrors = {};
if (typeof customValidate === 'function') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the lodash isFunction() method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using typeof === 'function' in multiple places and I see that we never use lodash isFunction. If you think it's an improvement I can change it here and everywhere else.

const errorHandler = customValidate(prevFormData, createErrorHandler<T>(prevFormData), uiSchema);
const userErrorSchema = unwrapErrorHandler<T>(errorHandler);
customValidateErrors = userErrorSchema;
}
return customValidateErrors;
}

/** Validates the `formData` against the `schema` using the `altSchemaUtils` (if provided otherwise it uses the
* `schemaUtils` in the state), returning the results.
*
Expand Down Expand Up @@ -644,18 +662,39 @@ export default class Form<
if (resolvedSchema?.type !== 'object' && resolvedSchema?.type !== 'array') {
filteredErrors.__errors = schemaErrors.__errors;
}

const prevCustomValidateErrors = this.getPreviousCustomValidateErrors();
// Filtering out the previous raised customValidate errors so that they are cleared when no longer valid.
const filterPreviousCustomErrors = (errors: string[] = [], prevCustomErrors: string[]) => {
if (errors.length === 0) {
return errors;
}

return errors.filter((error) => {
return !prevCustomErrors.includes(error);
});
};

// Removing undefined, null and empty errors.
const filterNilOrEmptyErrors = (errors: any): ErrorSchema<T> => {
const filterNilOrEmptyErrors = (errors: any, previousCustomValidateErrors: any = {}): ErrorSchema<T> => {
_forEach(errors, (errorAtKey, errorKey: keyof typeof errors) => {
if (_isNil(errorAtKey)) {
const prevCustomValidateErrorAtKey = previousCustomValidateErrors[errorKey];
if (_isNil(errorAtKey) || (Array.isArray(errorAtKey) && errorAtKey.length === 0)) {
delete errors[errorKey];
} else if (
isObject(errorAtKey) &&
isObject(prevCustomValidateErrorAtKey) &&
Array.isArray(prevCustomValidateErrorAtKey?.__errors)
) {
// if previous customValidate error is an object and has __errors array, filter out the errors previous customValidate errors.
errors[errorKey] = filterPreviousCustomErrors(errorAtKey.__errors, prevCustomValidateErrorAtKey.__errors);
} else if (typeof errorAtKey === 'object' && !Array.isArray(errorAtKey.__errors)) {
filterNilOrEmptyErrors(errorAtKey);
filterNilOrEmptyErrors(errorAtKey, previousCustomValidateErrors[errorKey]);
}
});
return errors;
};
return filterNilOrEmptyErrors(filteredErrors);
return filterNilOrEmptyErrors(filteredErrors, prevCustomValidateErrors);
}

/** Function to handle changes made to a field in the `Form`. This handler receives an entirely new copy of the
Expand Down
76 changes: 76 additions & 0 deletions packages/core/test/Form.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2671,6 +2671,82 @@ describeRepeated('Form common', (createFormComponent) => {
);
});
});

describe('customValidate errors', () => {
it('customValidate should raise an error when End is larger than Start field.', () => {
let schema = {
required: ['Start', 'End'],
properties: {
Start: {
type: 'number',
},
End: {
type: 'number',
},
},
type: 'object',
};

// customValidate method to raise an error when Start is larger than End field.
const customValidate = (formData, errors) => {
if (formData['Start'] > formData['End']) {
errors['Start']?.addError('Validate error: Test should be LE than End');
}
return errors;
};

const { node, onChange } = createFormComponent({
schema,
liveValidate: true,
formData: { Start: 2, End: 1 },
customValidate,
});

expect(node.querySelectorAll('#root_Start__error')).to.have.length(1);
let errorMessageContent = node.querySelector('#root_Start__error .text-danger').textContent;
expect(errorMessageContent).to.contain('Validate error: Test should be LE than End');

// Change the End field to a larger value than Start field to remove customValidate raised errors.
const endInput = node.querySelector('#root_End');
act(() => {
fireEvent.change(endInput, {
target: { value: 3 },
});
});

expect(node.querySelectorAll('#root_Start__error')).to.have.length(0);
sinon.assert.calledWithMatch(
onChange.lastCall,
{
errorSchema: {},
errors: [],
},
'root'
);

// Change the End field to a lesser value than Start field to raise customValidate errors.
act(() => {
fireEvent.change(endInput, {
target: { value: 0 },
});
});

expect(node.querySelectorAll('#root_Start__error')).to.have.length(1);
errorMessageContent = node.querySelector('#root_Start__error .text-danger').textContent;
expect(errorMessageContent).to.contain('Validate error: Test should be LE than End');
sinon.assert.calledWithMatch(
onChange.lastCall,
{
errorSchema: {
Start: {
__errors: ['Validate error: Test should be LE than End'],
},
},
},
'root'
);
});
});
});

describe('Schema and formData updates', () => {
Expand Down
Loading