jaredpalmer / formik

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

Validation on each step of the Wizard Flow #867

Closed danielwong2268 closed 5 years ago

danielwong2268 commented 6 years ago

In the wizard example, I expected there to be validation on each step. As in, if you don't complete the form you can't continue to the next piece of the form.

What would be the idiomatic Formik way to do this? Can we add this to the examples?

Also the documentation (readme) doesn't mention Wizard. It would be nice to add.

kevinb1003 commented 6 years ago

Export the schemas as an array, and then just access the right one using page index: validationSchema={validationSchemas[page]}

luandevpro commented 6 years ago

Snowier100 , you please said a bit was not?I really problem with it , please for me see a example for me understand it ..

luandevpro commented 6 years ago

122 ..this problem is only resolve multi form problem ... if you don't complete the form you can't continue to the next piece of the form. ...

download ,,,, in Formik I specify error occurred while submit

longnt80 commented 6 years ago

@luandevpro I'm using the Multistep example and it won't let me go to the next step if there is error from validation.

Huanzhang89 commented 6 years ago

I think that is the intended behaviour? You wouldn't want the user to proceed to the next page if there are validation errors.

Davidmycodeguy commented 6 years ago

Export the schemas as an array, and then just access the right one using page index: validationSchema={validationSchemas[page]}

This my attempt at that. https://codesandbox.io/s/k5m5po380v

waweru-kamau commented 5 years ago

Export the schemas as an array, and then just access the right one using page index: validationSchema={validationSchemas[page]}

This my attempt at that. https://codesandbox.io/s/k5m5po380v

I have a similar implementation in react-native, and I've noticed that also in your code, that when one goes back to a previous page, you need to click twice on next button to go to the next page. How can this be fixed?

eduardocurva commented 5 years ago

It would be nice if we had an official example of how to use Wizard Flow with Formik. I've seen different approaches and cool ideas but, as I'm new with Formik, I can't tell what would be the best recommended approach for things like:

Sorry if this comment is out of place.

alaskaa commented 5 years ago

Export the schemas as an array, and then just access the right one using page index: validationSchema={validationSchemas[page]}

This my attempt at that. https://codesandbox.io/s/k5m5po380v

Thanks for that! Helped me a great deal πŸ‘

sunviwo commented 5 years ago

Export the schemas as an array, and then just access the right one using page index: validationSchema={validationSchemas[page]}

This my attempt at that. https://codesandbox.io/s/k5m5po380v

Hi, @Davidmycodeguy , good snippet, but how do you submit the form at the final step? Seems the handleSubmit function is called at every single step.

kylemh commented 5 years ago

@sunviwo just conditionally fire the real submit handler if it's the last page

sibelius commented 5 years ago

what is the proper solution for this?

Andreyco commented 5 years ago

@sibelius setting class member (submit type with e.g. "partial validation" or "full validation"), submit form and do validation based on class member value.

kylemh commented 5 years ago

If it helps anybody, I have an opinionated implementation here: https://github.com/OperationCode/front-end/blob/master/components/Form/MultiStepForm.js

It demands a certain shape of every "step", but custom prop-types make it a protected foot-gun πŸ˜…

sibelius commented 5 years ago

My approach:

Define each step component, their step label and the validationSchema for the step

export const MyStepOne = () => {};

MyStepOne.label = 'stepOne';
MyStepOne.validationSchema = yup.object().shape({})
const steps = [
  MyStepOne,
   ...,
   MyStepTen,
];

