jaredpalmer / formik

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

Multistep Wizard example - How to access this.state.value from Wizard inside each Wizard.page? #844

Closed Huanzhang89 closed 5 years ago

Huanzhang89 commented 6 years ago

I am using the multistep wizard from the examples section to implement a 2 page form but there is one small issue that I have and haven't been able to figure out.

The example showed how to easily hook up the input fields so that changes update automatically in the Wizard component's state.

For example I have a Field like the one below and I want the user's selection in this Field to affect the label of a sibling input. I am struggling to find a way to access the value of the Colour field.

<div className="form-input">
      <label className="input-label">Select Colour</label>
      <Field name="colour" component="select">
        <option value={"red"}>Red</option>
        <option value={"blue"}>Blue</option>
        <option value={"yellow"}>Yellow</option>
      </Field>
    </div>

I have successfully attached state of the Wizard component to each child component using the following code:

const childrenWithState = React.Children.map(children, child => {
    return React.cloneElement(child, { parentState: this.state })
})
const activePage = React.Children.toArray(childrenWithState)[page]

But what I am stuck on is how to access the props in Wizard.Page, since we are just calling the static method Page on the Wizard component and have no access to this.

njj commented 6 years ago

@Huanzhang89 Have you considered abstracting the Page into its own component and then passing state from the parent as props, using a callback for updating props to the parent, etc..?

Huanzhang89 commented 6 years ago

@njj I did consider that but the issue here is passing the props down to Wizard.Page and accessing them inside Wizard.Page. Even if we abstract the Page into its own component, at the point where we call <Page props={parentState} /> we still do not have access to the parentState to pass into the Page component.

I have however found a solution to this using renderProps! The problem is that inside the static method Wizard.Page, the props are not passed down to the children as it simply returns the children.

static Page = ({ children }) => {
    return children
  }

By modifying this static method to

static Page = ({ children, parentState }) => {
    return children(parentState)
  }

And also modifying the JSX inside each Wizard.Page to be a function that returns the JSX, we can pass the parentState down to each Wizard.Page, like so.

  <Wizard.Page>
              {props => {
                return (
                     // JSX here
                )}
               }
  </Wizard.Page>

props in the above function = parentState

Hope this helps anyone else getting stuck with this issue!

njj commented 6 years ago

@Huanzhang89 Nice, you could also do this by using React.cloneElement for the {activePage}, i.e:

{ React.cloneElement(activePage, { ...props }) 
Huanzhang89 commented 6 years ago

Oh really, do you mean instead of just {activePage} inside the render function of the Wizard?

But if you look at the first post I made, I am already using React.cloneElement to pass the parentState to the activePage.

I think we still need to renderProps inside Wizard.Page to pass the parentState down to the children. Not 100% sure about this though.

njj commented 6 years ago

@Huanzhang89 Right then you will need to handle it in the Page function again. Either way I think works. I ended up doing something similar, but I'm passing the Formik props (values, fns, etc..) so my fields can have them.

Huanzhang89 commented 6 years ago

Ah ok I see what you mean. Maybe we should create a PR to either update the current example or create a new multipage example? I can see a lot of use cases where we would need to access the form data inside each page and its not immediately clear how this can be done from the current example.

Huanzhang89 commented 6 years ago

@njj Actually after some testing I found that my method doesn't update the form values correctly as the React.cloneElement was outside of the Formik component. So your method of { React.cloneElement(activePage, { ...props }) is the way to go!

shoaibkhan94 commented 6 years ago

Hi @Huanzhang89 @njj I am stuck on the same issue. Can you please share a code snippet or example to fix it. Any help would be really appreciate! Thanks

longnt80 commented 6 years ago

@shoaibkhan94 here's the code example with their solution: https://codesandbox.io/s/62nk7x0p73

shoaibkhan94 commented 6 years ago

@longnt80 Thanks. It's working!

Huanzhang89 commented 5 years ago

Sorry guys been away for a while, thanks @longnt80 for working it out and helping :)

timrombergjakobsson commented 5 years ago

Hey I saw this issue. Im having a somewhat similar problem. Im trying to access the values from a radiobutton and use that as an conditional rendering of a wizard.page. For example Im wrapping pages like this:

