jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.98k stars 2.79k forks source link

No error for custom yup validation using .test method #2146

Open apieceofbart opened 4 years ago

apieceofbart commented 4 years ago

🐛 Bug report

Current Behavior

Formik doesn't throw any error when my custom validation fails. You can see that yup validation works fine when triggered manually however formik doesn't show anything in errors object.

Expected behavior

Formik "errors" should be populated with the custom error.

Reproducible example

https://codesandbox.io/s/formik-example-using-when-test-of-yup-47ds6

Your environment

Software Version(s)
Formik 2.08
React 16.12
TypeScript none
Browser chrome
npm/Yarn
Operating System
apieceofbart commented 4 years ago

I found a workaround which is to create a fake field on the schema and use yups test method there. You can get access to other fields by using this.parent inside test callback. Here's an example: https://codesandbox.io/s/formik-example-using-when-test-of-yup-ni2yt

I'm not sure how can it be handled inside formik - it would probably require introducing another property (like customErrors) for this case.

pradej commented 4 years ago

Just happened to me: I need it to check for a field that is on the root of the object to validate a deeply nested field. While test() returns false on invalid input, no error is displayed :/

samuelpetroline commented 4 years ago

I ran into the same issue. I noticed that adding a test in the root object causes the error to not be thrown by Formik.

Tracking down the source, I noticed that this behaviour is caused by the error path being empty, as stated in this function: https://github.com/jaredpalmer/formik/blob/f117c04738ed218b5eb8916d7189e0849962d50d/packages/formik/src/Formik.tsx#L1061-L1073

According to yup, the ValidationError path is empty at root level:

path: a string, indicating where there error was thrown. path is empty at the root level.

At this point I'm not sure whether this is intended or not

DeszczMikolaj commented 4 years ago

Here the same, yup is working fine, it fills the error object after unsuccessful .test() validation but Formik doesn't display it underneath my customField. Someone solved that problem ?

apieceofbart commented 4 years ago

@DeszczMikolaj perhaps my workaround would help you: https://codesandbox.io/s/formik-example-using-when-test-of-yup-ni2yt

Faithfinder commented 4 years ago

I've also encountered this, and can add that this is a degradation from v1.x.x, worked fine there

atdrago commented 4 years ago

Apologies for the noise, but I don't want this to be marked stale. This is still an issue in 2.1.4

Alebron23 commented 4 years ago

Having this same issue. Any workarounds yet?

queltos commented 4 years ago

Like @samuelpetroline I've also noticed that the unset path is the problem. I've worked around this by setting a path in the custom test function by explicitly creating an error. Yup docs for custom test functions show how to do it: https://github.com/jquense/yup#mixedtestname-string-message-string--function-test-function-schema

My code now looks like this:

yup
.object()
.shape({
  field1: yup.string(),
  field2: yup.string(),
})
.test({
  name: "atLeastOneRequired",
  test: function(values) {
    const isValid = ["field1", "field2"].some(field => isNotEmpty(values[field]));

    if(isValid) return true;
    return this.createError({
      path: "field1 | field2",
      message: "One field must be set",
    })
  }
})

Important is to use a non arrow function for test: to have this set correctly. For path any string will do, even with path: "" Formik will be able to recognize it as a validation error.

p.s. in case you're wondering how to test for the path being set with jest, this is how:

expect(schema.validate(values)).rejects.toMatchObject({
  path: expect.stringMatching(/.*/),
});
lysenkoph-mms commented 2 years ago

Important is to use a non arrow function for test: to have this set correctly

Or just pass context as second argument via arrow function and call createError on that. This is more type-safe than an anonymous function

jahirfiquitiva commented 2 years ago

Using version 2.2.9 and also facing this issue 😩

My code looks like:

  referralCode: yup.string().test({
    name: 'is-valid',
    message: 'This is not a valid referral code',
    async test(value, ctx) {
      console.log(`Validating: ${value}`);
      if (!value) return true;
      const isValid = await verifyReferralCode(value);
      if (isValid) return true;
      console.error(`"${value}" is not a valid referral code`);
      return ctx.createError({
        message: `"${value}" is not a valid referral code`,
      });
    },
  }),

the code runs find, as the console statements run. but the form is not showing the error message 😕

Update: this custom function helped me resolve this https://github.com/jaredpalmer/formik/pull/2902#issuecomment-922492137

azashi commented 1 year ago

Using default value before chaining test helped me. yup.mixed().default({}).test(...)

kharithomas commented 1 year ago

In case you're in my position, your custom test is working but you didn't include a <ErrorMessage /> component in your form.

First, check to make sure your yup test is actually failing or not by sticking this line of code inside your Formik render:

<pre>{JSON.stringify(errors, null, 2)}</pre>

Like so:

<Formik
  // ...props
  >
    {({ values, errors }) => (
      <Form>
         <pre>{JSON.stringify(errors, null, 2)}</pre>
         // ... rest of input fields
      </Form>
    )}
  </Formik>

If your errors are appearing on the screen then you simply need to provide the correct path inside your error message component like this:

<ErrorMessage name="<name of your input field>" >

Hope this helps 👍🏾

zzzej commented 1 year ago

I was able to solve it using <ErrorMessage /> from Formik

import { Field, ErrorMessage } from 'formik';

return (
  <div className={is_scheduled ? 'block' : 'hidden'}>
    <label htmlFor="schedule_date">
     Date
    </label>
    <Field id="schedule_date" name="schedule_date" type="datetime-local" />
    <ErrorMessage name="schedule_date" />
  </div>
)

Here's the schema

const validationSchema = object({
  is_scheduled: boolean().required(),
  schedule_date: date().test(
    'future',
    'must be future date',
    (val, ctx) => {
      const selectedDate = dayjs(val);
      const now = dayjs(new Date());

      if (selectedDate.isAfter(now)) {
        return true;
      }

      return ctx.createError({
        message: 'must be future date.',
      });
    }
  ),
});

Hope this helps

jaure96 commented 1 year ago

What if i want to do this validation? the createError is not working whit this path general.

export const FormSchema = yup
  .object()
  .shape({
    source: yup.string().required(),
    featureType: yup.string().required(),
    file: yup.mixed().when('source', {
      is: 'file',
      then: (schema) => schema.required(),
    }),
    table: yup.object({
      data: yup.array().of(
        yup.object({
          timestamp: yup.date().required(),
          mmsi: yup.string(),
          ircs: yup.string(),
          extMarking: yup.string(),
          name: yup.string(),
          flag: yup.string(),
          type: yup
            .string()
            .oneOf(TypeSelectOptions.map((opt) => opt.value))
            .required(),
          latitude: yup.string(),
          longitude: yup.string(),
          heading: yup.string(),
        })
      ),
    }),
    externalPositions: yup.object({
      active: yup.boolean(),
    }),
  })
  .test({
    name: 'maxMmsi',
    test: function (form, { createError }) {
      if (
        form.externalPositions.active &&
        _.uniqBy(form.table.data, 'mmsi').length > 2
      )
        return createError({
          path: 'general',
          message: 'Importing is limited to a max of 10 distinct vesselss',
        })
      return true
    },
  })