const MultiStepForm = () => {
   const [step, setStep] = useState<number>(0);

   const isLastStep = () => {
    return step === steps.length - 1;
  };

  const handleNext = () => {
    setStep(step + 1);
  };

  const handleBack = () => {
    if (step !== 0) {
      setStep(step - 1);
    }
  };

  const getNavigationButtons = (back: boolean, formikBag: FormikProps<Values>) => {
    const { isValid, isSubmitting, handleSubmit } = formikBag;

    const isDisabled = !isValid || isSubmitting;

    return (
      <StyledNavBtn>
        {back ? <Button onClick={handleBack}>Go back</Button> : <div></div>}
        <Button variant='contained' color='primary' onClick={handleSubmit} disabled={isDisabled}>
                Next
      </Button>
      </StyledNavBtn>
    );
  };

  const handleSubmit = (values: Values, formikBag: FormikProps<Values>) => {
    const { setSubmitting } = formikBag;

    if (!isLastStep()) {
      setSubmitting(false);
      handleNext();
      return;
    }

    // TODO - handle final submit
    setSubmitting(false);
  };

  const CurrentStep = steps[step];
  const { validationSchema }  = CurrentStep;

   return (
      <Formik
        initialValues={{}}
        validationSchema={validationSchema}
        onSubmit={handleSubmit}
        render={(formikBag) => {
          return (
            <StyledContent>
              <CurrentStep />
              {getNavigationButtons(step > 0, formikBag)}
            </StyledContent>
          );
        }}
      />
    );
}
sunviwo commented 5 years ago

@sunviwo just conditionally fire the real submit handler if it's the last page

Thanks @kylemh. Hush, I tweaked the code to make it work in React Native and it is working fine except... "conditional submission". I could do this using a state value but I realize state is not accessible from handleSubmit function. Would you have a suggestion please ?

kylemh commented 5 years ago

I don't use React Native, but there's nothing to stop you from access state in a class method like handleSubmit

handleSubmit = (event) => {
  const { state } = this;
  // stuff
};
sunviwo commented 5 years ago

Worked. Thanks :)

arash87 commented 5 years ago

@sibelius thank you for the example you provided. But am i right that validateForm() from formikBag still must be called in order to get a proper isValid value on each step. Otherwise isValid is true as soon as one has passed the first step.

Would handleSubmit() be a proper place to add this?

    const handleSubmit = (values, formikBag) => {
        const {setSubmitting, validateForm} = formikBag

        if (!isLastStep()){
            setSubmitting(false)
            handleNext()
            validateForm() //Run validation manually on each step
            return
        }
        // Do submit
        sleep(300).then(() => {
            window.alert(JSON.stringify(values, null, 2))
            formikBag.setSubmitting(false)
        })
    }
kylemh commented 5 years ago

No because it should only be validating against the current visible step.

arash87 commented 5 years ago

@kylemh the issue that I have here is that once the first step has passed validation, than the Next button that is generated for the next step is enabled, because isValid is true (!!). Not exactly sure why Formik returns isValid = true on the 2. step, even though the form(step) clearly isn't valid, and also the fact that all the fields are set to touched after the first step is done. So the initial render of the Next button is enabled and validation only kicks in once that button is clicked, and then it gets disabled. I'm looking for a solution where validation (naturally) would kick in on initial render of every step, whether user is moving to the next or previous step.

validateForm() clearly would not help in this case, as errors will be added even before the user has had the chance to modifications.

kylemh commented 5 years ago

As a quick aside, I've always been recommended to never disable form submit buttons (although I do this if there's an HTTP request in progress from an existing submit): https://ux.stackexchange.com/a/76306

Regardless, you can read this and see when validation is run on the entire form: https://jaredpalmer.com/formik/docs/guides/validation#when-does-validation-run

If you'd like, you can use form.validateForm() on each step render to prevent a prime user experience.

ajmueller commented 5 years ago

For anyone using the method proposed above by @sibelius (which is brilliantly simple!), make sure you set your initialValues for your fields if you're using Material UI and more specifically formik-material-ui. The touched property of each field wasn't getting set and errors weren't displaying until we set initialValues.

arianitu commented 5 years ago

@arash87 @kylemh I'm having the same issue. After the first step, the form Next button is valid and we disable the button on an invalid form.

Did you figure out where to run validation or reset the isValid state?

arianitu commented 5 years ago

Looks like using validateForm() in handleSubmit seems to work fine. Anyone have issues with it?

Bulletninja commented 4 years ago

No because it should only be validating against the current visible step.

I thought this was the point? πŸ€”

