Skip to content

Commit

Permalink
fix: omitExtraData on submit and on validateForm (#4228)
Browse files Browse the repository at this point in the history
* fix: omitExtraData on submit and on validateForm

* test: update tests for omitExtraData

* ci: update changelog

* Update CHANGELOG.md

Co-authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com>

* docs: update changelog

---------

Co-authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com>
  • Loading branch information
helen-m-lin and heath-freenome committed Jul 1, 2024
1 parent 3d851f4 commit b1cf91d
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 29 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ should change the heading of the (upcoming) version to include a major version b

- Fix disabled property of options in CheckboxesWidget and RadioWidget ([#4216](https://github.com/rjsf-team/react-jsonschema-form/pull/4216))

## @rjsf/core

- Fixed `omitExtraData` not working in `onSubmit` and `validateForm`; fixing [#4187](https://github.com/rjsf-team/react-jsonschema-form/issues/4187), [#4165](https://github.com/rjsf-team/react-jsonschema-form/issues/4165) and [#4109](https://github.com/rjsf-team/react-jsonschema-form/issues/4109)

# 5.18.5

## @rjsf/antd
Expand Down
57 changes: 38 additions & 19 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -567,9 +567,23 @@ export default class Form<
return getAllPaths(pathSchema);
};

/** Returns the `formData` after filtering to remove any extra data not in a form field
*
* @param formData - The data for the `Form`
* @returns The `formData` after omitting extra data
*/
omitExtraData = (formData?: T): T | undefined => {
const { schema, schemaUtils } = this.state;
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;
};

/** 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 filterer to remove any extra data not
* 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.
Expand All @@ -593,12 +607,7 @@ export default class Form<

let _retrievedSchema: S | undefined;
if (omitExtraData === true && liveOmit === true) {
_retrievedSchema = schemaUtils.retrieveSchema(schema, formData);
const pathSchema = schemaUtils.toPathSchema(_retrievedSchema, '', formData);

const fieldNames = this.getFieldNames(pathSchema, formData);

newFormData = this.getUsedFormData(formData, fieldNames);
newFormData = this.omitExtraData(formData);
state = {
formData: newFormData,
};
Expand Down Expand Up @@ -702,18 +711,12 @@ export default class Form<
event.persist();
const { omitExtraData, extraErrors, noValidate, onSubmit } = this.props;
let { formData: newFormData } = this.state;
const { schema, schemaUtils } = this.state;

if (omitExtraData === true) {
const retrievedSchema = schemaUtils.retrieveSchema(schema, newFormData);
const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', newFormData);

const fieldNames = this.getFieldNames(pathSchema, newFormData);

newFormData = this.getUsedFormData(newFormData, fieldNames);
newFormData = this.omitExtraData(newFormData);
}

if (noValidate || this.validateForm()) {
if (noValidate || this.validateFormWithFormData(newFormData)) {
// There are no errors generated through schema validation.
// Check for user provided errors and update state accordingly.
const errorSchema = extraErrors || {};
Expand Down Expand Up @@ -804,14 +807,15 @@ export default class Form<
}
}

/** Programmatically validate the form. If `onError` is provided, then it will be called with the list of errors the
* same way as would happen on form submission.
/** Validates the form using the given `formData`. For use on form submission or on programmatic validation.
* If `onError` is provided, then it will be called with the list of errors.
*
* @param formData - The form data to validate
* @returns - True if the form is valid, false otherwise.
*/
validateForm() {
validateFormWithFormData = (formData?: T): boolean => {
const { extraErrors, extraErrorsBlockSubmit, focusOnFirstError, onError } = this.props;
const { formData, errors: prevErrors } = this.state;
const { errors: prevErrors } = this.state;
const schemaValidation = this.validate(formData);
let errors = schemaValidation.errors;
let errorSchema = schemaValidation.errorSchema;
Expand Down Expand Up @@ -855,6 +859,21 @@ export default class Form<
});
}
return !hasError;
};

/** Programmatically validate the form. If `omitExtraData` is true, the `formData` will first be filtered to remove
* any extra data not in a form field. If `onError` is provided, then it will be called with the list of errors the
* same way as would happen on form submission.
*
* @returns - True if the form is valid, false otherwise.
*/
validateForm() {
const { omitExtraData } = this.props;
let { formData: newFormData } = this.state;
if (omitExtraData === true) {
newFormData = this.omitExtraData(newFormData);
}
return this.validateFormWithFormData(newFormData);
}

/** Renders the `Form` fields inside the <form> | `tagName` or `_internalFormWrapper`, rendering any errors if
Expand Down
123 changes: 113 additions & 10 deletions packages/core/test/Form.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3291,7 +3291,7 @@ describe('Form omitExtraData and liveOmit', () => {
sandbox.restore();
});

it('should call getUsedFormData when the omitExtraData prop is true and liveOmit is true', () => {
it('should call omitExtraData when the omitExtraData prop is true and liveOmit is true', () => {
const schema = {
type: 'object',
properties: {
Expand All @@ -3316,18 +3316,18 @@ describe('Form omitExtraData and liveOmit', () => {
liveOmit,
});

sandbox.stub(comp.ref.current, 'getUsedFormData').returns({
sandbox.stub(comp.ref.current, 'omitExtraData').returns({
foo: '',
});

fireEvent.change(node.querySelector('[type=text]'), {
target: { value: 'new' },
});

sinon.assert.calledOnce(comp.ref.current.getUsedFormData);
sinon.assert.calledOnce(comp.ref.current.omitExtraData);
});

it('should not call getUsedFormData when the omitExtraData prop is true and liveOmit is unspecified', () => {
it('should not call omitExtraData when the omitExtraData prop is true and liveOmit is unspecified', () => {
const schema = {
type: 'object',
properties: {
Expand All @@ -3349,19 +3349,19 @@ describe('Form omitExtraData and liveOmit', () => {
omitExtraData,
});

sandbox.stub(comp.ref.current, 'getUsedFormData').returns({
sandbox.stub(comp.ref.current, 'omitExtraData').returns({
foo: '',
});

fireEvent.change(node.querySelector('[type=text]'), {
target: { value: 'new' },
});

sinon.assert.notCalled(comp.ref.current.getUsedFormData);
sinon.assert.notCalled(comp.ref.current.omitExtraData);
});

describe('getUsedFormData', () => {
it('should call getUsedFormData when the omitExtraData prop is true', () => {
describe('omitExtraData on submit', () => {
it('should call omitExtraData when the omitExtraData prop is true', () => {
const schema = {
type: 'object',
properties: {
Expand All @@ -3385,14 +3385,65 @@ describe('Form omitExtraData and liveOmit', () => {
omitExtraData,
});

sandbox.stub(comp.ref.current, 'getUsedFormData').returns({
sandbox.stub(comp.ref.current, 'omitExtraData').returns({
foo: '',
});

fireEvent.submit(node);

sinon.assert.calledOnce(comp.ref.current.getUsedFormData);
sinon.assert.calledOnce(comp.ref.current.omitExtraData);
});

it('Should call validateFormWithFormData with the current formData if omitExtraData is false', () => {
const omitExtraData = false;
const schema = {
type: 'object',
properties: {
foo: { type: 'string' },
},
};
const formData = { foo: 'bar', baz: 'baz' };
const formRef = React.createRef();
const props = {
ref: formRef,
schema,
formData,
omitExtraData: omitExtraData,
};
const { comp, node } = createFormComponent(props);
sandbox.stub(comp.ref.current, 'validateFormWithFormData').returns({
foo: '',
});
fireEvent.submit(node);
sinon.assert.calledWithMatch(comp.ref.current.validateFormWithFormData, formData);
});

it('Should call validateFormWithFormData with a new formData with only used fields if omitExtraData is true', () => {
const omitExtraData = true;
const schema = {
type: 'object',
properties: {
foo: { type: 'string' },
},
};
const formData = { foo: 'bar', baz: 'baz' };
const formRef = React.createRef();
const props = {
ref: formRef,
schema,
formData,
omitExtraData: omitExtraData,
};
const { comp, node } = createFormComponent(props);
sandbox.stub(comp.ref.current, 'validateFormWithFormData').returns({
foo: '',
});
fireEvent.submit(node);
sinon.assert.calledWithMatch(comp.ref.current.validateFormWithFormData, { foo: 'bar' });
});
});

describe('getUsedFormData', () => {
it('should just return the single input form value', () => {
const schema = {
title: 'A single-field form',
Expand Down Expand Up @@ -4352,6 +4403,58 @@ describe('Form omitExtraData and liveOmit', () => {
});

describe('validateForm()', () => {
it('Should call validateFormWithFormData with the current formData if omitExtraData is false', () => {
const omitExtraData = false;
const schema = {
type: 'object',
properties: {
foo: { type: 'string' },
},
};
const formData = { foo: 'bar', baz: 'baz' };
const formRef = React.createRef();
const props = {
ref: formRef,
schema,
formData,
omitExtraData: omitExtraData,
};
const { comp } = createFormComponent(props);
sandbox.stub(comp.ref.current, 'validateFormWithFormData').returns({
foo: '',
});
act(() => {
comp.ref.current.validateForm();
});
sinon.assert.calledWithMatch(comp.ref.current.validateFormWithFormData, formData);
});

it('Should call validateFormWithFormData with a new formData with only used fields if omitExtraData is true', () => {
const omitExtraData = true;
const schema = {
type: 'object',
properties: {
foo: { type: 'string' },
},
};
const formData = { foo: 'bar', baz: 'baz' };
const formRef = React.createRef();
const props = {
ref: formRef,
schema,
formData,
omitExtraData: omitExtraData,
};
const { comp } = createFormComponent(props);
sandbox.stub(comp.ref.current, 'validateFormWithFormData').returns({
foo: '',
});
act(() => {
comp.ref.current.validateForm();
});
sinon.assert.calledWithMatch(comp.ref.current.validateFormWithFormData, { foo: 'bar' });
});

it('Should update state when data updated from invalid to valid', () => {
const ref = createRef();
const props = {
Expand Down

0 comments on commit b1cf91d

Please sign in to comment.