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

Field level validation dependent on other fields. #1737

Open
pumanitro opened this issue Aug 8, 2019 · 24 comments
Open

Field level validation dependent on other fields. #1737

pumanitro opened this issue Aug 8, 2019 · 24 comments

Comments

@pumanitro
Copy link

🚀 Feature request

Current Behavior

<Field validate={(fieldValue) => ... } />
For now field level validaiton funciton gives us only field value.

Desired Behavior

<Field validate={(fieldValue, formikBag) => ... } />
I want to have access to formikBag inside of field level validation.

Suggested Solution

Pass additional argument with formikBag to validate callback function.

Who does this impact? Who is this for?

I want to have possibility to access other formik values to create dependent validation. E.g. I have Field1 and Field2. Field2 should be invalid if Field1 is true and Field2 have some values inside. Form is reused and you can compound it from smallest reuseable pieces. It means sometimes you can have Field2 defined or undefined, that is why I don't want to use for this case global <Formik validation method.

Describe alternatives you've considered

  1. Global Formik validation method - no, bcs of -> I want to see validation on component Blur, and want to compound form from smallest Fields element that can be included or not dynamically.

  2. Field Field nesting just to have form values inside of second Field validation

<FIeld>
{
({form}) => <FIeld validate={value => form.values.FIELD1} > ...</Field>
}
</Field>

No. Just have a look at this. Unnecessary component just to retrieve values I should have access to.

@stale stale bot added the stale label Oct 7, 2019
@BlindDespair
Copy link

Having same issue here. We have range inputs, where numbers should not revert range (e.g. min value should not be more than max and vice versa). Lacking this feature forces us to do some complicated solutions like top level validation which is really hard to follow with a huge form. I believe you pass the whole form object as a second parameter into field validation function then I can decide whatever I want to use it for.

@stale stale bot removed the stale label Nov 28, 2019
@iwarshak
Copy link

iwarshak commented Dec 2, 2019

I agree. If the formik bag is available to a <Field>'s children, why would it not be available to validate as well?

@milesingrams
Copy link

This missing feature is causing a lot of issues for me as well, especially with related fields like password and re-enter password which need cross-validation. I would prefer to use field-level validation for this but it's impossible unless you can get access to up-to-date values. I tried to pass the current form object into my validation function but the values are one render cycle behind and therefore useless. Any update on making this possible?

@relwiwa
Copy link

relwiwa commented Dec 9, 2019

We are also finding it troublesome for implementing cross field validation. It would be great, when using the useField-Hook, FieldMetaProps would not only include the value "plucked out from values", but also the values themselves. Like that, cross-field validation would be easy to implement.

@gouthampai
Copy link

I would also like support for something like this. Form level validation would work if I was able to tell if fields had been touched or not.

@jaredpalmer
Copy link
Owner

Seems reasonable, can this be done with useEffect though?

@relwiwa
Copy link

relwiwa commented Jan 6, 2020

I am not sure why/where you would use useEffect. I thought it should be easy to not "pluck out from values" the one value of the respective field, but instead give all those values to the field, isn't it?

@jaredpalmer
Copy link
Owner

Yeah that’s fine by me

@relwiwa
Copy link

relwiwa commented Jan 6, 2020

Great! Are you saying that you will be working on it?

@mainfraame
Copy link

I’m running into the same issue with range fields. In my case, validation props like min/max are strings that refer to other values in the formik values object. So my range inputs are just components that wrap formik inputs.

I managed to create a workaround by using validation callback and using useFormikContext’s setFormikState to register the validation for each field on mounting.

When the validation callback is called I iterate through each yup validator and provide it the values object.

I tried to use the useField validate prop, but there wasn’t any point because I had to pass in the formik values object. I got that from useFormikContext. This resulted in duplicated rendering and terrible performance.

@tonypine
Copy link

tonypine commented Apr 4, 2020

I have this same issue. In my form I have a date field, and two other time fields in which their time must be after 'now', if the selected day is today. So I'm having trouble validating these time fields dynamically when the date is changed.

I tried something like @Menardi mentioned above, using the useField hook, and the useFormikContext to get the value of the date field, and use it inside the validate of the validate method. But the performance loss was huge, and for some reason I didn't got the data flow right, the validate method passed to the useField hook, appears to be being called before the updated date value, comes from the useFormiKContext hook :/

