Skip to content

Commit b6c1825

Browse files
fix: formData change clear errorMessage (rjsf-team#4429)
* fix: formData change clear errorMessage fix: merge errorSchema fix: merge errorSchema * test: add tests for getChangedFields and update CHANGELOG * fix: core test and error message when formData is a string not cleared * feat: modify CHANGELOG --------- Co-authored-by: Heath C <[email protected]>
1 parent a521990 commit b6c1825

File tree

6 files changed

+220
-9
lines changed

6 files changed

+220
-9
lines changed

CHANGELOG.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ should change the heading of the (upcoming) version to include a major version b
1818

1919
# 5.24.0
2020

21-
## @rjsf/core
21+
## @rjsf/core
2222

2323
- 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+
- Fixed issue error message will not be cleared after the controlled Form formData is changed. Fixes [#4426](https://github.com/rjsf-team/react-jsonschema-form/issues/4426)
2425

2526
## @rjsf/utils
2627

2728
- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)
2829
- 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)
30+
- Fixed issue error message will not be cleared after the controlled Form formData is changed. Fixes [#4426](https://github.com/rjsf-team/react-jsonschema-form/issues/4426)
2931
- 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).
3032

3133
# 5.23.2
@@ -194,18 +196,18 @@ should change the heading of the (upcoming) version to include a major version b
194196
## @rjsf/core
195197

