Skip to content

Commit 4d3094c

Browse files
abdalla-rkoAbdallah Al-SoqatriAbdallah Al-Soqatriheath-freenome
authored
Feat: Allow raising errors from within a custom whatever(#2718) (#4188)
* #2718 feature - raise errors from within fields * fixed failing tests * Fixed failing build * Removing raiseError message and errorSchema is updated now using the onChange. * reverting tests * Filtering errors based on your retrieved schema to only show errors for properties in the selected branch. * fixed issue with typing causing build failures. * Improvement based on feedback * improvement based on feedback and written test for custom widget * documenting the feature * docs improvement base on feedback * removed empty line * fixed lodash import * Update packages/core/src/components/Form.tsx Ordered lodash import * Update packages/core/src/components/Form.tsx * Update CHANGELOG.md Added missing packages * Update CHANGELOG.md Added missing space --------- Co-authored-by: Abdallah Al-Soqatri <[email protected]> Co-authored-by: Abdallah Al-Soqatri <[email protected]> Co-authored-by: Heath C <[email protected]>
1 parent 16b33c0 commit 4d3094c

File tree

7 files changed

+413
-2
lines changed

7 files changed

+413
-2
lines changed

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19+
# 5.20.0
20+
21+
## @rjsf/core
22+
23+
- Support allowing raising errors from within a custom Widget [#2718](https://github.com/rjsf-team/react-jsonschema-form/issues/2718)
24+
25+
## @rjsf/utils
26+
27+
- Updated the `WidgetProps` type to add `es?: ErrorSchema<T>, id?: string` to the params of the `onChange` handler function
28+
29+
## Dev / docs / playground
30+
31+
- Update the `custom-widget-fields.md` to add documentation for how to raise errors from a custom widget or field
32+
1933
# 5.19.4
2034

2135
## @rjsf/core

packages/core/src/components/Form.tsx

+42-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
ValidatorType,
3535
Experimental_DefaultFormStateBehavior,
3636
} from '@rjsf/utils';
37+
import _forEach from 'lodash/forEach';
3738
import _get from 'lodash/get';
3839
import _isEmpty from 'lodash/isEmpty';
3940
import _pick from 'lodash/pick';
@@ -421,7 +422,17 @@ export default class Form<
421422
if (mustValidate) {
422423
const schemaValidation = this.validate(formData, schema, schemaUtils, _retrievedSchema);
423424
errors = schemaValidation.errors;
424-
errorSchema = schemaValidation.errorSchema;
425+
// If the schema has changed, we do not merge state.errorSchema.
426+
// Else in the case where it hasn't changed, we merge 'state.errorSchema' with 'schemaValidation.errorSchema.' This done to display the raised field error.
427+
if (isSchemaChanged) {
428+
errorSchema = schemaValidation.errorSchema;
429+
} else {
430+
errorSchema = mergeObjects(
431+
this.state?.errorSchema,
432+
schemaValidation.errorSchema,
433+
'preventDuplicates'
434+
) as ErrorSchema<T>;
435+
}
425436
schemaValidationErrors = errors;
426437
schemaValidationErrorSchema = errorSchema;
427438
} else {
@@ -581,6 +592,31 @@ export default class Form<
581592
return newFormData;
582593
};
583594

595+
// Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
596+
private filterErrorsBasedOnSchema(schemaErrors: ErrorSchema<T>, resolvedSchema?: S, formData?: any): ErrorSchema<T> {
597+
const { retrievedSchema, schemaUtils } = this.state;
598+
const _retrievedSchema = resolvedSchema ?? retrievedSchema;
599+
const pathSchema = schemaUtils.toPathSchema(_retrievedSchema, '', formData);
600+
const fieldNames = this.getFieldNames(pathSchema, formData);
601+
const filteredErrors: ErrorSchema<T> = _pick(schemaErrors, fieldNames as unknown as string[]);
602+
// If the root schema is of a primitive type, do not filter out the __errors
603+
if (resolvedSchema?.type !== 'object' && resolvedSchema?.type !== 'array') {
604+
filteredErrors.__errors = schemaErrors.__errors;
605+
}
606+
// Removing undefined and empty errors.
607+
const filterUndefinedErrors = (errors: any): ErrorSchema<T> => {
608+
_forEach(errors, (errorAtKey, errorKey: keyof typeof errors) => {
609+
if (errorAtKey === undefined) {
610+
delete errors[errorKey];
611+
} else if (typeof errorAtKey === 'object' && !Array.isArray(errorAtKey.__errors)) {
612+
filterUndefinedErrors(errorAtKey);
613+
}
614+
});
615+
return errors;
616+
};
617+
return filterUndefinedErrors(filteredErrors);
618+
}
619+
584620
/** Function to handle changes made to a field in the `Form`. This handler receives an entirely new copy of the
585621
* `formData` along with a new `ErrorSchema`. It will first update the `formData` with any missing default fields and
586622
* then, if `omitExtraData` and `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not
@@ -624,6 +660,11 @@ export default class Form<
624660
errorSchema = merged.errorSchema;
625661
errors = merged.errors;
626662
}
663+
// Merging 'newErrorSchema' into 'errorSchema' to display the custom raised errors.
664+
if (newErrorSchema) {
665+
const filteredErrors = this.filterErrorsBasedOnSchema(newErrorSchema, retrievedSchema, newFormData);
666+
errorSchema = mergeObjects(errorSchema, filteredErrors, 'preventDuplicates') as ErrorSchema<T>;
667+
}
627668
state = {
628669
formData: newFormData,
629670
errors,

packages/core/test/ArrayField.test.jsx

+118
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import sinon from 'sinon';
55

66
import { createFormComponent, createSandbox, submitForm } from './test_utils';
77
import SchemaField from '../src/components/fields/SchemaField';
8+
import ArrayField from '../src/components/fields/ArrayField';
9+
import { TextWidgetTest } from './StringField.test';
810

911
const ArrayKeyDataAttr = 'data-rjsf-itemkey';
1012
const ExposedArrayKeyTemplate = function (props) {
@@ -157,6 +159,26 @@ const ArrayFieldTestItemTemplate = (props) => {
157159
);
158160
};
159161

162+
const ArrayFieldTest = (props) => {
163+
const onChangeTest = (newFormData, errorSchema, id) => {
164+
if (Array.isArray(newFormData) && newFormData.length === 1) {
165+
const itemValue = newFormData[0]?.text;
166+
if (itemValue !== 'Appie') {
167+
const raiseError = {
168+
...errorSchema,
169+
0: {
170+
text: {
171+
__errors: ['Value must be "Appie"'],
172+
},
173+
},
174+
};
175+
props.onChange(newFormData, raiseError, id);
176+
}
177+
}
178+
};
179+
return <ArrayField {...props} onChange={onChangeTest} />;
180+
};
181+
160182
describe('ArrayField', () => {
161183
let sandbox;
162184
const CustomComponent = (props) => {
@@ -3196,5 +3218,101 @@ describe('ArrayField', () => {
31963218
},
31973219
});
31983220
});
3221+
3222+
it('raise an error and check if the error is displayed', () => {
3223+
const { node } = createFormComponent({
3224+
schema,
3225+
formData: [
3226+
{
3227+
text: 'y',
3228+
},
3229+
],
3230+
templates,
3231+
fields: {
3232+
ArrayField: ArrayFieldTest,
3233+
},
3234+
});
3235+
3236+
const inputs = node.querySelectorAll('.field-string input[type=text]');
3237+
act(() => {
3238+
fireEvent.change(inputs[0], { target: { value: 'test' } });
3239+
});
3240+
3241+
const errorMessages = node.querySelectorAll('#root_0_text__error');
3242+
expect(errorMessages).to.have.length(1);
3243+
const errorMessageContent = node.querySelector('#root_0_text__error .text-danger').textContent;
3244+
expect(errorMessageContent).to.contain('Value must be "Appie"');
3245+
});
3246+
3247+
it('should not raise an error if value is correct', () => {
3248+
const { node } = createFormComponent({
3249+
schema,
3250+
formData: [
3251+
{
3252+
text: 'y',
3253+
},
3254+
],
3255+
templates,
3256+
fields: {
3257+
ArrayField: ArrayFieldTest,
3258+
},
3259+
});
3260+
3261+
const inputs = node.querySelectorAll('.field-string input[type=text]');
3262+
act(() => {
3263+
fireEvent.change(inputs[0], { target: { value: 'Appie' } });
3264+
});
3265+
3266+
const errorMessages = node.querySelectorAll('#root_0_text__error');
3267+
expect(errorMessages).to.have.length(0);
3268+
});
3269+
3270+
it('raise an error and check if the error is displayed using custom text widget', () => {
3271+
const { node } = createFormComponent({
3272+
schema,
3273+
formData: [
3274+
{
3275+
text: 'y',
3276+
},
3277+
],
3278+
templates,
3279+
widgets: {
3280+
TextWidget: TextWidgetTest,
3281+
},
3282+
});
3283+
3284+
const inputs = node.querySelectorAll('.field-string input[type=text]');
3285+
act(() => {
3286+
fireEvent.change(inputs[0], { target: { value: 'hello' } });
3287+
});
3288+
3289+
const errorMessages = node.querySelectorAll('#root_0_text__error');
3290+
expect(errorMessages).to.have.length(1);
3291+
const errorMessageContent = node.querySelector('#root_0_text__error .text-danger').textContent;
3292+
expect(errorMessageContent).to.contain('Value must be "test"');
3293+
});
3294+
3295+
it('should not raise an error if value is correct using custom text widget', () => {
3296+
const { node } = createFormComponent({
3297+
schema,
3298+
formData: [
3299+
{
3300+
text: 'y',
3301+
},
3302+
],
3303+
templates,
3304+
widgets: {
3305+
TextWidget: TextWidgetTest,
3306+
},
3307+
});
3308+
3309+
const inputs = node.querySelectorAll('.field-string input[type=text]');
3310+
act(() => {
3311+
fireEvent.change(inputs[0], { target: { value: 'test' } });
3312+
});
3313+
3314+
const errorMessages = node.querySelectorAll('#root_0_text__error');
3315+
expect(errorMessages).to.have.length(0);
3316+
});
31993317
});
32003318
});

packages/core/test/ObjectField.test.jsx

+90
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,26 @@ import sinon from 'sinon';
55
import { UI_GLOBAL_OPTIONS_KEY } from '@rjsf/utils';
66

77
import SchemaField from '../src/components/fields/SchemaField';
8+
import ObjectField from '../src/components/fields/ObjectField';
9+
import { TextWidgetTest } from './StringField.test';
810
import { createFormComponent, createSandbox, submitForm } from './test_utils';
911

12+
const ObjectFieldTest = (props) => {
13+
const onChangeTest = (newFormData, errorSchema, id) => {
14+
const propertyValue = newFormData?.foo;
15+
if (propertyValue !== 'test') {
16+
const raiseError = {
17+
...errorSchema,
18+
foo: {
19+
__errors: ['Value must be "test"'],
20+
},
21+
};
22+
props.onChange(newFormData, raiseError, id);
23+
}
24+
};
25+
return <ObjectField {...props} onChange={onChangeTest} />;
26+
};
27+
1028
describe('ObjectField', () => {
1129
let sandbox;
1230

@@ -208,6 +226,78 @@ describe('ObjectField', () => {
208226
expect(node.querySelector(`code#${formContext[key]}`)).to.exist;
209227
});
210228
});
229+
230+
it('raise an error and check if the error is displayed', () => {
231+
const { node } = createFormComponent({
232+
schema,
233+
fields: {
234+
ObjectField: ObjectFieldTest,
235+
},
236+
});
237+
238+
const inputs = node.querySelectorAll('.field-string input[type=text]');
239+
act(() => {
240+
fireEvent.change(inputs[0], { target: { value: 'hello' } });
241+
});
242+
243+
const errorMessages = node.querySelectorAll('#root_foo__error');
244+
expect(errorMessages).to.have.length(1);
245+
const errorMessageContent = node.querySelector('#root_foo__error .text-danger').textContent;
246+
expect(errorMessageContent).to.contain('Value must be "test"');
247+
});
248+
249+
it('should not raise an error if value is correct', () => {
250+
const { node } = createFormComponent({
251+
schema,
252+
fields: {
253+
ObjectField: ObjectFieldTest,
254+
},
255+
});
256+
257+
const inputs = node.querySelectorAll('.field-string input[type=text]');
258+
act(() => {
259+
fireEvent.change(inputs[0], { target: { value: 'test' } });
260+
});
261+
262+
const errorMessages = node.querySelectorAll('#root_foo__error');
263+
expect(errorMessages).to.have.length(0);
264+
});
265+
266+
it('raise an error and check if the error is displayed using custom text widget', () => {
267+
const { node } = createFormComponent({
268+
schema,
269+
widgets: {
270+
TextWidget: TextWidgetTest,
271+
},
272+
});
273+
274+
const inputs = node.querySelectorAll('.field-string input[type=text]');
275+
act(() => {
276+
fireEvent.change(inputs[0], { target: { value: 'hello' } });
277+
});
278+
279+
const errorMessages = node.querySelectorAll('#root_foo__error');
280+
expect(errorMessages).to.have.length(1);
281+
const errorMessageContent = node.querySelector('#root_foo__error .text-danger').textContent;
282+
expect(errorMessageContent).to.contain('Value must be "test"');
283+
});
284+
285+
it('should not raise an error if value is correct using custom text widget', () => {
286+
const { node } = createFormComponent({
287+
schema,
288+
widgets: {
289+
TextWidget: TextWidgetTest,
290+
},
291+
});
292+
293+
const inputs = node.querySelectorAll('.field-string input[type=text]');
294+
act(() => {
295+
fireEvent.change(inputs[0], { target: { value: 'test' } });
296+
});
297+
298+
const errorMessages = node.querySelectorAll('#root_foo__error');
299+
expect(errorMessages).to.have.length(0);
300+
});
211301
});
212302

213303
describe('fields ordering', () => {

0 commit comments

Comments
 (0)