@stale stale bot removed the stale label Apr 4, 2020
@tonypine
Copy link

tonypine commented Apr 8, 2020

I successfully managed to get my validation to work with a little hack:

  const { validateField, values } = useFormikContext();
  const fieldValue1 = values?.fieldValue1;
  const fieldValue2 = values?.fieldValue2;

  const validate = useCallback(
    value => {
      // validation logic
    },
    [fieldValue1, fieldValue2]
  );
  ...
  const [field, meta, helpers] = useField({
    name: fieldName,
    validate
  });
  useEffect(() => {
    const timeoutId = setTimeout(() => validateField(fieldName), 50);
    return () => clearTimeout(timeoutId);
  }, [fieldName, validate, validateField]);

By the time the validate method of invoked by formik, the other field's values weren't propagated yet to the component updating the validate method, but with this useEffect I re-invoke the validation of the field once, and it did the trick.

Is it a hack? Yes. Does it work? Yes.

@MikelMNJ
Copy link

I managed to reference other fields with custom validation using a ref. Full example below -- don't let the length of the example throw you off, it's a clean and simple solution.

Relevant code: const fieldRef = useRef();, <Field>{({ form, field }) => <input ref={ref} {...field} />}</Field>, <Field validation={val => myValidationFn(val)} /> and myValidationFn()

TLDR: Any extra field I want to reference in my validation is wrapped within <Field></Field> and has a ref added to the child input. Ref value is called in a custom validation function via the validate prop: <Field validate={val => customFn(val)} /> -- yup validations are preserved and ran along side your custom validation function.

import React, { useRef } from 'react';
import { Formik, Form, Field } from 'formik';
import MyFeedbackComponent from '/path/to/MyFeedbackComponent';
import * as yup from 'yup';

const MyComponent = props => {
  const fieldRef = useRef();
  const defaults = {
    myReferenceField: null,
    myOtherField: null,
  };

  const schema = yup.object().shape({
    myReferenceField: yup.number().required('Reference field is required.'),
    myOtherField: yup.number().required('My other field is required.'),
  });

  const handleSubmit = (values, resetForm, setSubmitting) => {
    const { myReferenceField, myOtherField } = values;

    // Your submit payload, API call, form/submit reset etc. here...
  };

  const myValidationFn = otherFieldVal => {
    const limit = 100;
    const referenceVal = fieldRef.current.value;
    const errorMsg = `Total must be under ${limit}.`;

    if (referenceVal) {
      // Your custom condition and error.
      const underLimit = otherFieldVal * referenceVal < limit;
      return !underLimit && errorMsg;
    }

    return true;
  };

  return (
    <Formik 
      initialValues={defaults}
      enableReinitialize={true}
      validationSchema={schema}
      onSubmit={(values, {resetForm, setSubmitting}) => (
        handleSubmit(values, resetForm, setSubmitting)
      )}>
        {form => {
          return (
            <Form>
              <label>
                /* This field is written this way so we can attach a ref to it.
                *  That way the most recent state value for this field is 
                *  available in our custom validation function. */
                <Field type="number" name="myReferenceField">
                 {({form, field}) => <input ref={fieldRef} {...field} />}
                </Field>

                <MyFeedbackComponent name="myReferenceField" />
              </label>

              <label>
                // This field will call a custom validation function that will 
                // reference fieldRef's value.
                <Field 
                  type="number" 
                  name="myOtherField" 
                  validate={otherFieldVal => myValidationFn(otherFieldVal} />

                <MyFeedbackComponent name="myOtherField" />
              </label>

              <button type="submit" disabled={form.isSubmitting}>
                Submit
              </button>
            </Form>
          )};
        }
    </Formik>
  );
};

@stale stale bot added the stale label Jun 10, 2020
@zveljkovic
Copy link

Any news on this?

@danielwyb
Copy link

danielwyb commented Aug 4, 2020

I agree that the values should be passed as a second argument to the validate function. Here's a quick workaround in the meantime:

const {
  values,
  validateField
}: FormikProps<YourFormData> = useFormikContext();

useEffect(() => {
  validateField("amount");
}, [values.amountType, validateField]);

@samgoulden28
Copy link

samgoulden28 commented Oct 5, 2020

It also works to provide the values to your validation function at the time of render, i.e have your validation function derived from a returned function:

export const validateLaunchDateIsBeforePreLaunch = ( values ) => {
    return (launchDate) => {
        const preLaunchDate = values.preLaunch;
        const myIsBefore = (preLaunchDate && isBefore(new Date(launchDate), new Date(preLaunchDate)));
        if(myIsBefore) {
            return "Launch date is before pre launch date";
        }
    };
};

const { values } = useFormikContext();

return (
  <Field
    name="launchDate"
    validate={validateLaunchDateisBeforePreLaunch(values)}
    .../>
)}