196198
- Support allowing raising errors from within a custom Widget [#2718](https://github.com/rjsf-team/react-jsonschema-form/issues/2718)
197-
- Updated `ArrayField`, `BooleanField` and `StringField` to call `optionsList()` with the additional `UiSchema` parameter, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215) and [#4260](https://github.com/rjsf-team/react-jsonschema-form/issues/4260)
199+
- Updated `ArrayField`, `BooleanField` and `StringField` to call `optionsList()` with the additional `UiSchema` parameter, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215) and [#4260](https://github.com/rjsf-team/react-jsonschema-form/issues/4260)
198200

199201
## @rjsf/utils
200202

201203
- Updated the `WidgetProps` type to add `es?: ErrorSchema<T>, id?: string` to the params of the `onChange` handler function
202204
- Updated `UIOptionsBaseType` to add the new `enumNames` prop to support an alternate way to provide labels for `enum`s in a schema, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215)
203-
- Updated `optionsList()` to take an optional `uiSchema` that is used to extract alternate labels for `enum`s or `oneOf`/`anyOf` in a schema, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215) and [#4260](https://github.com/rjsf-team/react-jsonschema-form/issues/4260)
205+
- Updated `optionsList()` to take an optional `uiSchema` that is used to extract alternate labels for `enum`s or `oneOf`/`anyOf` in a schema, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215) and [#4260](https://github.com/rjsf-team/react-jsonschema-form/issues/4260)
204206
- NOTE: The generics for `optionsList()` were expanded from `<S extends StrictRJSFSchema = RJSFSchema>` to `<S extends StrictRJSFSchema = RJSFSchema, T = any, F extends FormContextType = any>` to support the `UiSchema`.
205207

206208
## Dev / docs / playground
207209

208-
- Update the `custom-widget-fields.md` to add documentation for how to raise errors from a custom widget or field
210+
- Update the `custom-widget-fields.md` to add documentation for how to raise errors from a custom widget or field
209211

210212
# 5.19.4
211213

packages/core/src/components/Form.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ErrorTransformer,
88
FormContextType,
99
GenericObjectType,
10+
getChangedFields,
1011
getTemplate,
1112
getUiOptions,
1213
IdSchema,
@@ -316,16 +317,21 @@ export default class Form<
316317
prevState: FormState<T, S, F>
317318
): { nextState: FormState<T, S, F>; shouldUpdate: true } | { shouldUpdate: false } {
318319
if (!deepEquals(this.props, prevProps)) {
320+
const formDataChangedFields = getChangedFields(this.props.formData, prevProps.formData);
319321
const isSchemaChanged = !deepEquals(prevProps.schema, this.props.schema);
320-
const isFormDataChanged = !deepEquals(prevProps.formData, this.props.formData);
322+
// When formData is not an object, getChangedFields returns an empty array.
323+
// In this case, deepEquals is most needed to check again.
324+
const isFormDataChanged =
325+
formDataChangedFields.length > 0 || !deepEquals(prevProps.formData, this.props.formData);
321326
const nextState = this.getStateFromProps(
322327
this.props,
323328
this.props.formData,
324329
// If the `schema` has changed, we need to update the retrieved schema.
325330
// Or if the `formData` changes, for example in the case of a schema with dependencies that need to
326331
// match one of the subSchemas, the retrieved schema must be updated.
327332
isSchemaChanged || isFormDataChanged ? undefined : this.state.retrievedSchema,
328-
isSchemaChanged
333+
isSchemaChanged,
334+
formDataChangedFields
329335
);
330336
const shouldUpdate = !deepEquals(nextState, prevState);
331337
return { nextState, shouldUpdate };
@@ -375,13 +381,15 @@ export default class Form<
375381
* @param inputFormData - The new or current data for the `Form`
376382
* @param retrievedSchema - An expanded schema, if not provided, it will be retrieved from the `schema` and `formData`.
377383
* @param isSchemaChanged - A flag indicating whether the schema has changed.
384+
* @param formDataChangedFields - The changed fields of `formData`
378385
* @returns - The new state for the `Form`
379386
*/
380387
getStateFromProps(
381388
props: FormProps<T, S, F>,
382389
inputFormData?: T,
383390
retrievedSchema?: S,
384-
isSchemaChanged = false
391+
isSchemaChanged = false,
392+
formDataChangedFields: string[] = []
385393
): FormState<T, S, F> {
386394
const state: FormState<T, S, F> = this.state || {};
387395
const schema = 'schema' in props ? props.schema : this.props.schema;
@@ -460,6 +468,17 @@ export default class Form<
460468
const currentErrors = getCurrentErrors();
461469
errors = currentErrors.errors;
462470
errorSchema = currentErrors.errorSchema;
471+
if (formDataChangedFields.length > 0) {
472+
const newErrorSchema = formDataChangedFields.reduce((acc, key) => {
473+
acc[key] = undefined;
474+
return acc;
475+
}, {} as Record<string, undefined>);
476+
errorSchema = schemaValidationErrorSchema = mergeObjects(
477+
currentErrors.errorSchema,
478+
newErrorSchema,
479+
'preventDuplicates'
480+
) as ErrorSchema<T>;
481+
}
463482
}
464483

465484
if (props.extraErrors) {

packages/core/test/Form.test.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4489,8 +4489,8 @@ describe('Form omitExtraData and liveOmit', () => {
44894489
// // error should still be present.
44904490
errors = node.querySelectorAll('.error-detail');
44914491
// screen.debug();
4492-
expect(errors).to.have.lengthOf(1);
4493-
expect(errors[0].textContent).to.be.eql("must have required property 'input'");
4492+
// change formData and make sure the error disappears.
4493+
expect(errors).to.have.lengthOf(0);
44944494

44954495
// trigger programmatic validation again and make sure the error disappears.
44964496
act(() => {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import keys from 'lodash/keys';
2+
import pickBy from 'lodash/pickBy';
3+
import isPlainObject from 'lodash/isPlainObject';
4+
import get from 'lodash/get';
5+
import difference from 'lodash/difference';
6+
import deepEquals from './deepEquals';
7+
8+
/**
9+
* Compares two objects and returns the names of the fields that have changed.
10+
* This function iterates over each field of object `a`, using `_.isEqual` to compare the field value
11+
* with the corresponding field value in object `b`. If the values are different, the field name will
12+
* be included in the returned array.
13+
*
14+
* @param {unknown} a - The first object, representing the original data to compare.
15+
* @param {unknown} b - The second object, representing the updated data to compare.
16+
* @returns {string[]} - An array of field names that have changed.
17+
*
18+
* @example
19+
* const a = { name: 'John', age: 30 };
20+
* const b = { name: 'John', age: 31 };
21+
* const changedFields = getChangedFields(a, b);
22+
* console.log(changedFields); // Output: ['age']
23+
*/
24+
export default function getChangedFields(a: unknown, b: unknown): string[] {
25+
const aIsPlainObject = isPlainObject(a);
26+
const bIsPlainObject = isPlainObject(b);
27+
// If strictly equal or neither of them is a plainObject returns an empty array
28+
if (a === b || (!aIsPlainObject && !bIsPlainObject)) {
29+
return [];
30+
}
31+
if (aIsPlainObject && !bIsPlainObject) {
32+
return keys(a);
33+
} else if (!aIsPlainObject && bIsPlainObject) {
34+
return keys(b);
35+
} else {
36+
const unequalFields = keys(pickBy(a as object, (value, key) => !deepEquals(value, get(b, key))));
37+
const diffFields = difference(keys(b), keys(a));
38+
return [...unequalFields, ...diffFields];
39+
}
40+
}

packages/utils/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import utcToLocal from './utcToLocal';
5252
import validationDataMerge from './validationDataMerge';
5353
import withIdRefPrefix from './withIdRefPrefix';
5454
import getOptionMatchingSimpleDiscriminator from './getOptionMatchingSimpleDiscriminator';
55+
import getChangedFields from './getChangedFields';
5556

5657
export * from './types';
5758
export * from './enums';
@@ -82,6 +83,7 @@ export {
8283
examplesId,
8384
ErrorSchemaBuilder,
8485
findSchemaDefinition,
86+
getChangedFields,
8587
getDateElementProps,
8688
getDiscriminatorFieldFromSchema,
8789
getInputProps,
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { getChangedFields } from '../src';
2+
import cloneDeep from 'lodash/cloneDeep';
3+
4+
const complexObject = {
5+
a: 1,
6+
b: '2',
7+
c: { c1: {}, c2: [] },
8+
d: ['item1', 'item2', 'item2'],
9+
e: function () {},
10+
};
11+
const complexObjectKeys = ['a', 'b', 'c', 'd', 'e'];
12+
13+
describe('getChangedFields()', () => {
14+
it('Empty parameter', () => {
15+
expect(getChangedFields(undefined, undefined)).toEqual([]);
16+
expect(getChangedFields(complexObject, undefined)).toEqual(complexObjectKeys);
17+
expect(getChangedFields(undefined, complexObject)).toEqual(complexObjectKeys);
18+
});
19+
it('Both not plainObject parameter', () => {
20+
expect(getChangedFields(1, 2)).toEqual([]);
21+
expect(getChangedFields(2, '1')).toEqual([]);
22+
expect(
23+
getChangedFields(
24+
function a() {},
25+
function b() {}
26+
)
27+
).toEqual([]);
28+
expect(getChangedFields(new Date(), new Date())).toEqual([]);
29+
});
30+
it('One is not plainObject parameter', () => {
31+
expect(getChangedFields(1, complexObject)).toEqual(complexObjectKeys);
32+
expect(getChangedFields('1', complexObject)).toEqual(complexObjectKeys);
33+
expect(getChangedFields(function noop() {}, complexObject)).toEqual(complexObjectKeys);
34+
expect(getChangedFields(new Date(), complexObject)).toEqual(complexObjectKeys);
35+
36+
expect(getChangedFields(complexObject, 1)).toEqual(complexObjectKeys);
37+
expect(getChangedFields(complexObject, '1')).toEqual(complexObjectKeys);
38+
expect(getChangedFields(complexObject, function noop() {})).toEqual(complexObjectKeys);
39+
expect(getChangedFields(complexObject, new Date())).toEqual(complexObjectKeys);
40+
});
41+
it('Deep equal', () => {
42+
expect(getChangedFields(complexObject, complexObject)).toEqual([]);
43+
expect(getChangedFields(complexObject, cloneDeep(complexObject))).toEqual([]);
44+
});
45+
it('Change one field', () => {
46+
expect(getChangedFields(complexObject, { ...cloneDeep(complexObject), a: 2 })).toEqual(['a']);
47+
expect(getChangedFields({ ...cloneDeep(complexObject), a: 2 }, complexObject)).toEqual(['a']);
48+
});
49+
it('Change some fields', () => {
50+
expect(
51+
getChangedFields(complexObject, {
52+
a: 2,
53+
b: '3',
54+
c: { c1: {}, c2: [], c3: [] },
55+
d: ['item1', 'item2'],
56+
e: function () {},
57+
})
58+
).toEqual(['a', 'b', 'c', 'd']);
59+
expect(
60+
getChangedFields(
61+
{
62+
a: 2,
63+
b: '3',
64+
c: { c1: {}, c2: [], c3: [] },
65+
d: ['item1', 'item2'],
66+
e: function () {},
67+
},
68+
complexObject
69+
)
70+
).toEqual(['a', 'b', 'c', 'd']);
71+
});
72+
it('Delete one field', () => {
73+
expect(
74+
getChangedFields(complexObject, {
75+
a: 1,
76+
b: '2',
77+
c: { c1: {}, c2: [] },
78+
d: ['item1', 'item2', 'item2'],
79+
})
80+
).toEqual(['e']);
81+
expect(
82+
getChangedFields(
83+
{
84+
a: 1,
85+
b: '2',
86+
c: { c1: {}, c2: [] },
87+
d: ['item1', 'item2', 'item2'],
88+
},
89+
complexObject
90+
)
91+
).toEqual(['e']);
92+
});
93+
it('Delete some fields', () => {
94+
expect(
95+
getChangedFields(complexObject, {
96+
a: 1,
97+
b: '2',
98+
c: { c1: {}, c2: [] },
99+
})
100+
).toEqual(['d', 'e']);
101+
expect(
102+
getChangedFields(
103+
{
104+
a: 1,
105+
b: '2',
106+
c: { c1: {}, c2: [] },
107+
},
108+
complexObject
109+
)
110+
).toEqual(['d', 'e']);
111+
});
112+
it('Add one field', () => {
113+
expect(
114+
getChangedFields(complexObject, {
115+
...complexObject,
116+
f: {},
117+
})
118+
).toEqual(['f']);
119+
expect(
120+
getChangedFields(
121+
{
122+
...complexObject,
123+
f: {},
124+
},
125+
complexObject
126+
)
127+
).toEqual(['f']);
128+
});
129+
it('Add some fields', () => {
130+
expect(
131+
getChangedFields(complexObject, {
132+
...complexObject,
133+
f: {},
134+
g: [],
135+
})
136+
).toEqual(['f', 'g']);
137+
expect(
138+
getChangedFields(
139+
{
140+
...complexObject,
141+
f: {},
142+
g: [],
143+
},
144+
complexObject
145+
)
146+
).toEqual(['f', 'g']);
147+
});
148+
});

0 commit comments

Comments
 (0)