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
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ should change the heading of the (upcoming) version to include a major version b
- Updated `ArrayField` and `ObjectField` to check whether it `shouldRenderOptionalData()` and if true, calls `ObjectDataControlsField` and passes the result to its associated render template as `optionalDataControl`
- Updated `ArrayFieldTemplate`, `ObjectFieldTemplate`, `TitleField` to add support for the new `optionalDataControl` feature
- Added the new `OptionalDataControlTemplate` to the theme, adding it to the `templates` list
- Updated `Form` as follows to fix [#4796](https://github.com/rjsf-team/react-jsonschema-form/issues/4796)
- Refactored the `liveValidate()` and `mergeErrors()` functions out of `getStateFromProp()` and `processPendingChange()`
- Added new, optional `customErrors?: ErrorSchemaBuilder<T>` to the `FormState`, updating the `IChangeEvent` interface to remove all of the private variables
- Reworked the `newErrorSchema` handling in `processPendingChange()` to simplify the handling since `newErrorSchema` is now path-specific, adding `newErrorSchema` to `customErrors` when they don't match an existing validator-based validation
- This rework resulted in any custom errors passed from custom widgets/fields will now be remembered during the validation stage
- Removed the now unused `getPreviousCustomValidateErrors()` and `filterErrorsBasedOnSchema()` methods
- Updated `LayoutGridField` to simplify `onFieldChange()` to just return the given `errorSchema` now that it is path-specific, fixing [#4796](https://github.com/rjsf-team/react-jsonschema-form/issues/4796)
- Updated `NullField` to pass `fieldPathId.path` for the `onChange()` instead of `[name]`

## @rjsf/daisyui

Expand Down Expand Up @@ -99,16 +107,18 @@ should change the heading of the (upcoming) version to include a major version b
- Updated `getDefaultFormState` to fix an issue where optional array props had their default set to an empty array when they shouldn't be
- Updated the `TranslatableString` enum to add three new strings in support of the new feature: `OptionalObjectAdd`, `OptionalObjectRemove` and `OptionalObjectEmptyMsg`
- Added four new utility functions: `isFormDataAvailable()`, `isRootSchema()`, `optionalControlsId()`, and `shouldRenderOptionalField()`
- Updated `validationDataMerge()` to add an additional, optional parameter `preventDuplicates = false`, that causes the `mergeObjects()` call to receive `preventDuplicates` instead of `true`

## Dev / docs / playground

- Updated docs for `getDefaultFormState` to reflect addition of the `initialDefaultsGenerated` prop
- Updated `utility-function.me` docs to add documentation for the new functions
- Updated `utility-function.me` docs to add documentation for the new functions and to update the `validationDataMerge()` function's new parameter
- Also updated docs for `retrieveSchema` and `SchemaUtilsType` for the new prop
- Updated `uiSchema.md` to add documentation for the new `enableOptionalDataFieldForType` prop
- Updated the `v6x upgrade guide.md` to document the new feature and utility functions and changes to `retrieveSchema`
- Updated the playground to add a new `Optional Data Controls` example
- Updated the snapshot and jest tests for `Form` to test the new `Optional Data Controls` feature
- Updated `custom-widgets-fields.md` to change the documentation around passing errors via `onChange()` to reflect the new reality
- Updated the `v6x upgrade guide.md` to document the new feature, utility functions and changes to existing method parameters

# 6.0.0-beta-20

Expand Down
276 changes: 143 additions & 133 deletions packages/core/src/components/Form.tsx

Large diffs are not rendered by default.

10 changes: 2 additions & 8 deletions packages/core/src/components/fields/LayoutGridField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
UiSchema,
ITEMS_KEY,
} from '@rjsf/utils';
import cloneDeep from 'lodash/cloneDeep';
import each from 'lodash/each';
import flatten from 'lodash/flatten';
import get from 'lodash/get';
Expand Down Expand Up @@ -705,13 +704,8 @@ export default class LayoutGridField<
*/
onFieldChange = (dottedPath: string) => {
return (value: T | undefined, path: FieldPathList, errSchema?: ErrorSchema<T>, id?: string) => {
const { onChange, errorSchema } = this.props;
let newErrorSchema = errorSchema;
if (errSchema && errorSchema) {
newErrorSchema = cloneDeep(errorSchema);
set(newErrorSchema, dottedPath, errSchema);
}
onChange(value, path, newErrorSchema, id);
const { onChange } = this.props;
Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out this change fixed the bulk of the performance issue.

onChange(value, path, errSchema, id);
};
};

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/components/fields/NullField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { FieldProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf
function NullField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: FieldProps<T, S, F>,
) {
const { name, formData, onChange } = props;
const { formData, onChange, fieldPathId } = props;
useEffect(() => {
if (formData === undefined) {
onChange(null as unknown as T, [name]);
onChange(null as unknown as T, fieldPathId.path);
Copy link
Member Author

Choose a reason for hiding this comment

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

Whoops, missed one place from my previous PR

}
}, [name, formData, onChange]);
}, [fieldPathId, formData, onChange]);