@boroth
Copy link

boroth commented Mar 23, 2021

Not sure how to achieve this if you're using a class component instead of functional.

@johnrom
Copy link
Collaborator

johnrom commented Mar 23, 2021

@boroth you can use connect() from formik to connect your class-based child of Formik to state, then use componentDidUpdate which is basically the same "lifecycle hook" as useEffect.

class MyClassComponent {
  componentDidUpdate(props) {
    console.log('validate your thing here');
  }
}

const MyConnectedClassComponent = connect(MyClassComponent);

const MyForm = () => {
  return <Formik {...formikProps}>
    <MyConnectedClassComponent />
  </Formik>;
};

My plan, if TypeScript enables it, is eventually to support:

const MyDependentField = () => <Field 
  name="myField"
  include={state => ({ otherFieldValue: state.values.otherField })} 
  validate={(value, { otherFieldValue }) => value === otherFieldValue ? "Fields cannot be the same" : ""}
/>

However, that will be dependent on #1334 and #3089 because we need to resolve types before adding this functionality, as well as optimize subscriptions to Formik's API so this isn't super expensive.

@boroth
Copy link

boroth commented Mar 23, 2021

@johnrom, I could kiss you right now. Been jumping through hoops trying to get this to work and I had totally missed the connect() method in the documentation. I'm still only a few months into learning React, so it completely slipped by me. Thanks! I'll definitely need to spend more time learning about Redux and React lifecycle methods. I really appreciate it!

@johnrom
Copy link
Collaborator

johnrom commented Mar 23, 2021

Formik's connect is different from redux's connect, but it does basically the same thing specific to connecting to Formik's state.

@Pinheirovisky
Copy link

To fix it here, first I created a useState, which will store the variable that causes dependency. In my case, the card's flag is required to validate the card number.

const [brandName, setBrandName] = useState('')

Then, I transformed the onChange function into an asynchronous function. Thus, when the card brand changes, we execute the validateField of the card number in parallel. That way, it doesn't need a timeout or anything.

const handleOnChange = async (field: string, value: string) => {
    // The cardNumber field has a dependency of brand name. So, needed a another validation:
    if (field === 'paymentCompanyCode' && formik.values.cardNumber !== '') {
      setBrandName(value)
      await validateField('cardNumber')
    }
    setFieldValue(field, value);
    setFieldTouched(field, true, false);
    return validateForm({ ...formik.values, [field]: value });
  };

@Mati20041
Copy link

Mati20041 commented May 20, 2021

Recently I have moved to https://github.com/final-form/react-final-form because they support validate function with following signature:

( value: FieldValue,  allValues: object,  meta?: FieldState<FieldValue>) => any

allValues gives me access to the whole state without precedence/reference problems (which hacks like useEffect with validateField can solve, but it doesn't fit right with me ).

It would be great if @johnrom 's proposal would be accepted ( also in useField hook). That would bring me back to using formik (which I personally prefer due to better documentation and typing) :)

ErikWittern added a commit to ErikWittern/formik that referenced this issue Sep 16, 2021
@ctsstc
Copy link

ctsstc commented Nov 10, 2021

This seemed to work for me using Yup.ref, hopefully it's relevant?

<Formik
  initialValues={{ newPassword: '', confirmNewPassword: '' }}
  validationSchema={Yup.object().shape({
    newPassword: Yup.string()
      .required('New Password is required.')
      .min(8, 'New Password must be at least 8 characters long.')
      .matches(/\d/, 'New Password must contain at least 1 number.')
      .matches(
        /[A-Z]/,
        'New Password must contain at least 1 uppercase letter.',
      ),
    confirmNewPassword: Yup.string().oneOf(
      [Yup.ref('newPassword')],
      'Passwords must match',
    ),
  })}

@hassandewitt
Copy link

Any updates on this requested logic. On field level validation, having the ability to validate a field's value based on another field's value using the most up to date values object after the other field's value has been changed would be super helpful.

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

No branches or pull requests