Bulletninja commented 4 years ago

@sibelius's approach looks great, but it doesn't run any validations for me :/

kylemh commented 4 years ago

@arianitu @Bulletninja

Nothing is wrong with Sibelius' approach. It works for me with and validation work fine too. Ensure y'all have initialValues set 🀷

Maybe this will help? It's not very abstracted, but still:

https://github.com/OperationCode/front-end/blob/master/components/Form/MultiStepForm.js

Bulletninja commented 4 years ago

Yeah, you're probably right. I got started with this huge form using this as a reference https://dev.to/nicholasperetti/the-concept-of-subforms-with-react-and-formik-55mh and was trying to make Sibelius' approach fit with it. But had no luck. Thanks for that example πŸ‘πŸ»

nirmaljoshi4 commented 4 years ago

Export the schemas as an array, and then just access the right one using page index: validationSchema={validationSchemas[page]}

This my attempt at that. https://codesandbox.io/s/k5m5po380v

Thanks for that! Helped me a great deal πŸ‘

When moving to Previous Step again error is not being updated according to the schema form

kevin26exe commented 4 years ago

My implementation, not really good but I hope it helps somebody got the idea from the comments above render() {

    const schemaArray = [
        SchemaPersonalInfo, 
        SchemaAddress, 
        SchemaEducation, 
        SchemaEmploymentDetails, 
        SchemaAdditionalInfo ];

    const formData = FormData;

    let isSaving = this.state.isSaving;
    let isSaved = this.state.showSuccessMessage;
    let currentTab = this.state.key;

    return(
        <div className="container bg-white mt-4 mb-5 pb-2 div-shadow" >
            <div className="row>">
                <div className="col-sm-12">
                    <Formik
                        initialValues={formData}
                        validationSchema={schemaArray[currentTab - 1]}
                        validateOnBlur={true}
                        validateOnChange={false}
                        enableReinitialize={true} 
                        onSubmit={this.saveEmployee}>
                        {(props) => {
                            console.log(props);
                            return(
                            <Form>
                                <Tabs 
                                    activeKey={this.state.key}
                                    onSelect={this.handleTabSelect} 
                                    id="controlled-tab-example" >
                                    <Tab eventKey={1} title="Personal Information" disabled>
                                        <PersonalInfo />
                                    </Tab>
                                    <Tab eventKey={2} title="Address" disabled>
                                        <Address />
                                    </Tab>
                                    <Tab eventKey={3} title="Educational Background" disabled>
                                        <Education />
                                    </Tab>
                                    <Tab eventKey={4} title="Employment Details" disabled>
                                        <EmploymentDetails />
                                    </Tab>
                                    <Tab eventKey={5} title="Additional Info" disabled>
                                        <AdditionalInfo />
                                    </Tab>
                                </Tabs>
                                <div className="cl-lg-12 justify-content-center">
                                    {currentTab !=1 ?  
                                            <Button                                        
                                            variant="danger"
                                            className="m-2 btn-lg"
                                            disabled={currentTab === 1 || isSaving}
                                            type="button"
                                            onClick={()=>this.handlePreviousButton(currentTab)} > 
                                            <FontAwesomeIcon icon={faArrowCircleLeft} /> Prev 
                                        </Button> : null}
                                    {currentTab === 5 ? null : 
                                         <Button 
                                            variant="primary"
                                            className="m-2 btn-lg"
                                            disabled={currentTab === 5}
                                            type="button"
                                            onClick={() => {                        
                                                props.validateForm()
                                                .then(validation => {
                                                    if(this.hasNoError(validation)) this.handleNextButton(currentTab);
                                                    props.setTouched(validation)                   
                                                })  
                                            }}> Next <FontAwesomeIcon icon={faArrowCircleRight} />
                                        </Button> }
                                    { currentTab === 5 ? 
                                        <Button 
                                        variant="primary"
                                        className="mt-2 mb-2 btn-block"
                                        disabled={isSaving ||  isSaved}
                                        type="submit">
                                        {isSaving ? 'Saving…' : 'Save'}
                                        </Button> : null }
                                </div>
                            </Form>)
                        }}
                    </Formik>
                </div>
            </div>
        </div>
    )
}
mossa-Sammer commented 4 years ago

