jaredpalmer / formik

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

Field level validation dependent on other fields. #1737

Open pumanitro opened 5 years ago

pumanitro commented 5 years ago

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

BlindDespair commented 4 years ago

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.

iwarshak commented 4 years ago

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

milesingrams commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

Seems reasonable, can this be done with useEffect though?

relwiwa commented 4 years ago

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 commented 4 years ago

Yeah that’s fine by me

relwiwa commented 4 years ago

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

mainfraame commented 4 years ago

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 commented 4 years ago

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

tonypine commented 4 years ago

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 commented 4 years ago

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>
  );
};
zveljkovic commented 4 years ago

Any news on this?

danielwyb commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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

johnrom commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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

Pinheirovisky commented 3 years ago

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 commented 3 years ago

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

ctsstc commented 3 years ago

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 commented 2 years ago

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.