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
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"beforeSelfClosing": "always"
}
],
"react-hooks/exhaustive-deps": "error",
"curly": [
2
],
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ should change the heading of the (upcoming) version to include a major version b

- Added support for dynamic UI schema in array fields - the `items` property in `uiSchema` can now accept a function that returns a UI schema based on the array item's data, index, and form context ([#4706](https://github.com/rjsf-team/react-jsonschema-form/pull/4706))
- Fixed checkbox widget to use current value instead of event target in onFocus/onBlur handlers, fixing [#4704](https://github.com/rjsf-team/react-jsonschema-form/issues/4704)
- Updated all of the `XxxxField` components and `Form` to handle the new `path` parameter in `FieldProps.onChange`, making `Form` queue up changes so that they are all processed and no data is lost, fixing [#3367](https://github.com/rjsf-team/react-jsonschema-form/issues/3367)
- Updated a bug in `AltDateWidget` related to the `clear` button not working after the fix for [#3367](https://github.com/rjsf-team/react-jsonschema-form/issues/3367)
- Fixed the missing hook dependencies for the `CheckboxesWidget` so that they work properly

## @rjsf/chakra-ui

Expand All @@ -29,6 +32,7 @@ should change the heading of the (upcoming) version to include a major version b
## @rjsf/daisyui

- Fixed checkbox widget to use current value instead of event target in onFocus/onBlur handlers, fixing [#4704](https://github.com/rjsf-team/react-jsonschema-form/issues/4704)
- Fixed the missing hook dependencies in the `DateTimeWidget` and `DateWidget` so that they work properly

## @rjsf/fluentui-rc

Expand Down Expand Up @@ -62,6 +66,7 @@ should change the heading of the (upcoming) version to include a major version b

- Updated `UiSchema` type to support dynamic array item UI schemas - the `items` property can now be either a `UiSchema` object or a function that returns a `UiSchema` ([#4706](https://github.com/rjsf-team/react-jsonschema-form/pull/4706))
- Added `title` property to `RJSFValidationError` [PR](https://github.com/rjsf-team/react-jsonschema-form/pull/4700)
- BREAKING CHANGE: Updated the `FieldProps` interface's `onChange` handler to inject a new optional `path` before the `ErrorSchema` parameter as part of the fix for [#3367](https://github.com/rjsf-team/react-jsonschema-form/issues/3367)

## @rjsf/validator-ajv8

Expand All @@ -72,6 +77,9 @@ should change the heading of the (upcoming) version to include a major version b
- Added comprehensive documentation for dynamic UI schema feature with TypeScript examples ([#4706](https://github.com/rjsf-team/react-jsonschema-form/pull/4706))
- Updated array documentation to reference the new dynamic UI schema capabilities ([#4706](https://github.com/rjsf-team/react-jsonschema-form/pull/4706))
- Updated nearly all of the libraries in the `package.json` files to the latest non-breaking versions
- Fixed the broken `Custom Array` sample
- Improved the `Any Of with Custom Field` sample so that it renders using the appropriate theme components
- Updated the `custom-widgets-fields.md` and `v6.x upgrade guide.md` to document the BREAKING CHANGE to the `FieldProps.onChange` behavior

# 6.0.0-beta.13

Expand Down
2 changes: 0 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# react-jsonschema-form

The react-jsonschema-form docs have been moved [here](https://rjsf-team.github.io/react-jsonschema-form/docs).

We are in the process of migrating our versioned documentation. For documentation prior to version 5.0.0, please select the version in the bottom-right corner of this page.
54 changes: 37 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"build-serial": "nx run-many --target=build --parallel=1",
"start": "echo 'use \"npm run build\" from main directory and then \"npm start\" in the playground package'",
"pre-commit:husky": "nx run-many --parallel=1 --target=precommit",
"prepare": "husky install",
"prepare": "is-ci || husky",
"format": "prettier --write .",
"format-check": "prettier --check .",
"bump-all-packages": "echo 'NOTE: Make sure to sanity check the playground locally before commiting changes' && npm update --save && npm install && npm run lint && npm run build && npm run test",
Expand Down Expand Up @@ -69,6 +69,7 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7",
"is-ci": "^4.1.0",
"jest": "^30.0.5",
"jest-environment-jsdom": "^30.0.5",
"jest-watch-typeahead": "^3.0.1",
Expand Down
107 changes: 85 additions & 22 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ import {
createErrorHandler,
unwrapErrorHandler,
} from '@rjsf/utils';
import _cloneDeep from 'lodash/cloneDeep';
import _forEach from 'lodash/forEach';
import _get from 'lodash/get';
import _isEmpty from 'lodash/isEmpty';
import _isNil from 'lodash/isNil';
import _pick from 'lodash/pick';
import _set from 'lodash/set';
import _toPath from 'lodash/toPath';

import getDefaultRegistry from '../getDefaultRegistry';
Expand Down Expand Up @@ -272,6 +274,19 @@ export interface IChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema,
status?: 'submitted';
}

/** The definition of a pending change that will be processed in the `onChange` handler
*/
interface PendingChange<T> {
/** The path into the formData/errorSchema at which the `newValue`/`newErrorSchema` will be set */
path?: (number | string)[];
/** The new value to set into the formData */
newValue?: T;
/** The new errors to be set into the errorSchema, if any */
newErrorSchema?: ErrorSchema<T>;
/** The optional id of the field for which the change is being made */
id?: string;
}

/** The `Form` component renders the outer form and all the fields defined in the `schema` */
export default class Form<
T = any,
Expand All @@ -283,6 +298,10 @@ export default class Form<
*/
formElement: RefObject<any>;

/** The list of pending changes
*/
pendingChanges: PendingChange<T>[] = [];

/** Constructs the `Form` from the `props`. Will setup the initial state from the props. It will also call the
* `onChange` handler if the initially provided `formData` is modified to add missing default values as part of the
* state construction.
Expand Down Expand Up @@ -539,8 +558,7 @@ export default class Form<
let customValidateErrors = {};
if (typeof customValidate === 'function') {
const errorHandler = customValidate(prevFormData, createErrorHandler<T>(prevFormData), uiSchema);
const userErrorSchema = unwrapErrorHandler<T>(errorHandler);
customValidateErrors = userErrorSchema;
customValidateErrors = unwrapErrorHandler<T>(errorHandler);
}
return customValidateErrors;
}
Expand All @@ -550,7 +568,8 @@ export default class Form<
*
* @param formData - The new form data to validate
* @param schema - The schema used to validate against
* @param altSchemaUtils - The alternate schemaUtils to use for validation
* @param [altSchemaUtils] - The alternate schemaUtils to use for validation
* @param [retrievedSchema] - An optionally retrieved schema for per
*/
validate(
formData: T | undefined,
Expand Down Expand Up @@ -655,11 +674,16 @@ export default class Form<
const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);
const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', formData);
const fieldNames = this.getFieldNames(pathSchema, formData);
const newFormData = this.getUsedFormData(formData, fieldNames);
return newFormData;
return this.getUsedFormData(formData, fieldNames);
};

// Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
/** Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
*
* @param schemaErrors - The schema errors to filter
* @param [resolvedSchema] - An optionally resolved schema to use for performance reasons
* @param [formData] - The formData to help filter errors
* @private
*/
private filterErrorsBasedOnSchema(schemaErrors: ErrorSchema<T>, resolvedSchema?: S, formData?: any): ErrorSchema<T> {
const { retrievedSchema, schemaUtils } = this.state;
const _retrievedSchema = resolvedSchema ?? retrievedSchema;
Expand Down Expand Up @@ -705,23 +729,47 @@ export default class Form<
return filterNilOrEmptyErrors(filteredErrors, prevCustomValidateErrors);
}

/** Function to handle changes made to a field in the `Form`. This handler receives an entirely new copy of the
* `formData` along with a new `ErrorSchema`. It will first update the `formData` with any missing default fields and
* then, if `omitExtraData` and `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not
* in a form field. Then, the resulting formData will be validated if required. The state will be updated with the new
* updated (potentially filtered) `formData`, any errors that resulted from validation. Finally the `onChange`
* callback will be called if specified with the updated state.
/** Pushes the given change information into the `pendingChanges` array and then calls `processPendingChanges()` if
* the array only contains a single pending change.
*
* @param formData - The new form data from a change to a field
* @param newErrorSchema - The new `ErrorSchema` based on the field change
* @param id - The id of the field that caused the change
* @param newValue - The new form data from a change to a field
* @param [path] - The path to the change into which to set the formData
* @param [newErrorSchema] - The new `ErrorSchema` based on the field change
* @param [id] - The id of the field that caused the change
*/
onChange = (newValue: T | undefined, path?: (number | string)[], newErrorSchema?: ErrorSchema<T>, id?: string) => {
this.pendingChanges.push({ newValue, path, newErrorSchema, id });
if (this.pendingChanges.length === 1) {
this.processPendingChange();
}
};

/** Function to handle changes made to a field in the `Form`. This handler gets the first change from the
* `pendingChanges` list, containing the `newValue` for the `formData` and the `path` at which the `newValue` is to be
* updated, along with a new, optional `ErrorSchema` for that same `path` and potentially the `id` of the field being
* changed. It will first update the `formData` with any missing default fields and then, if `omitExtraData` and
* `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not in a form field. Then, the
* resulting `formData` will be validated if required. The state will be updated with the new updated (potentially
* filtered) `formData`, any errors that resulted from validation. Finally the `onChange` callback will be called, if
* specified, with the updated state and the `processPendingChange()` function is called again.
*/
onChange = (formData: T | undefined, newErrorSchema?: ErrorSchema<T>, id?: string) => {
const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props;
const { schemaUtils, schema } = this.state;
processPendingChange() {
if (this.pendingChanges.length === 0) {
return;
}
const { newValue, path, id } = this.pendingChanges[0];
let { newErrorSchema } = this.pendingChanges[0];
const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange, idPrefix = '' } = this.props;
const { formData: oldFormData, schemaUtils, schema, errorSchema } = this.state;

const isRootPath = !path || path.length === 0 || (path.length === 1 && path[0] === idPrefix);
let retrievedSchema = this.state.retrievedSchema;
let formData = isRootPath ? newValue : _cloneDeep(oldFormData);
if (isObject(formData) || Array.isArray(formData)) {
if (!isRootPath) {
// If the newValue is not on the root path, then set it into the form data
_set(formData, path, newValue);
}
const newState = this.getStateFromProps(this.props, formData);
formData = newState.formData;
retrievedSchema = newState.retrievedSchema;
Expand All @@ -738,6 +786,13 @@ export default class Form<
};
}

// First update the value in the newErrorSchema in a copy of the old error schema if it was specified and the path
// is not the root
if (newErrorSchema && !isRootPath) {
const errorSchemaCopy = _cloneDeep(errorSchema);
_set(errorSchemaCopy, path, newErrorSchema);
newErrorSchema = errorSchemaCopy;
}
if (mustValidate) {
const schemaValidation = this.validate(newFormData, schema, schemaUtils, retrievedSchema);
let errors = schemaValidation.errors;
Expand All @@ -762,6 +817,7 @@ export default class Form<
schemaValidationErrorSchema,
};
} else if (!noValidate && newErrorSchema) {
// Merging 'newErrorSchema' into 'errorSchema' to display the custom raised errors.
const errorSchema = extraErrors
? (mergeObjects(newErrorSchema, extraErrors, 'preventDuplicates') as ErrorSchema<T>)
: newErrorSchema;
Expand All @@ -771,8 +827,15 @@ export default class Form<
errors: toErrorList(errorSchema),
};
}
this.setState(state as FormState<T, S, F>, () => onChange && onChange({ ...this.state, ...state }, id));
};
this.setState(state as FormState<T, S, F>, () => {
if (onChange) {
onChange({ ...this.state, ...state }, id);
}
// Now remove the change we just completed and call this again
this.pendingChanges.shift();
this.processPendingChange();
});
}

/**
* If the retrievedSchema has changed the new retrievedSchema is returned.
Expand Down Expand Up @@ -1029,7 +1092,7 @@ export default class Form<
const {
children,
id,
idPrefix,
idPrefix = '',
idSeparator,
className = '',
tagName,
Expand Down Expand Up @@ -1082,7 +1145,7 @@ export default class Form<
>
{showErrorList === 'top' && this.renderErrors(registry)}
<_SchemaField
name=''
name={idPrefix}
schema={schema}
uiSchema={uiSchema}
errorSchema={errorSchema}
Expand Down
Loading