<Wizard.Page validate={validators.validateFirstPage}>
                { props => (
                        <React.Fragment>
                        ....more code here

And I have 3 radiobuttons on the first page and I would like to use the value of the radiobuttons to conditionally render pages like:

 { props => (                 
                    <React.Fragment>
                         { props.values.radioGroup1 === 'daily' ? (
                          .... render content here

But it is not working. Im getting a TypeError: children is not a function from my wizard class right here:

static Page = ({ children, parentState }) => {
      return children(parentState); <---- from here.
    };

I must be doing something wrong when trying to render base on the value of the radioGroup. Anyone tried anything like this?

Huanzhang89 commented 5 years ago

@timrombergjakobsson

Hey Tim, can you log out what children is inside static Page ? Could be you cloned the activepage wrong and it Wizard.Page does not have access to this.props.children?

Can you also page your React.cloneElement part of the form as that would shed some light on the issue!

timrombergjakobsson commented 5 years ago

@Huanzhang89 heres the React.cloneElement part:

render() {
        const { handleSubmit, children, prevButton, nextButton, classes } = this.props;
        const { page, values } = this.state;
        const activePage = React.Children.toArray(children)[page];
        const isLastPage = page === React.Children.count(children) - 1;
        const isFirstPage = page === 0;

        const PrevButton = this.props.prevButton;
        const NextButton = this.props.nextButton;
        let currentStep = this.state.page;
        console.log(values);
        console.log(this.props.children, 'some children');
        return (
            <section className="site-main__content-wrapper">
              <Header currentStep={currentStep}/>
              <article className="site-main__content content-padding">
                 <div className="reminder__start">
                    <Formik
                      initialValues={this.state.values}
                      validate={this.validate}
                      enableReinitialize={false}
                      onSubmit={this.handleSubmit}
                      { ...children }
                      render={props => ( 
                        <form className="reminder-form" onSubmit={props.handleSubmit}>
                            {React.cloneElement(activePage, { parentState: { ...props } })}
Huanzhang89 commented 5 years ago

Okay so one thing I notice is that you are spreading children and assigning it to the props of Formik. You should remove that.

However the true culprit for your error is actually the fact that you spread props inside parentState. This means that your Page no longer has access to this.props.children.

{React.cloneElement(activePage, { props.children, parentState: { ...props } })} would work or {React.cloneElement(activePage, { parentState: props.parentState })} assuming thats what you wanted to pass to parentState

timrombergjakobsson commented 5 years ago

@Huanzhang89 ah ok, so remove children from here: const { handleSubmit, children, prevButton, nextButton, classes } = this.props; ? What about this part { ...children }? And is it not supposed to be {React.cloneElement(activePage, { props: props.children, parentState: { ...props } })} and not {React.cloneElement(activePage, { props.children, parentState: { ...props } })}?

Huanzhang89 commented 5 years ago

Yes you should remove { ...children } since that prop is not being used by formik.

For the second point I'll try to explain. The entire object of the second argument for React.cloneElement gets assigned to the props of the resulting React element. Remember the props of a React Element is simply an object, so in your case the props of your Element would be { props.children: Function, parentState: Object }. And to access the children you would have to use this.props.props.children which doesn't really make sense.

Did making that change fix your issue btw?

timrombergjakobsson commented 5 years ago

@Huanzhang89 I think Im with you, the only thing is that {React.cloneElement(activePage, { props.children, parentState: { ...props } })}does not work, cos its syntax error. So I had to change to {React.cloneElement(activePage, { props: props.children, parentState: { ...props } })} instead.

Huanzhang89 commented 5 years ago

Ah sorry I made a mistake with that, it should be {React.cloneElement(activePage, { children: ...props.children, parentState: { ...props } })

But I would actually recommend not spreading ...props into parentState. Rather construct the parentState object before passing it to cloneElement here.

timrombergjakobsson commented 5 years ago

Ah alright, ok great thanks. Well yes I had to do a quite verbose and ugly solution to my problem:

<Wizard.Page validate={validators.validateSecondPage}>
                { props => (                 
                    <React.Fragment>
                        { props.values.radioGroup1 === 'daily' ? ( <--- this is the condtional.
                        html here

Rather construct the parentState object before passing it to cloneElement

How do you mean?

mjangir commented 5 years ago

You can check out this wizard component built for formik:

https://github.com/mjangir/formik-wizard-form

Andreyco commented 5 years ago

closing in favor of #1315

vincentntang commented 4 years ago

I wrote an example using Reactstrap (UI library) with Yup + Formik, demonstrating all the important features for a wizard:

https://github.com/vincentntang/multistep-wizard-formik-yup-reactstrap