diff --git a/CHANGELOG.md b/CHANGELOG.md index 246532e641..5b5092c213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,16 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/chakra-ui - Added support for `chakra-react-select` v4, fixing [#3152](https://github.com/rjsf-team/react-jsonschema-form/issues/3152). -## @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 = ( { target: { value: "" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: { 1: { __errors: ["should be integer"] } }, - errors: [ - { - message: "should be integer", - name: "type", - params: { type: "integer" }, - property: "[1]", - schemaPath: "#/items/type", - stack: "[1] should be integer", - }, - ], - formData: [1, null, 3], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: { 1: { __errors: ["should be integer"] } }, + errors: [ + { + message: "should be integer", + name: "type", + params: { type: "integer" }, + property: "[1]", + schemaPath: "#/items/type", + stack: "[1] should be integer", + }, + ], + formData: [1, null, 3], + }, + "root_1" + ); submitForm(node); sinon.assert.calledWithMatch(onError.lastCall, [ @@ -1181,9 +1185,13 @@ describe("ArrayField", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: ["foo", "bar"], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: ["foo", "bar"], + }, + "root" + ); }); it("should handle a blur event", () => { @@ -1318,9 +1326,13 @@ describe("ArrayField", () => { target: { checked: true }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: ["foo", "fuzz"], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: ["foo", "fuzz"], + }, + "root" + ); }); it("should fill field with data", () => { @@ -1460,12 +1472,16 @@ describe("ArrayField", () => { await new Promise(setImmediate); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: [ - "data:text/plain;name=file1.txt;base64,x=", - "data:text/plain;name=file2.txt;base64,x=", - ], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: [ + "data:text/plain;name=file1.txt;base64,x=", + "data:text/plain;name=file2.txt;base64,x=", + ], + }, + "root" + ); }); it("should fill field with data", () => { @@ -1721,9 +1737,13 @@ describe("ArrayField", () => { Simulate.change(strInput, { target: { value: "bar" } }); Simulate.change(numInput, { target: { value: "101" } }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: ["bar", 101], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: ["bar", 101], + }, + "root" + ); }); it("should generate additional fields and fill data", () => { @@ -1867,9 +1887,13 @@ describe("ArrayField", () => { expect(node.querySelectorAll(".field-string")).to.have.length.of(2); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: [1, 2, "foo", undefined], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: [1, 2, "foo", undefined], + }, + "root" + ); }); it("should retain existing row keys/ids when adding additional items", () => { @@ -1897,9 +1921,13 @@ describe("ArrayField", () => { Simulate.change(inputs[0], { target: { value: "bar" } }); Simulate.change(inputs[1], { target: { value: "baz" } }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: [1, 2, "bar", "baz"], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: [1, 2, "bar", "baz"], + }, + "root" + ); }); it("should remove array items when clicking remove buttons", () => { @@ -1909,17 +1937,25 @@ describe("ArrayField", () => { expect(node.querySelectorAll(".field-string")).to.have.length.of(1); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: [1, 2, "baz"], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: [1, 2, "baz"], + }, + "root" + ); dropBtns = node.querySelectorAll(".array-item-remove"); Simulate.click(dropBtns[0]); expect(node.querySelectorAll(".field-string")).to.be.empty; - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: [1, 2], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: [1, 2], + }, + "root" + ); }); }); }); @@ -1948,9 +1984,13 @@ describe("ArrayField", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: [1, 2], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: [1, 2], + }, + "root" + ); }); }); diff --git a/packages/core/test/BooleanField_test.js b/packages/core/test/BooleanField_test.js index c0dbca8854..ba52f22c75 100644 --- a/packages/core/test/BooleanField_test.js +++ b/packages/core/test/BooleanField_test.js @@ -267,7 +267,7 @@ describe("BooleanField", () => { Simulate.change(node.querySelector("input"), { target: { checked: true }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { formData: true }); + sinon.assert.calledWithMatch(onChange.lastCall, { formData: true }, "root"); }); it("should fill field with data", () => { @@ -664,6 +664,7 @@ describe("BooleanField", () => { }); expect($select.value).eql("true"); expect(spy.lastCall.args[0].formData).eql(true); + expect(spy.lastCall.args[1]).eql("root"); }); it("should render a string field with a label", () => { @@ -700,9 +701,13 @@ describe("BooleanField", () => { target: { value: "false" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: false, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: false, + }, + "root" + ); }); it("should render the widget with the expected id", () => { diff --git a/packages/core/test/Form_test.js b/packages/core/test/Form_test.js index d5b2874275..26dd4e6c47 100644 --- a/packages/core/test/Form_test.js +++ b/packages/core/test/Form_test.js @@ -1013,13 +1013,17 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "new" }, }); - sinon.assert.calledWithMatch(onChange, { - formData: { - foo: "new", + sinon.assert.calledWithMatch( + onChange, + { + formData: { + foo: "new", + }, + schema, + uiSchema, }, - schema, - uiSchema, - }); + "root_foo" + ); }); it("should call last provided change handler", async () => { const schema = { @@ -1391,9 +1395,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "yo" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: "yo", - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: "yo", + }, + "root" + ); }); it("object", () => { const { node, onChange } = createFormComponent({ @@ -1411,9 +1419,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "yo" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "yo" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "yo" }, + }, + "root_foo" + ); }); it("array of strings", () => { const schema = { @@ -1429,9 +1441,13 @@ describeRepeated("Form common", (createFormComponent) => { Simulate.change(node.querySelector("input[type=text]"), { target: { value: "yo" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: ["yo"], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: ["yo"], + }, + "root_0" + ); }); it("array of objects", () => { const schema = { @@ -1451,9 +1467,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "yo" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: [{ name: "yo" }], - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: [{ name: "yo" }], + }, + "root_0" + ); }); it("dependency with array of objects", () => { const schema = { @@ -1501,12 +1521,16 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "yo" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { - show: true, - participants: [{ name: "yo" }], + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + show: true, + participants: [{ name: "yo" }], + }, }, - }); + "root_participants_0_name" + ); }); }); @@ -1524,9 +1548,13 @@ describeRepeated("Form common", (createFormComponent) => { Simulate.change(node.querySelector("input[type=text]"), { target: { value: "short" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: {}, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: {}, + }, + "root" + ); }); it("should not denote an error in the field", () => { @@ -1589,11 +1617,15 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "short" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: { - __errors: ["should NOT be shorter than 8 characters"], + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: { + __errors: ["should NOT be shorter than 8 characters"], + }, }, - }); + "root" + ); }); it("should denote the new error in the field", () => { @@ -1625,9 +1657,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "short" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: {}, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: {}, + }, + "root" + ); }); }); @@ -2156,9 +2192,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "not a number" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: { field1: { __errors: ["should be number"] } }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: { field1: { __errors: ["should be number"] } }, + }, + "root" + ); }); it("should only show errors for properties in selected branch", () => { @@ -2172,16 +2212,20 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "not a number" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: { - field1: { - __errors: ["should be number"], - }, - field2: { - __errors: ["is a required property"], + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: { + field1: { + __errors: ["should be number"], + }, + field2: { + __errors: ["is a required property"], + }, }, }, - }); + "root_field1" + ); }); it("should not show any errors when branch is empty", () => { @@ -2195,9 +2239,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: 3 }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: {}, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: {}, + }, + "root_branch" + ); }); }); }); @@ -2234,9 +2282,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "baz" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { bar: "baz" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { bar: "baz" }, + }, + "root_bar" + ); }); it("should replace state when props change formData keys", () => { @@ -2262,9 +2314,13 @@ describeRepeated("Form common", (createFormComponent) => { target: { value: "baz" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "foo", baz: "baz" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "foo", baz: "baz" }, + }, + "root_baz" + ); }); }); @@ -3163,9 +3219,13 @@ describe("Form omitExtraData and liveOmit", () => { target: { value: "foobar" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "foobar", baz: "baz" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "foobar", baz: "baz" }, + }, + "root_foo" + ); }); it("should not omit data on change with omitExtraData=true and liveOmit=false", () => { @@ -3190,9 +3250,13 @@ describe("Form omitExtraData and liveOmit", () => { target: { value: "foobar" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "foobar", baz: "baz" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "foobar", baz: "baz" }, + }, + "root_foo" + ); }); it("should not omit data on change with omitExtraData=false and liveOmit=true", () => { @@ -3217,9 +3281,13 @@ describe("Form omitExtraData and liveOmit", () => { target: { value: "foobar" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "foobar", baz: "baz" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "foobar", baz: "baz" }, + }, + "root_foo" + ); }); it("should omit data on change with omitExtraData=true and liveOmit=true", () => { @@ -3244,9 +3312,13 @@ describe("Form omitExtraData and liveOmit", () => { target: { value: "foobar" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "foobar" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "foobar" }, + }, + "root_foo" + ); }); it("should not omit additionalProperties on change with omitExtraData=true and liveOmit=true", () => { @@ -3275,9 +3347,13 @@ describe("Form omitExtraData and liveOmit", () => { target: { value: "foobar" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "foobar", add: { prop: 123 } }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "foobar", add: { prop: 123 } }, + }, + "root_foo" + ); }); it("should rename formData key if key input is renamed in a nested object with omitExtraData=true and liveOmit=true", () => { @@ -3301,9 +3377,13 @@ describe("Form omitExtraData and liveOmit", () => { target: { value: "key1new" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { nested: { key1new: "value" } }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { nested: { key1new: "value" } }, + }, + "root_nested" + ); }); describe("Async errors", () => { diff --git a/packages/core/test/NumberField_test.js b/packages/core/test/NumberField_test.js index fdb2affb57..4433880c55 100644 --- a/packages/core/test/NumberField_test.js +++ b/packages/core/test/NumberField_test.js @@ -159,9 +159,13 @@ describe("NumberField", () => { target: { value: "2" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: 2, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: 2, + }, + "root" + ); }); it("should handle a blur event", () => { @@ -271,9 +275,13 @@ describe("NumberField", () => { target: { value: test.input }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: test.output, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: test.output, + }, + "root" + ); // "2." is not really a valid number in a input field of type number // so we need to use getAttribute("value") instead since .value outputs the empty string expect($input.getAttribute("value")).eql(test.input); @@ -295,9 +303,13 @@ describe("NumberField", () => { target: { value: ".00" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: 0, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: 0, + }, + "root" + ); expect($input.value).eql(".00"); }); @@ -430,6 +442,7 @@ describe("NumberField", () => { }); expect($select.value).eql("1"); expect(spy.lastCall.args[0].formData).eql(1); + expect(spy.lastCall.args[1]).eql("root"); }); it("should render a string field with a label", () => { @@ -469,7 +482,7 @@ describe("NumberField", () => { target: { value: "2" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { formData: 2 }); + sinon.assert.calledWithMatch(onChange.lastCall, { formData: 2 }, "root"); }); it("should fill field with data", () => { diff --git a/packages/core/test/ObjectField_test.js b/packages/core/test/ObjectField_test.js index 51a75ab5fd..dcad4be298 100644 --- a/packages/core/test/ObjectField_test.js +++ b/packages/core/test/ObjectField_test.js @@ -156,9 +156,13 @@ describe("ObjectField", () => { target: { value: "changed" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "changed" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "changed" }, + }, + "root_foo" + ); }); it("should handle object fields with blur events", () => { @@ -637,9 +641,13 @@ describe("ObjectField", () => { target: { value: "newFirst" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { newFirst: 1, first: undefined }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { newFirst: 1, first: undefined }, + }, + "root" + ); }); it("should retain and display user-input data if key-value pair has a title present in the schema when renaming key", () => { @@ -659,9 +667,13 @@ describe("ObjectField", () => { target: { value: "Renamed custom title" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { "Renamed custom title": 1 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { "Renamed custom title": 1 }, + }, + "root" + ); const keyInput = node.querySelector("#root_Renamed\\ custom\\ title-key"); expect(keyInput.value).eql("Renamed custom title"); @@ -704,9 +716,13 @@ describe("ObjectField", () => { target: { value: "newSecond" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { first: 1, newSecond: 2, third: 3 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { first: 1, newSecond: 2, third: 3 }, + }, + "root" + ); expect(Object.keys(onChange.lastCall.args[0].formData)).eql([ "first", @@ -730,9 +746,13 @@ describe("ObjectField", () => { target: { value: "second" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { second: 2, "second-1": 1 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { second: 2, "second-1": 1 }, + }, + "root" + ); }); it("uses a custom separator between the duplicate key name and the suffix", () => { @@ -753,9 +773,13 @@ describe("ObjectField", () => { target: { value: "second" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { second: 2, second_1: 1 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { second: 2, second_1: 1 }, + }, + "root" + ); }); it("should not attach suffix when input is only clicked", () => { diff --git a/packages/core/test/StringField_test.js b/packages/core/test/StringField_test.js index 48b6d48611..bef54684f6 100644 --- a/packages/core/test/StringField_test.js +++ b/packages/core/test/StringField_test.js @@ -144,9 +144,13 @@ describe("StringField", () => { }); }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: "yo", - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: "yo", + }, + "root" + ); }); it("should handle a blur event", () => { @@ -191,7 +195,11 @@ describe("StringField", () => { target: { value: "" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { formData: undefined }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { formData: undefined }, + "root" + ); }); it("should handle an empty string change event with custom ui:emptyValue", () => { @@ -205,9 +213,13 @@ describe("StringField", () => { target: { value: "" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: "default", - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: "default", + }, + "root" + ); }); it("should handle an empty string change event with defaults set", () => { @@ -222,9 +234,13 @@ describe("StringField", () => { target: { value: "" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: undefined, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: undefined, + }, + "root" + ); }); it("should fill field with data", () => { @@ -363,9 +379,13 @@ describe("StringField", () => { target: { value: "foo" }, }); }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: "foo", - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: "foo", + }, + "root" + ); }); it("should reflect undefined in change event if empty option selected", () => { @@ -380,9 +400,13 @@ describe("StringField", () => { target: { value: "" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: undefined, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: undefined, + }, + "root" + ); }); it("should reflect the change into the dom", () => { @@ -531,9 +555,13 @@ describe("StringField", () => { target: { value: "" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: undefined, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: undefined, + }, + "root" + ); }); it("should handle an empty string change event with custom ui:emptyValue", () => { @@ -550,9 +578,13 @@ describe("StringField", () => { target: { value: "" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: "default", - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: "default", + }, + "root" + ); }); it("should render a textarea field with rows", () => { diff --git a/packages/core/test/anyOf_test.js b/packages/core/test/anyOf_test.js index b1f2ca7ec7..499330523c 100644 --- a/packages/core/test/anyOf_test.js +++ b/packages/core/test/anyOf_test.js @@ -85,9 +85,13 @@ describe("anyOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "defaultbar" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "defaultbar" }, + }, + "root__anyof_select" + ); }); it("should assign a default value and set defaults on option change when using references", () => { @@ -124,9 +128,13 @@ describe("anyOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "defaultbar" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "defaultbar" }, + }, + "root__anyof_select" + ); }); it("should assign a default value and set defaults on option change with 'type': 'object' missing", () => { @@ -158,9 +166,13 @@ describe("anyOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "defaultbar" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "defaultbar" }, + }, + "root__anyof_select" + ); }); it("should render a custom widget", () => { @@ -252,9 +264,13 @@ describe("anyOf", () => { target: { value: "Lorem ipsum dolor sit amet" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "Lorem ipsum dolor sit amet" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "Lorem ipsum dolor sit amet" }, + }, + "root_foo" + ); }); it("should clear previous data when changing options", () => { @@ -285,16 +301,30 @@ describe("anyOf", () => { target: { value: "Lorem ipsum dolor sit amet" }, }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + buzz: "Lorem ipsum dolor sit amet", + }, + }, + "root_buzz" + ); + Simulate.change(node.querySelector("input#root_foo"), { target: { value: "Consectetur adipiscing elit" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { - buzz: "Lorem ipsum dolor sit amet", - foo: "Consectetur adipiscing elit", + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + buzz: "Lorem ipsum dolor sit amet", + foo: "Consectetur adipiscing elit", + }, }, - }); + "root_foo" + ); const $select = node.querySelector("select"); @@ -335,9 +365,13 @@ describe("anyOf", () => { target: { value: 12345 }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { userId: 12345 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { userId: 12345 }, + }, + "root_userId" + ); const $select = node.querySelector("select"); @@ -345,17 +379,25 @@ describe("anyOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { userId: undefined }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { userId: undefined }, + }, + "root_userId" + ); Simulate.change(node.querySelector("input#root_userId"), { target: { value: "Lorem ipsum dolor sit amet" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { userId: "Lorem ipsum dolor sit amet" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { userId: "Lorem ipsum dolor sit amet" }, + }, + "root_userId" + ); }); it("should support custom fields", () => { diff --git a/packages/core/test/oneOf_test.js b/packages/core/test/oneOf_test.js index 3669f3dd0a..236a8a5b1e 100644 --- a/packages/core/test/oneOf_test.js +++ b/packages/core/test/oneOf_test.js @@ -87,9 +87,13 @@ describe("oneOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "defaultbar" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "defaultbar" }, + }, + "root__oneof_select" + ); }); it("should assign a default value and set defaults on option change when using refs", () => { @@ -125,9 +129,13 @@ describe("oneOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "defaultbar" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "defaultbar" }, + }, + "root__oneof_select" + ); }); it("should assign a default value and set defaults on option change with 'type': 'object' missing", () => { @@ -159,9 +167,13 @@ describe("oneOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "defaultbar" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "defaultbar" }, + }, + "root__oneof_select" + ); }); it("should render a custom widget", () => { @@ -253,9 +265,13 @@ describe("oneOf", () => { target: { value: "Lorem ipsum dolor sit amet" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: "Lorem ipsum dolor sit amet" }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: "Lorem ipsum dolor sit amet" }, + }, + "root_foo" + ); }); it("should clear previous data when changing options", () => { @@ -286,16 +302,30 @@ describe("oneOf", () => { target: { value: "Lorem ipsum dolor sit amet" }, }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + buzz: "Lorem ipsum dolor sit amet", + }, + }, + "root_buzz" + ); + Simulate.change(node.querySelector("input#root_foo"), { target: { value: "Consectetur adipiscing elit" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { - buzz: "Lorem ipsum dolor sit amet", - foo: "Consectetur adipiscing elit", + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + buzz: "Lorem ipsum dolor sit amet", + foo: "Consectetur adipiscing elit", + }, }, - }); + "root_foo" + ); const $select = node.querySelector("select"); @@ -336,11 +366,15 @@ describe("oneOf", () => { target: { value: 12345 }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { - userId: 12345, + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + userId: 12345, + }, }, - }); + "root_userId" + ); const $select = node.querySelector("select"); @@ -348,20 +382,28 @@ describe("oneOf", () => { target: { value: $select.options[1].value }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { - userId: undefined, + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + userId: undefined, + }, }, - }); + "root_userId" + ); Simulate.change(node.querySelector("input#root_userId"), { target: { value: "Lorem ipsum dolor sit amet" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { - userId: "Lorem ipsum dolor sit amet", + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + userId: "Lorem ipsum dolor sit amet", + }, }, - }); + "root_userId" + ); }); it("should support custom fields", () => { @@ -614,9 +656,13 @@ describe("oneOf", () => { expect($select.value).eql("1"); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { lorem: undefined, ipsum: {} }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { lorem: undefined, ipsum: {} }, + }, + "root__oneof_select" + ); }); describe("Arrays", () => { diff --git a/packages/core/test/uiSchema_test.js b/packages/core/test/uiSchema_test.js index ab2d44876b..762a82a2dd 100644 --- a/packages/core/test/uiSchema_test.js +++ b/packages/core/test/uiSchema_test.js @@ -1160,9 +1160,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: 6.28 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: 6.28 }, + }, + "root_foo" + ); }); describe("Constraint attributes", () => { @@ -1243,9 +1247,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: 6.28 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: 6.28 }, + }, + "root_foo" + ); }); describe("Constraint attributes", () => { @@ -1336,9 +1344,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: 1.4142 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: 1.4142 }, + }, + "root_foo" + ); }); }); @@ -1435,9 +1447,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: 6 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: 6 }, + }, + "root_foo" + ); }); }); @@ -1481,9 +1497,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: 6 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: 6 }, + }, + "root_foo" + ); }); }); @@ -1537,9 +1557,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: 2 }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: 2 }, + }, + "root_foo" + ); }); }); @@ -1648,9 +1672,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: false }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: false }, + }, + "root_foo" + ); }); it("should call onChange handler when true is checked", () => { @@ -1668,9 +1696,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: true }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: true }, + }, + "root_foo" + ); }); }); @@ -1710,9 +1742,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: true }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: true }, + }, + "root_foo" + ); }); it("should call onChange handler when false is selected", () => { @@ -1731,9 +1767,13 @@ describe("uiSchema", () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: { foo: false }, - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { foo: false }, + }, + "root_foo" + ); }); }); diff --git a/packages/core/test/validate_test.js b/packages/core/test/validate_test.js index 4e43cb6ebb..69b7cfea6f 100644 --- a/packages/core/test/validate_test.js +++ b/packages/core/test/validate_test.js @@ -161,11 +161,15 @@ describe("Validation", () => { target: { value: "1234" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: { __errors: ["Invalid"] }, - errors: [{ property: ".", message: "Invalid", stack: ". Invalid" }], - formData: "1234", - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: { __errors: ["Invalid"] }, + errors: [{ property: ".", message: "Invalid", stack: ". Invalid" }], + formData: "1234", + }, + "root" + ); }); it("should submit form on valid data", () => { @@ -649,11 +653,15 @@ describe("Validation", () => { target: { value: "1234" }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - errorSchema: { __errors: ["Invalid"] }, - errors: [{ property: ".", message: "Invalid", stack: ". Invalid" }], - formData: "1234", - }); + sinon.assert.calledWithMatch( + onChange.lastCall, + { + errorSchema: { __errors: ["Invalid"] }, + errors: [{ property: ".", message: "Invalid", stack: ". Invalid" }], + formData: "1234", + }, + "root" + ); }); it("should submit form on valid data", () => { diff --git a/packages/playground/src/app.jsx b/packages/playground/src/app.jsx index 057cd40a1e..0d4234978f 100644 --- a/packages/playground/src/app.jsx +++ b/packages/playground/src/app.jsx @@ -396,8 +396,12 @@ class Playground extends Component { setLiveSettings = ({ formData }) => 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 */