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: Add programmatic form validation support #3122

Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/release-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: main
- name: Setup Python
uses: actions/setup-python@v3
with:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ should change the heading of the (upcoming) version to include a major version b

## @rjsf/core
- Updated the `FieldErrorTemplate` to remove the explicit typing of the `error` to string to support the two options
- Implemented programmatic validation via new `validateForm()` method on `Form`, fixing (https://github.com/rjsf-team/react-jsonschema-form/issues/2755, https://github.com/rjsf-team/react-jsonschema-form/issues/2552, https://github.com/rjsf-team/react-jsonschema-form/issues/2381, https://github.com/rjsf-team/react-jsonschema-form/issues/2343, https://github.com/rjsf-team/react-jsonschema-form/issues/1006, https://github.com/rjsf-team/react-jsonschema-form/issues/246)

## @rjsf/semantic-ui
- Updated the `FieldErrorTemplate` to use the `children` variation of the `List.Item` that supports ReactElement
Expand All @@ -31,6 +32,8 @@ should change the heading of the (upcoming) version to include a major version b

## Dev / docs / playground
- Updated the `custom-templates.md` file to add the missing asterisk to the new `FieldErrorTemplate` and `FieldHelpTemplate`
- Updated the playground to add a new button for programmatically validating a form
- Also updated the `validation.md` documentation to describe how to programmatically validate a form

# 5.0.0-beta.8

Expand Down
27 changes: 27 additions & 0 deletions docs/usage/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,33 @@ render((
), document.getElementById("app"));
```

## Validate form programmatically

It is possible to programmatically validate a form using the `validateForm()` function on `Form`.
Add a `ref` to your `Form` component and call the `validateForm()` method to validate the form programmatically.
The `validateForm()` method returns true if the form is valid, false otherwise.
If you have provided an `onError` callback it will be called with the list of errors when the `validatorForm()` method returns false.

```jsx
import { createRef } from "react"
import validator from "@rjsf/validator-ajv6";

const formRef = createRef();
const onError = (errors) => alert(errors);

const schema = {
type: "string"
};

render((
<Form schema={schema} validator={validator} onError={onError} ref={formRef}/>
), document.getElementById("app"));

if (formRef.current.validateForm()) {
alert("Form is valid");
}
```

## HTML5 Validation

By default, the form uses HTML5 validation. This may cause unintuitive results because the HTML5 validation errors (such as when a field is `required`) may be displayed before the form is submitted, and thus these errors will display differently from the react-jsonschema-form validation errors. You can turn off HTML validation by setting the `noHtml5Validate` to `true`.
Expand Down
124 changes: 67 additions & 57 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -613,8 +613,7 @@ export default class Form<T = any, F = any> extends Component<
}

event.persist();
const { omitExtraData, extraErrors, noValidate, onSubmit, onError } =
this.props;
const { omitExtraData, extraErrors, noValidate, onSubmit } = this.props;
let { formData: newFormData } = this.state;
const { schema, schemaUtils } = this.state;

Expand All @@ -631,63 +630,31 @@ export default class Form<T = any, F = any> extends Component<
newFormData = this.getUsedFormData(newFormData, fieldNames);
}

if (!noValidate) {
const schemaValidation = this.validate(schemaUtils, newFormData);
let errors = schemaValidation.errors;
let errorSchema = schemaValidation.errorSchema;
const schemaValidationErrors = errors;
const schemaValidationErrorSchema = errorSchema;
if (errors.length > 0) {
if (extraErrors) {
const merged = schemaUtils.mergeValidationData(
schemaValidation,
extraErrors
);
errorSchema = merged.errorSchema;
errors = merged.errors;
}
this.setState(
{
errors,
errorSchema,
schemaValidationErrors,
schemaValidationErrorSchema,
},
() => {
if (onError) {
onError(errors);
} else {
console.error("Form validation failed", errors);
}
if (noValidate || this.validateForm()) {
// There are no errors generated through schema validation.
// Check for user provided errors and update state accordingly.
const errorSchema = extraErrors || {};
const errors = extraErrors
? schemaUtils.getValidator().toErrorList(extraErrors)
: [];
this.setState(
{
formData: newFormData,
errors,
errorSchema,
schemaValidationErrors: [],
schemaValidationErrorSchema: {},
},
() => {
if (onSubmit) {
onSubmit(
{ ...this.state, formData: newFormData, status: "submitted" },
event
);
}
);
return;
}
}

// There are no errors generated through schema validation.
// Check for user provided errors and update state accordingly.
const errorSchema = extraErrors || {};
const errors = extraErrors
? schemaUtils.getValidator().toErrorList(extraErrors)
: [];
this.setState(
{
formData: newFormData,
errors,
errorSchema,
schemaValidationErrors: [],
schemaValidationErrorSchema: {},
},
() => {
if (onSubmit) {
onSubmit(
{ ...this.state, formData: newFormData, status: "submitted" },
event
);
}
}
);
);
}
};

/** Returns the registry for the form */
Expand Down Expand Up @@ -723,6 +690,49 @@ export default class Form<T = any, F = any> extends Component<
}
}

/** 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.
*
* @returns - True if the form is valid, false otherwise.
*/
validateForm() {
const { extraErrors, onError } = this.props;
const { formData } = this.state;
const { schemaUtils } = this.state;
const schemaValidation = this.validate(schemaUtils, formData);
let errors = schemaValidation.errors;
let errorSchema = schemaValidation.errorSchema;
const schemaValidationErrors = errors;
const schemaValidationErrorSchema = errorSchema;
if (errors.length > 0) {
if (extraErrors) {
const merged = schemaUtils.mergeValidationData(
schemaValidation,
extraErrors
);
errorSchema = merged.errorSchema;
errors = merged.errors;
}
this.setState(
{
errors,
errorSchema,
schemaValidationErrors,
schemaValidationErrorSchema,
},
() => {
if (onError) {
onError(errors);
} else {
console.error("Form validation failed", errors);
}
}
);
return false;
}
return true;
}

/** Renders the `Form` fields inside the <form> | `tagName` or `_internalFormWrapper`, rendering any errors if
* needed along with the submit button or any children of the form.
*/
Expand Down
19 changes: 16 additions & 3 deletions packages/playground/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,16 +487,28 @@ class Playground extends Component {
validator={validator}
select={this.onValidatorSelected}
/>
<CopyLink shareURL={this.state.shareURL} onShare={this.onShare} />
<span> </span>
<button
title="Click me to submit the form programmatically."
className="btn btn-default"
type="button"
onClick={() => this.playGroundForm.current.submit()}
>
Programmatic Submit
Prog. Submit
</button>
<span> </span>
<button
title="Click me to validate the form programmatically."
className="btn btn-default"
type="button"
onClick={() => {
const valid = this.playGroundForm.current.validateForm();
alert(valid ? "Form is valid" : "Form has errors");
}}
>
Prog. Validate
</button>
<div style={{ marginTop: "5px" }} />
<CopyLink shareURL={this.state.shareURL} onShare={this.onShare} />
</div>
</div>
</div>
Expand Down Expand Up @@ -578,6 +590,7 @@ class Playground extends Component {
onSubmit={({ formData }, e) => {
console.log("submitted formData", formData);
console.log("submit event", e);
window.alert("Form submitted");
}}
fields={{ geo: GeoPosition }}
customValidate={validate}
Expand Down