formik
formik copied to clipboard
Optionally show multiple validation errors per field
This PR adds a new prop called validationSchemaOptions
to add the feature described here:
https://github.com/jaredpalmer/formik/issues/243
Example usage:
<Formik
validationSchema={validationSchema}
validationSchemaOptions={{ showMultipleFieldErrors: true }}
...
showMultipleFieldErrors
is false by default, to maintain consistency with the current behavior of returning just a single string per field.
Do we really need an additional configuration prop for this? I believe you can set (or resolve errors while validating) like this.
<Formik
validate={values => doYourYupValidationMagicAndReturnErrors(values)}
/>
@andreyco Yes, that works fine for validate
callbacks. I should have mentioned that this PR is specifically about validationSchema
using Yup. The internal logic of formik currently only returns one error at a time (per field) when using Yup.
@mbrowne were you ever able to merge this in?
thanks, another user hoping for the same functionality
Hello! @jaredpalmer I absolutely LOVE this library!
@mbrowne Thank you for making this pull request!
Is this, or something similar to this functionality arriving in the library soon? I would love to have the support and ability to provide context for multiple different errors at a time without having to re-declare everything in the validate
function!
Thank you for all the hard work! Please keep it up!
:+1:
in the meantime, here's a userland solution to make this work today, based on the code of this PR: https://codesandbox.io/s/formik-example-forked-wxzl1?file=/index.js
// Helper styles for demo
import "./helper.css";
import React from "react";
import { render } from "react-dom";
import { Formik, validateYupSchema, setIn, getIn } from "formik";
import * as Yup from "yup";
// Copied from PR: https://github.com/formium/formik/pull/1573
/**
* Transform Yup ValidationError to a more usable object
*/
export function yupToFormErrors(yupError, validationSchemaOptions) {
let errors = {};
if (yupError.inner.length === 0) {
return setIn(errors, yupError.path, yupError.message);
}
// if showMultipleFieldErrors is enabled, set the error value
// to an array of all errors for that field
if (validationSchemaOptions.showMultipleFieldErrors) {
for (let err of yupError.inner) {
let fieldErrors = getIn(errors, err.path);
if (!fieldErrors) {
fieldErrors = [];
}
fieldErrors.push(err.message);
errors = setIn(errors, err.path, fieldErrors);
}
} else {
for (let err of yupError.inner) {
if (!errors[err.path]) {
errors = setIn(errors, err.path, err.message);
}
}
}
return errors;
}
const validateYupSchemaMultiErrors = async (values, schema) => {
try {
await validateYupSchema(values, schema);
return {};
} catch (e) {
return yupToFormErrors(e, { showMultipleFieldErrors: true });
}
};
const PasswordSchema = Yup.object().shape({
password: Yup.string()
.matches(/[a-z]/, "company.users.edit.form.errors.lowercase")
.matches(/[\d]{1}/, "company.users.edit.form.errors.digit")
.matches(/[A-Z]/, "company.users.edit.form.errors.uppercase")
.min(8, "company.users.edit.form.errors.min"),
});
const App = () => (
<div className="app">
<h1>
Basic{" "}
<a
href="https://github.com/jaredpalmer/formik"
target="_blank"
rel="noopener noreferrer"
>
Formik
</a>{" "}
Demo
</h1>
<Formik
initialValues={{ password: "" }}
onSubmit={async (values) => {
await new Promise((resolve) => setTimeout(resolve, 500));
alert(JSON.stringify(values, null, 2));
}}
validate={(values) =>
validateYupSchemaMultiErrors(values, PasswordSchema)
}
>
{(props) => {
const {
values,
touched,
errors,
handleChange,
handleBlur,
handleSubmit,
} = props;
console.log(props);
return (
<form onSubmit={handleSubmit}>
<input
id="password"
placeholder="Enter your password"
type="text"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
className={
errors.password && touched.password
? "text-input error"
: "text-input"
}
/>
{errors.password && touched.password && (
<div style={{ color: "red" }}>
{errors.password instanceof Array
? errors.password.map((error) => <div>{error}</div>)
: errors.password}
</div>
)}
</form>
);
}}
</Formik>
</div>
);
render(<App />, document.getElementById("root"));
@slorber 's code works but here is my updated version based on my specifications:
import { getIn, setIn } from "formik"; // remove validateYupSchema
import { AnySchema } from "yup";
/**
* Transform Yup ValidationError to a more usable object
*/
function yupToFormErrors(yupError: any, validationSchemaOptions: any) {
let errors: any = {};
if (yupError.inner.length === 0) {
return setIn(errors, yupError.path, yupError.message);
}
// if showMultipleFieldErrors is enabled, set the error value
// to an array of all errors for that field
if (validationSchemaOptions.showMultipleFieldErrors) {
for (let err of yupError.inner) {
let fieldErrors = getIn(errors, err.path);
if (!fieldErrors) {
fieldErrors = [];
}
// Push to array if not yet added
if (!fieldErrors.includes(err.message)) {
fieldErrors.push(err.message);
}
errors = setIn(errors, err.path, fieldErrors);
}
} else {
for (let err of yupError.inner) {
if (!errors[err.path]) {
errors = setIn(errors, err.path, err.message);
}
}
}
return errors;
}
export const ValidateYupSchemaArrErrors = async (
values: any,
schema: AnySchema
) => {
try {
// change to .validate method so that we can add abortEarly
await schema.validate(values, {
abortEarly: false,
});
return {};
} catch (e) {
return yupToFormErrors(e, { showMultipleFieldErrors: true });
}
};
This will solve the following:
- in the same field different rule but the same message, it will merge as 1 rule below will validate email + custom email validation
.email("Please enter valid email")
.matches(emailRule, { message: "Please enter valid email" })
- on the original code, when you submit the form as blank, only required rule will be validated, meaning if field has value (which passes required rule) but fails on other rules, it will still pass