From 861a404229883b6346656d7533d7c2ac319cca92 Mon Sep 17 00:00:00 2001 From: Heath Chiavettone Date: Mon, 3 Oct 2022 10:34:22 -0700 Subject: [PATCH] fix: #2768 by returning id on the Form.onChange handler - In @rjsf/utils, updated the `FieldProps` type for `onChange` to add an optional `id: string` parameter, making `onChange` for `FieldTemplateProps` use `FieldProps` definition - In @rjsf/core, updated the library to support returning an optional second parameter, `id: string`, to the `onChange()` prop on `Form` as follows: - Updated the `Form.onChange()` callback handler passed to all fields to take an optional third parameter, `id: string`, passing it out to the `onChange()` prop call - Updated `ArrayField` to make the callback returned from the `onChangeForIndex()` method take the additional `id` parameter and pass it along to the `props.onChange()` handler - Updated `MultiSchemaField` to make the `onOptionChange()` method pass along the `this.getFieldId()` value to the `props.onChange()` handler - Refactored the `id` logic for the select widget used in the `MultiSchemaField` into the `getFieldId()` method - Updated `ObjectField` to make the callback returned from the `onPropertyChange()` method take the additional `id` parameter and pass it along to the `props.onChange()` handler - Updated `SchemaField` to add a new `handleFieldComponentChange()` callback that ensures that an `id` is passed up the `onChange` callback chain - Updated the render of the `FieldComponent` to pass the `handleFieldComponentChange()` as the `onChange` handler - In @rjsf/playground, updated the `onFormDataChange` handler to log the `id` of the changed field if it is provided - Updated the `form-props.md` documentation to describe the new `id` parameter that may be returned - Updated the `CHANGELOG.md` accordingly --- CHANGELOG.md | 10 +++++++- docs/api-reference/form-props.md | 9 ++++++-- package-lock.json | 1 - packages/core/src/components/Form.tsx | 10 ++++---- .../core/src/components/fields/ArrayField.tsx | 9 ++++---- .../components/fields/MultiSchemaField.tsx | 19 +++++++++++---- .../src/components/fields/ObjectField.tsx | 5 ++-- .../src/components/fields/SchemaField.tsx | 23 +++++++++++++------ packages/playground/src/app.jsx | 8 +++++-- packages/utils/src/types.ts | 4 ++-- 10 files changed, 68 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ff827db32..14c76b82cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,16 @@ should change the heading of the (upcoming) version to include a major version b --> # 5.0.0-beta.11 -## @rjsf/playground +## @rjsf/core +- Extended `Form.onChange` to optionally return the `id` of the field that caused the change, fixing (https://github.com/rjsf-team/react-jsonschema-form/issues/2768) + +## @rjsf/utils +- Updated the `onChange` prop on `FieldProps` and `FieldTemplateProps` to add an optional `id` parameter to the callback. + +## Dev / docs / playground - Added an error boundary to prevent the entire app from crashing when an error is thrown by Form. See [#3164](https://github.com/rjsf-team/react-jsonschema-form/pull/3164) for closed issues. +- Updated the playground to log the `id` of the field being changed on the `onChange` handler +- Updated `form-props.md` file to describe the new `id` parameter being returned by the `Form.onChange` handler # 5.0.0-beta.10 diff --git a/docs/api-reference/form-props.md b/docs/api-reference/form-props.md index 0fac4cc2af..f25bcb7d8b 100644 --- a/docs/api-reference/form-props.md +++ b/docs/api-reference/form-props.md @@ -193,7 +193,10 @@ Sometimes you may want to trigger events or modify external state when a field h ## onChange -If you plan on being notified every time the form data are updated, you can pass an `onChange` handler, which will receive the same args as `onSubmit` any time a value is updated in the form. +If you plan on being notified every time the form data are updated, you can pass an `onChange` handler, which will receive the same first argument as `onSubmit` any time a value is updated in the form. +It will also receive, as the second argument, the `id` of the field which experienced the change. +Generally, this will be the `id` of the field for which input data is modified. +In the case of adding/removing of new fields in arrays or objects with `additionalProperties` and the rearranging of items in arrays, the `id` will be that of the array or object itself, rather than the item/field being added, removed or moved. ## onError @@ -218,7 +221,9 @@ Sometimes you may want to trigger events or modify external state when a field h ## onSubmit -You can pass a function as the `onSubmit` prop of your `Form` component to listen to when the form is submitted and its data are valid. It will be passed a result object having a `formData` attribute, which is the valid form data you're usually after. The original event will also be passed as a second parameter: +You can pass a function as the `onSubmit` prop of your `Form` component to listen to when the form is submitted and its data are valid. +It will be passed a result object having a `formData` attribute, which is the valid form data you're usually after. +The original event will also be passed as a second parameter: ```jsx import validator from "@rjsf/validator-ajv6"; diff --git a/package-lock.json b/package-lock.json index 63f0720f67..a5d7327104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "react-jsonschema-form", - "version": "5.0.0", "hasInstallScript": true, "license": "Apache-2.0", "devDependencies": { diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 383001ce84..448d4c1a70 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -80,9 +80,10 @@ export interface FormProps { widgets?: RegistryWidgetsType; // Callbacks /** If you plan on being notified every time the form data are updated, you can pass an `onChange` handler, which will - * receive the same args as `onSubmit` any time a value is updated in the form + * receive the same args as `onSubmit` any time a value is updated in the form. Can also return the `id` of the field + * that caused the change */ - onChange?: (data: IChangeEvent) => void; + onChange?: (data: IChangeEvent, id?: string) => void; /** To react when submitted form data are invalid, pass an `onError` handler. It will be passed the list of * encountered errors */ @@ -503,8 +504,9 @@ export default class Form extends Component< * * @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 */ - onChange = (formData: T, newErrorSchema?: ErrorSchema) => { + onChange = (formData: T, newErrorSchema?: ErrorSchema, id?: string) => { const { extraErrors, omitExtraData, @@ -572,7 +574,7 @@ export default class Form extends Component< } this.setState( state as FormState, - () => onChange && onChange({ ...this.state, ...state }) + () => onChange && onChange({ ...this.state, ...state }, id) ); }; diff --git a/packages/core/src/components/fields/ArrayField.tsx b/packages/core/src/components/fields/ArrayField.tsx index 1014044f62..ba45cb45c9 100644 --- a/packages/core/src/components/fields/ArrayField.tsx +++ b/packages/core/src/components/fields/ArrayField.tsx @@ -344,7 +344,7 @@ class ArrayField extends Component< * @param index - The index of the item being changed */ onChangeForIndex = (index: number) => { - return (value: any, newErrorSchema?: ErrorSchema) => { + return (value: any, newErrorSchema?: ErrorSchema, id?: string) => { const { formData, onChange, errorSchema } = this.props; const arrayData = Array.isArray(formData) ? formData : []; const newFormData = arrayData.map((item: T, i: number) => { @@ -359,15 +359,16 @@ class ArrayField extends Component< errorSchema && { ...errorSchema, [index]: newErrorSchema, - } + }, + id ); }; }; /** Callback handler used to change the value for a checkbox */ onSelectChange = (value: any) => { - const { onChange } = this.props; - onChange(value); + const { onChange, idSchema } = this.props; + onChange(value, undefined, idSchema && idSchema.$id); }; /** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements diff --git a/packages/core/src/components/fields/MultiSchemaField.tsx b/packages/core/src/components/fields/MultiSchemaField.tsx index 02921b3f40..23d2bf3309 100644 --- a/packages/core/src/components/fields/MultiSchemaField.tsx +++ b/packages/core/src/components/fields/MultiSchemaField.tsx @@ -132,7 +132,12 @@ class AnyOfField extends Component< } // Call getDefaultFormState to make sure defaults are populated on change. onChange( - schemaUtils.getDefaultFormState(options[selectedOption], newFormData) as T + schemaUtils.getDefaultFormState( + options[selectedOption], + newFormData + ) as T, + undefined, + this.getFieldId() ); this.setState({ @@ -140,6 +145,13 @@ class AnyOfField extends Component< }); }; + getFieldId() { + const { idSchema, schema } = this.props; + return `${idSchema.$id}${ + schema.oneOf ? "__oneof_select" : "__anyof_select" + }`; + } + /** Renders the `AnyOfField` selector along with a `SchemaField` for the value of the `formData` */ render() { @@ -161,7 +173,6 @@ class AnyOfField extends Component< options, registry, uiSchema, - schema, } = this.props; const { widgets, fields } = registry; @@ -190,9 +201,7 @@ class AnyOfField extends Component<
extends Component< * @returns - The onPropertyChange callback for the `name` property */ onPropertyChange = (name: string, addedByAdditionalProperties = false) => { - return (value: T, newErrorSchema?: ErrorSchema) => { + return (value: T, newErrorSchema?: ErrorSchema, id?: string) => { const { formData, onChange, errorSchema } = this.props; if (value === undefined && addedByAdditionalProperties) { // Don't set value = undefined for fields added by @@ -81,7 +81,8 @@ class ObjectField extends Component< errorSchema && { ...errorSchema, [name]: newErrorSchema, - } + }, + id ); }; }; diff --git a/packages/core/src/components/fields/SchemaField.tsx b/packages/core/src/components/fields/SchemaField.tsx index fab6a2bcf7..d73b9b5e60 100644 --- a/packages/core/src/components/fields/SchemaField.tsx +++ b/packages/core/src/components/fields/SchemaField.tsx @@ -5,6 +5,7 @@ import { getUiOptions, getSchemaType, getTemplate, + ErrorSchema, FieldProps, FieldTemplateProps, IdSchema, @@ -132,16 +133,23 @@ function SchemaFieldRender(props: FieldProps) { uiOptions ); const schema = schemaUtils.retrieveSchema(_schema, formData); + const fieldId = _idSchema[ID_KEY]; const idSchema = mergeObjects( - schemaUtils.toIdSchema( - schema, - _idSchema.$id, - formData, - idPrefix, - idSeparator - ), + schemaUtils.toIdSchema(schema, fieldId, formData, idPrefix, idSeparator), _idSchema ) as IdSchema; + + /** Intermediary `onChange` handler for field components that will inject the `id` of the current field into the + * `onChange` chain if it is not already being provided from a deeper level in the hierarchy + */ + const handleFieldComponentChange = React.useCallback( + (formData: T, newErrorSchema?: ErrorSchema, id?: string) => { + const theId = id || fieldId; + return onChange(formData, newErrorSchema, theId); + }, + [fieldId, onChange] + ); + const FieldComponent = getFieldComponent( schema, uiOptions, @@ -180,6 +188,7 @@ function SchemaFieldRender(props: FieldProps) { const field = ( this.setState({ liveSettings: formData }); - onFormDataChange = ({ formData = "" }) => - this.setState({ formData, shareURL: null }); + onFormDataChange = ({ formData = "" }, id) => { + if (id) { + console.log("Field changed, id: ", id); + } + return this.setState({ formData, shareURL: null }); + }; onShare = () => { const { formData, schema, uiSchema, liveSettings, errorSchema, theme } = diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 392b8c4272..cf32782558 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -282,7 +282,7 @@ export interface FieldProps /** The tree of errors for this field and its children */ errorSchema?: ErrorSchema; /** The field change event handler; called with the updated form data and an optional `ErrorSchema` */ - onChange: (newFormData: T, es?: ErrorSchema) => any; + onChange: (newFormData: T, es?: ErrorSchema, id?: string) => any; /** The input blur event handler; call it with the field id and value */ onBlur: (id: string, value: any) => void; /** The input focus event handler; call it with the field id and value */ @@ -357,7 +357,7 @@ export type FieldTemplateProps = { /** The formData for this field */ formData: T; /** The value change event handler; Can be called with a new value to change the value for this field */ - onChange: (value: T) => void; + onChange: FieldProps["onChange"]; /** The key change event handler; Called when the key associated with a field is changed for an additionalProperty */ onKeyChange: (value: string) => () => void; /** The property drop/removal event handler; Called when a field is removed in an additionalProperty context */