@sibelius after adding setTouched({}) in the if statement inside the submit function works for me, please tell me if there is something worng with it

bitlustechnologies commented 3 years ago

Export the schemas as an array, and then just access the right one using page index: validationSchema={validationSchemas[page]}

This my attempt at that. https://codesandbox.io/s/k5m5po380v

I have a similar implementation in react-native, and I've noticed that also in your code, that when one goes back to a previous page, you need to click twice on next button to go to the next page. How can this be fixed?

import React from "react"; import ReactDOM from "react-dom"; import { Formik, Form, Field, ErrorMessage } from "formik"; import * as Yup from "yup";

function App() { const [step,setStep] = React.useState(0); const [lastStep,setLastStep] = React.useState(1);

const SignUpSchema1 = Yup.object().shape({ firstName: Yup.string() .required("Firstname is required"),

lastName: Yup.string()

  .required("Lastname is required")

});

const SignUpSchema2 = Yup.object().shape({

email:Yup.string().email().required("Email is required"),

password:Yup.string()
  .required("Password is required")

});

const SignUpSchema=[SignUpSchema1,SignUpSchema2] const initialValues = { firstName:"", lastName:"", email: "", password: "",

};

const handleSubmit = (values, formikbag) => {

if(step===1){
  console.log("FINAL",values)

}
setTimeout(() => {
  formikbag.setSubmitting(false);
}, 1000);

}; const onBack =()=>{ setStep(0);

}

const nextStep = (props) => { // Next Step dont go over Max step console.log(props.errors); console.log("Next step"); console.log(Object.keys(props.errors).length) props.submitForm().then(() => { if (props.isValid && props.values.firstName!=="" && props.values.lastName!=="") { setStep(1); setLastStep(2) props.validateForm(); props.setTouched({}); } }); if(lastStep===2 && props.values.firstName!=="" && props.values.lastName!==""){ setTimeout(()=>{ setStep(1); props.validateForm(); props.setTouched({}); },100) }

};

const step1 = props => { if(step===0){ return(

  )
}

}

const step2 = props => { if(step===1){ return(

  )
}

} return (

(
{step1(props)} {step2(props)}
)} />

); } export default App

aryaniyaps commented 3 years ago
  const handleSubmit = async (values, bag) => {
    if (step.props.onSubmit) {
      // a server error is set here.
      // (by the step's submit method).
      await step.props.onSubmit(values, bag);
    }
    if (isLastStep) {
      return props.onSubmit(values, bag);
    } else {
      // the wizard moves to the next step even if there
      // are server side errors set here.
      bag.setTouched({});
      next(values);
    }
  };

This is my problem. The wizard doesn't check for server side errors inbetween steps. Possible solution is to check if the form is valid before going to the next step. However, is this possible?

What could possibly be the solution?

    if (step.props.onSubmit) {
      // OVER HERE
      await step.props.onSubmit(values, bag);
    }

If I return Promise.reject() here, then the submit handler stops and the wizard doesn't move to the next step. However, this seems like a hacky solution.

aryaniyaps commented 3 years ago

@jaredpalmer could there be an example which also takes care of server side errors?

aryaniyaps commented 3 years ago

I am currently catching server side errors like this (instead of catching errors and setting them inside the onSubmit method). This looks clean to me. Please let me know if this is the best way to do this.

  const handleSubmit = async (values, bag) => {
    try {
      if (step.props.onSubmit) {
        await step.props.onSubmit(values);
      }
      if (isLastStep) {
        await props.onSubmit(values);
      } else {
        bag.setTouched({});
        next(values);
      }
    } catch (err) {
      // handle server side errors.
      const resp = err.response;
      if (typeof resp === "object" && "data" in resp) {
        bag.setErrors(transformErrors(resp.data));
      }
    }
  };