return null;
}
Expand Down
64 changes: 64 additions & 0 deletions packages/core/test/ArrayField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3337,6 +3337,38 @@ describe('ArrayField', () => {
expect(errorMessages).to.have.length(0);
});

it('should clear an error if value is entered correctly', () => {
const { node } = createFormComponent({
schema,
formData: [
{
text: 'y',
},
],
templates,
fields: {
ArrayField: ArrayFieldTest,
},
});

const inputs = node.querySelectorAll('.rjsf-field-string input[type=text]');
act(() => {
fireEvent.change(inputs[0], { target: { value: 'test' } });
});

let errorMessages = node.querySelectorAll('#root_0_text__error');
expect(errorMessages).to.have.length(1);
const errorMessageContent = node.querySelector('#root_0_text__error .text-danger').textContent;
expect(errorMessageContent).to.contain('Value must be "Appie"');

act(() => {
fireEvent.change(inputs[0], { target: { value: 'Appie' } });
});

errorMessages = node.querySelectorAll('#root_0_text__error');
expect(errorMessages).to.have.length(0);
});

it('raise an error and check if the error is displayed using custom text widget', () => {
const { node } = createFormComponent({
schema,
Expand Down Expand Up @@ -3384,6 +3416,38 @@ describe('ArrayField', () => {
const errorMessages = node.querySelectorAll('#root_0_text__error');
expect(errorMessages).to.have.length(0);
});

it('should clear an error if value is entered correctly using custom text widget', () => {
const { node } = createFormComponent({
schema,
formData: [
{
text: 'y',
},
],
templates,
widgets: {
TextWidget: TextWidgetTest,
},
});

const inputs = node.querySelectorAll('.rjsf-field-string input[type=text]');
act(() => {
fireEvent.change(inputs[0], { target: { value: 'hello' } });
});

let errorMessages = node.querySelectorAll('#root_0_text__error');
expect(errorMessages).to.have.length(1);
const errorMessageContent = node.querySelector('#root_0_text__error .text-danger').textContent;
expect(errorMessageContent).to.contain('Value must be "test"');

act(() => {
fireEvent.change(inputs[0], { target: { value: 'test' } });
});

errorMessages = node.querySelectorAll('#root_0_text__error');
expect(errorMessages).to.have.length(0);
});
});

describe('Dynamic uiSchema.items function', () => {
Expand Down
9 changes: 4 additions & 5 deletions packages/core/test/LayoutGridField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,7 @@ describe('LayoutGridField', () => {
expect(props.onFocus).toHaveBeenCalledWith(fieldId, '');
// Type to trigger the onChange
await userEvent.type(input, 'foo');
expect(props.onChange).toHaveBeenCalledWith('foo', fieldPathId.path, props.errorSchema, fieldId);
expect(props.onChange).toHaveBeenCalledWith('foo', fieldPathId.path, undefined, fieldId);
// Tab out of the input field to cause the blur
await userEvent.tab();
expect(props.onBlur).toHaveBeenCalledWith(fieldId, 'foo');
Expand All @@ -1474,7 +1474,7 @@ describe('LayoutGridField', () => {
expect(props.onFocus).toHaveBeenCalledWith(fieldId, '');
// Type to trigger the onChange
await userEvent.type(input, 'foo');
expect(props.onChange).toHaveBeenCalledWith('foo', fieldPathId.path, props.errorSchema, fieldId);
expect(props.onChange).toHaveBeenCalledWith('foo', fieldPathId.path, undefined, fieldId);
// Tab out of the input field to cause the blur
await userEvent.tab();
expect(props.onBlur).toHaveBeenCalledWith(fieldId, 'foo');
Expand All @@ -1500,7 +1500,7 @@ describe('LayoutGridField', () => {
expect(props.onFocus).toHaveBeenCalledWith(fieldId, '');
// Type to trigger the onChange
await userEvent.type(input, 'foo');
expect(props.onChange).toHaveBeenCalledWith('foo', fieldPathId.path, props.errorSchema, fieldId);
expect(props.onChange).toHaveBeenCalledWith('foo', fieldPathId.path, undefined, fieldId);
// Tab out of the input field to cause the blur
await userEvent.tab();
expect(props.onBlur).toHaveBeenCalledWith(fieldId, 'foo');
Expand Down Expand Up @@ -1654,8 +1654,7 @@ describe('LayoutGridField', () => {
const input = within(fields[0]).getByRole('textbox');
expect(input).toHaveValue(props.formData[fieldName]);
await userEvent.type(input, '!');
const expectedErrors = new ErrorSchemaBuilder().addErrors(ERRORS, fieldName).ErrorSchema;
expect(props.onChange).toHaveBeenCalledWith('foo!', fieldPathId.path, expectedErrors, fieldId);
expect(props.onChange).toHaveBeenCalledWith('foo!', fieldPathId.path, EXTRA_ERROR, fieldId);
});
test('renderCondition, condition fails, field and null value, NONE operator, no data', () => {
const gridProps = { operator: Operators.NONE, field: 'simpleString', value: null };
Expand Down
51 changes: 51 additions & 0 deletions packages/core/test/ObjectField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,32 @@ describe('ObjectField', () => {
expect(errorMessages).to.have.length(0);
});

it('should clear an error if value is entered correctly', () => {
const { node } = createFormComponent({
schema,
fields: {
ObjectField: ObjectFieldTest,
},
});

const inputs = node.querySelectorAll('.rjsf-field-string input[type=text]');
act(() => {
fireEvent.change(inputs[0], { target: { value: 'hello' } });
});

let errorMessages = node.querySelectorAll('#root_foo__error');
expect(errorMessages).to.have.length(1);
const errorMessageContent = node.querySelector('#root_foo__error .text-danger').textContent;
expect(errorMessageContent).to.contain('Value must be "test"');

act(() => {
fireEvent.change(inputs[0], { target: { value: 'test' } });
});

errorMessages = node.querySelectorAll('#root_foo__error');
expect(errorMessages).to.have.length(0);
});

it('raise an error and check if the error is displayed using custom text widget', () => {
const { node } = createFormComponent({
schema,
Expand Down Expand Up @@ -411,6 +437,31 @@ describe('ObjectField', () => {
const errorMessages = node.querySelectorAll('#root_foo__error');
expect(errorMessages).to.have.length(0);
});

it('should clear an error if value is entered correctly using custom text widget', () => {
const { node } = createFormComponent({
schema,
widgets: {
TextWidget: TextWidgetTest,
},
});

const inputs = node.querySelectorAll('.rjsf-field-string input[type=text]');
act(() => {
fireEvent.change(inputs[0], { target: { value: 'hello' } });
});

let errorMessages = node.querySelectorAll('#root_foo__error');
expect(errorMessages).to.have.length(1);
const errorMessageContent = node.querySelector('#root_foo__error .text-danger').textContent;
expect(errorMessageContent).to.contain('Value must be "test"');
act(() => {
fireEvent.change(inputs[0], { target: { value: 'test' } });
});

errorMessages = node.querySelectorAll('#root_foo__error');
expect(errorMessages).to.have.length(0);
});
});

describe('fields ordering', () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/core/test/StringField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,32 @@ describe('StringField', () => {
expect(errorMessages).to.have.length(0);
});

it('should clear an error if value is entered correctly', () => {
const { node } = createFormComponent({
schema: { type: 'string' },
fields: {
StringField: StringFieldTest,
},
});

const inputs = node.querySelectorAll('.rjsf-field-string input[type=text]');
act(() => {
fireEvent.change(inputs[0], { target: { value: 'hello' } });
});

let errorMessages = node.querySelectorAll('#root__error');
expect(errorMessages).to.have.length(1);
const errorMessageContent = node.querySelector('#root__error .text-danger').textContent;
expect(errorMessageContent).to.contain('Value must be "test"');

act(() => {
fireEvent.change(inputs[0], { target: { value: 'test' } });
});

errorMessages = node.querySelectorAll('#root__error');
expect(errorMessages).to.have.length(0);
});

it('raise an error and check if the error is displayed using custom text widget', () => {
const { node } = createFormComponent({
schema: { type: 'string' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,11 @@ The default widgets you can override are:
## Raising errors from within a custom widget or field

You can raise custom 'live validation' errors by overriding the `onChange` method to provide feedback while users are actively changing the form data.
Note that these errors are temporary and are not recognized during the form validation process.
If you do set errors this way, you must also clear them this way by passing `undefined` to the `onChange()` for the `errorSchema` parameter.

:::warning

This method of raising errors _only_ runs during `onChange`, i.e. when the user is changing data. This will not catch errors `onSubmit`, i.e when submitting the form.
If you wish to add generic validation logic for your component, you should use the [`customValidate` Form prop](../api-reference/form-props.md#customvalidate).
While these errors are retained during validation, it is still preferred for you to use the [`customValidate` Form prop](../api-reference/form-props.md#customvalidate) mechanism instead.

:::

Expand Down
1 change: 1 addition & 0 deletions packages/docs/docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,7 @@ If no `additionalErrorSchema` is passed, then `validationData` is returned.

- validationData: ValidationData&lt;T> - The current `ValidationData` into which to merge the additional errors
- [additionalErrorSchema]: ErrorSchema&lt;T> | undefined - The optional additional set of errors in an `ErrorSchema`
- [preventDuplicates=false]: boolean - Optional flag, if true, will call `mergeObjects()` with `preventDuplicates`

#### Returns

Expand Down
2 changes: 2 additions & 0 deletions packages/docs/docs/migration-guides/v6.x upgrade guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ function CustomField(props: FieldProps) {

The same change also applies to the `ErrorSchema` object being passed to the `Form`.
Therefore, if your custom `Field` also updated the `ErrorSchema` to add a new error, now you just need to pass that error as well.
Finally, the errors are preserved across validations, so if you want to clear an error you passed via `onChange`, you will have to pass `undefined`

Here is an example of a custom `Field` that was updated due to this change:

Expand Down Expand Up @@ -743,6 +744,7 @@ Three new validator-based utility functions are available in `@rjsf/utils`:
- `getDefaultFormState()`: Added an optional `initialDefaultsGenerated` boolean flag that indicates whether or not initial defaults have been generated
- `retrieveSchema()`: Added an optional `resolveAnyOfOrOneOfRefs` boolean flag that causes the internal `resolveAllSchemas()` to resolve `$ref`s inside of the options of `anyOf`/`oneOf` schemas
- This new optional flag was added to the `SchemaUtilsType` interface's version of `retrieveSchema()` as well.
- `validationDataMerge()`: Added optional `preventDuplicates` boolean flag that causes the `mergeObjects()` call to receive `preventDuplicates` instead of `true`

### Optional Data Controls

Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/ErrorSchemaBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import get from 'lodash/get';
import set from 'lodash/set';
import setWith from 'lodash/setWith';

import { ErrorSchema } from './types';
import { ErrorSchema, FieldPathList } from './types';
import { ERRORS_KEY } from './constants';

/** Represents the type of the path which can be a string of dotted path values or a list of string or numbers where
* numbers represent array indexes/
*/
export type PathType = string | (string | number)[];
export type PathType = string | FieldPathList;

/** The `ErrorSchemaBuilder<T>` is used to build an `ErrorSchema<T>` since the definition of the `ErrorSchema` type is
* designed for reading information rather than writing it. Use this class to add, replace or clear errors in an error
Expand Down
8 changes: 7 additions & 1 deletion packages/utils/src/validationDataMerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { ErrorSchema, ValidationData } from './types';
*
* @param validationData - The current `ValidationData` into which to merge the additional errors
* @param [additionalErrorSchema] - The optional additional set of errors in an `ErrorSchema`
* @param [preventDuplicates=false] - Optional flag, if true, will call `mergeObjects()` with `preventDuplicates`
* @returns - The `validationData` with the additional errors from `additionalErrorSchema` merged into it, if provided.
*/
export default function validationDataMerge<T = any>(
validationData: ValidationData<T>,
additionalErrorSchema?: ErrorSchema<T>,
preventDuplicates = false,
): ValidationData<T> {
if (!additionalErrorSchema) {
return validationData;
Expand All @@ -24,7 +26,11 @@ export default function validationDataMerge<T = any>(
let errors = toErrorList(additionalErrorSchema);
let errorSchema = additionalErrorSchema;
if (!isEmpty(oldErrorSchema)) {
errorSchema = mergeObjects(oldErrorSchema, additionalErrorSchema, true) as ErrorSchema<T>;
errorSchema = mergeObjects(
oldErrorSchema,
additionalErrorSchema,
preventDuplicates ? 'preventDuplicates' : true,
) as ErrorSchema<T>;
errors = [...oldErrors].concat(errors);
}
return { errorSchema, errors };
Expand Down
15 changes: 15 additions & 0 deletions packages/utils/test/validationDataMerge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,19 @@ describe('validationDataMerge()', () => {
};
expect(validationDataMerge(validationData, errorSchema)).toEqual(expected);
});
it('Returns merged data when additionalErrorSchema is passed, prevent duplicates', () => {
const oldError = 'ajv error';
const validationData: ValidationData<any> = {
errorSchema: { [ERRORS_KEY]: [oldError] } as ErrorSchema,
errors: [{ stack: oldError, name: 'foo', schemaPath: '.foo' }],
};
const errors = ['custom errors'];
const customErrors = [{ property: '.', message: errors[0], stack: `. ${errors[0]}` }];
const errorSchema: ErrorSchema = { [ERRORS_KEY]: errors } as ErrorSchema;
const expected = {
errorSchema: { [ERRORS_KEY]: [oldError, ...errors] },
errors: [...validationData.errors, ...customErrors],
};
expect(validationDataMerge(validationData, errorSchema, true)).toEqual(expected);
});
});