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

Optionally show multiple validation errors per field #1573

Open
wants to merge 1 commit into
base: next
Choose a base branch
from

Conversation

mbrowne
Copy link

@mbrowne mbrowne commented May 31, 2019

This PR adds a new prop called validationSchemaOptions to add the feature described here:
#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.

@Andreyco
Copy link
Collaborator

Andreyco commented Jun 2, 2019

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)}
/>

@mbrowne
Copy link
Author

mbrowne commented Jun 2, 2019

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

@stale stale bot added the stale label Aug 1, 2019
@malerba423
Copy link

@mbrowne were you ever able to merge this in?

thanks,
another user hoping for the same functionality

@stale stale bot removed the stale label Jan 10, 2020
@loganknecht
Copy link

loganknecht commented Feb 19, 2020

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!

@stale stale bot added the stale label Apr 19, 2020
@T04435
Copy link

T04435 commented Sep 22, 2020

👍

@slorber
Copy link
Collaborator

slorber commented Nov 6, 2020

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"));

@kpebron
Copy link

kpebron commented Feb 18, 2024

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants