formik icon indicating copy to clipboard operation
formik copied to clipboard

Optionally show multiple validation errors per field

Open mbrowne opened this issue 5 years ago • 7 comments

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.

mbrowne avatar May 31 '19 21:05 mbrowne

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 avatar Jun 02 '19 10:06 Andreyco

@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 avatar Jun 02 '19 10:06 mbrowne

@mbrowne were you ever able to merge this in?

thanks, another user hoping for the same functionality

malerba423 avatar Jan 10 '20 17:01 malerba423

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!

loganknecht avatar Feb 19 '20 08:02 loganknecht

:+1:

T04435 avatar Sep 22 '20 06:09 T04435

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 avatar Nov 06 '20 14:11 slorber

@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:

  1. 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" })
  1. 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

kpebron avatar Feb 18 '24 04:02 kpebron