Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: formData change clear errorMessage #4429

Merged
merged 8 commits into from
Jan 9, 2025
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
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']);
});
});
Loading