Skip to content

Commit

Permalink
fix: formData change clear errorMessage (#4429)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
JinYuSha0 and heath-freenome authored Jan 9, 2025
1 parent a521990 commit b6c1825
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 9 deletions.
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ should change the heading of the (upcoming) version to include a major version b

# 5.24.0

## @rjsf/core
## @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.
- 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)

## @rjsf/utils

- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)
- 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)
- 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)
- 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).

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

- Support allowing raising errors from within a custom Widget [#2718](https://github.com/rjsf-team/react-jsonschema-form/issues/2718)
- 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)
- 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)

## @rjsf/utils

- Updated the `WidgetProps` type to add `es?: ErrorSchema<T>, id?: string` to the params of the `onChange` handler function
- 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)
- 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)
- 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)
- 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`.

## Dev / docs / playground

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

# 5.19.4

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ErrorTransformer,
FormContextType,
GenericObjectType,
getChangedFields,
getTemplate,
getUiOptions,
IdSchema,
Expand Down Expand Up @@ -316,16 +317,21 @@ export default class Form<
prevState: FormState<T, S, F>
): { nextState: FormState<T, S, F>; shouldUpdate: true } | { shouldUpdate: false } {
if (!deepEquals(this.props, prevProps)) {
const formDataChangedFields = getChangedFields(this.props.formData, prevProps.formData);
const isSchemaChanged = !deepEquals(prevProps.schema, this.props.schema);
const isFormDataChanged = !deepEquals(prevProps.formData, this.props.formData);
// When formData is not an object, getChangedFields returns an empty array.
// In this case, deepEquals is most needed to check again.
const isFormDataChanged =
formDataChangedFields.length > 0 || !deepEquals(prevProps.formData, this.props.formData);
const nextState = this.getStateFromProps(
this.props,
this.props.formData,
// If the `schema` has changed, we need to update the retrieved schema.
// Or if the `formData` changes, for example in the case of a schema with dependencies that need to
// match one of the subSchemas, the retrieved schema must be updated.
isSchemaChanged || isFormDataChanged ? undefined : this.state.retrievedSchema,
isSchemaChanged
isSchemaChanged,
formDataChangedFields
);
const shouldUpdate = !deepEquals(nextState, prevState);
return { nextState, shouldUpdate };
Expand Down Expand Up @@ -375,13 +381,15 @@ export default class Form<
* @param inputFormData - The new or current data for the `Form`
* @param retrievedSchema - An expanded schema, if not provided, it will be retrieved from the `schema` and `formData`.
* @param isSchemaChanged - A flag indicating whether the schema has changed.
* @param formDataChangedFields - The changed fields of `formData`
* @returns - The new state for the `Form`
*/
getStateFromProps(
props: FormProps<T, S, F>,
inputFormData?: T,
retrievedSchema?: S,
isSchemaChanged = false
isSchemaChanged = false,
formDataChangedFields: string[] = []
): FormState<T, S, F> {
const state: FormState<T, S, F> = this.state || {};
const schema = 'schema' in props ? props.schema : this.props.schema;
Expand Down Expand Up @@ -460,6 +468,17 @@ export default class Form<
const currentErrors = getCurrentErrors();
errors = currentErrors.errors;
errorSchema = currentErrors.errorSchema;
if (formDataChangedFields.length > 0) {
const newErrorSchema = formDataChangedFields.reduce((acc, key) => {
acc[key] = undefined;
return acc;
}, {} as Record<string, undefined>);
errorSchema = schemaValidationErrorSchema = mergeObjects(
currentErrors.errorSchema,
newErrorSchema,
'preventDuplicates'
) as ErrorSchema<T>;
}
}

if (props.extraErrors) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/Form.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4489,8 +4489,8 @@ describe('Form omitExtraData and liveOmit', () => {
// // error should still be present.
errors = node.querySelectorAll('.error-detail');
// screen.debug();
expect(errors).to.have.lengthOf(1);
expect(errors[0].textContent).to.be.eql("must have required property 'input'");
// change formData and make sure the error disappears.
expect(errors).to.have.lengthOf(0);

// trigger programmatic validation again and make sure the error disappears.
act(() => {
Expand Down
40 changes: 40 additions & 0 deletions packages/utils/src/getChangedFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import keys from 'lodash/keys';
import pickBy from 'lodash/pickBy';
import isPlainObject from 'lodash/isPlainObject';
import get from 'lodash/get';
import difference from 'lodash/difference';
import deepEquals from './deepEquals';

/**
* Compares two objects and returns the names of the fields that have changed.
* This function iterates over each field of object `a`, using `_.isEqual` to compare the field value
* with the corresponding field value in object `b`. If the values are different, the field name will
* be included in the returned array.
*
* @param {unknown} a - The first object, representing the original data to compare.
* @param {unknown} b - The second object, representing the updated data to compare.
* @returns {string[]} - An array of field names that have changed.
*
* @example
* const a = { name: 'John', age: 30 };
* const b = { name: 'John', age: 31 };
* const changedFields = getChangedFields(a, b);
* console.log(changedFields); // Output: ['age']
*/
export default function getChangedFields(a: unknown, b: unknown): string[] {
const aIsPlainObject = isPlainObject(a);
const bIsPlainObject = isPlainObject(b);
// If strictly equal or neither of them is a plainObject returns an empty array
if (a === b || (!aIsPlainObject && !bIsPlainObject)) {
return [];
}
if (aIsPlainObject && !bIsPlainObject) {
return keys(a);
} else if (!aIsPlainObject && bIsPlainObject) {
return keys(b);
} else {
const unequalFields = keys(pickBy(a as object, (value, key) => !deepEquals(value, get(b, key))));
const diffFields = difference(keys(b), keys(a));
return [...unequalFields, ...diffFields];
}
}
2 changes: 2 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import utcToLocal from './utcToLocal';
import validationDataMerge from './validationDataMerge';
import withIdRefPrefix from './withIdRefPrefix';
import getOptionMatchingSimpleDiscriminator from './getOptionMatchingSimpleDiscriminator';
import getChangedFields from './getChangedFields';

export * from './types';
export * from './enums';
Expand Down Expand Up @@ -82,6 +83,7 @@ export {
examplesId,
ErrorSchemaBuilder,
findSchemaDefinition,
getChangedFields,
getDateElementProps,
getDiscriminatorFieldFromSchema,
getInputProps,
Expand Down
148 changes: 148 additions & 0 deletions packages/utils/test/getChangedFields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { getChangedFields } from '../src';
import cloneDeep from 'lodash/cloneDeep';

const complexObject = {
a: 1,
b: '2',
c: { c1: {}, c2: [] },
d: ['item1', 'item2', 'item2'],
e: function () {},
};
const complexObjectKeys = ['a', 'b', 'c', 'd', 'e'];

describe('getChangedFields()', () => {
it('Empty parameter', () => {
expect(getChangedFields(undefined, undefined)).toEqual([]);
expect(getChangedFields(complexObject, undefined)).toEqual(complexObjectKeys);
expect(getChangedFields(undefined, complexObject)).toEqual(complexObjectKeys);
});
it('Both not plainObject parameter', () => {
expect(getChangedFields(1, 2)).toEqual([]);
expect(getChangedFields(2, '1')).toEqual([]);
expect(
getChangedFields(
function a() {},
function b() {}
)
).toEqual([]);
expect(getChangedFields(new Date(), new Date())).toEqual([]);
});
it('One is not plainObject parameter', () => {
expect(getChangedFields(1, complexObject)).toEqual(complexObjectKeys);
expect(getChangedFields('1', complexObject)).toEqual(complexObjectKeys);
expect(getChangedFields(function noop() {}, complexObject)).toEqual(complexObjectKeys);
expect(getChangedFields(new Date(), complexObject)).toEqual(complexObjectKeys);

expect(getChangedFields(complexObject, 1)).toEqual(complexObjectKeys);
expect(getChangedFields(complexObject, '1')).toEqual(complexObjectKeys);
expect(getChangedFields(complexObject, function noop() {})).toEqual(complexObjectKeys);
expect(getChangedFields(complexObject, new Date())).toEqual(complexObjectKeys);
});
it('Deep equal', () => {
expect(getChangedFields(complexObject, complexObject)).toEqual([]);
expect(getChangedFields(complexObject, cloneDeep(complexObject))).toEqual([]);
});
it('Change one field', () => {
expect(getChangedFields(complexObject, { ...cloneDeep(complexObject), a: 2 })).toEqual(['a']);
expect(getChangedFields({ ...cloneDeep(complexObject), a: 2 }, complexObject)).toEqual(['a']);
});
it('Change some fields', () => {
expect(
getChangedFields(complexObject, {
a: 2,
b: '3',
c: { c1: {}, c2: [], c3: [] },
d: ['item1', 'item2'],
e: function () {},
})
).toEqual(['a', 'b', 'c', 'd']);
expect(
getChangedFields(
{
a: 2,
b: '3',
c: { c1: {}, c2: [], c3: [] },
d: ['item1', 'item2'],
e: function () {},
},
complexObject
)
).toEqual(['a', 'b', 'c', 'd']);
});
it('Delete one field', () => {
expect(
getChangedFields(complexObject, {
a: 1,
b: '2',
c: { c1: {}, c2: [] },
d: ['item1', 'item2', 'item2'],
})
).toEqual(['e']);
expect(
getChangedFields(
{
a: 1,
b: '2',
c: { c1: {}, c2: [] },
d: ['item1', 'item2', 'item2'],
},
complexObject
)
).toEqual(['e']);
});
it('Delete some fields', () => {
expect(
getChangedFields(complexObject, {
a: 1,
b: '2',
c: { c1: {}, c2: [] },
})
).toEqual(['d', 'e']);
expect(
getChangedFields(
{
a: 1,
b: '2',
c: { c1: {}, c2: [] },
},
complexObject
)
).toEqual(['d', 'e']);
});
it('Add one field', () => {
expect(
getChangedFields(complexObject, {
...complexObject,
f: {},
})
).toEqual(['f']);
expect(
getChangedFields(
{
...complexObject,
f: {},
},
complexObject
)
).toEqual(['f']);
});
it('Add some fields', () => {
expect(
getChangedFields(complexObject, {
...complexObject,
f: {},
g: [],
})
).toEqual(['f', 'g']);
expect(
getChangedFields(
{
...complexObject,
f: {},
g: [],
},
complexObject
)
).toEqual(['f', 'g']);
});
});

0 comments on commit b6c1825

Please sign in to comment.