Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: omitExtraData on submit and on validateForm #4228

Merged
merged 9 commits into from
Jul 1, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ should change the heading of the (upcoming) version to include a major version b
- Fix case where NumberField would not properly reset the field when using programmatic form reset (#4202)[https://github.com/rjsf-team/react-jsonschema-form/issues/4202]
- Updated widgets to handle undefined `target` in `onFocus` and `onBlur` handlers
- Fix field disable or readonly property can't cover globalOptions corresponding property (#4212)[https://github.com/rjsf-team/react-jsonschema-form/pull/4212]
- 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)

Copy link
Member

Choose a reason for hiding this comment

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

@helen-m-lin Since we released 5.18.5, can you move this change comment up to the 5.18.6

Copy link
Member

Choose a reason for hiding this comment

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

I tried rebasing your PR already and then I realized your comment needed to move. Plus another fix just merged so you'll have to rebase again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@heath-freenome I moved the changelog comment to 5.18.6. Let me know if there is anything else!

## @rfsf/fluent-ui

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 | undefined): T | undefined => {
helen-m-lin marked this conversation as resolved.
Show resolved Hide resolved
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 | undefined): boolean => {
helen-m-lin marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
Comment on lines +3363 to +3364
Copy link
Member

Choose a reason for hiding this comment

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

It may be useful to keep testing getUsedFormData() independently of omitExtraData()... @nickgros your thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this is just verifying that it's called when the prop is set. Method is unit tested below

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