Search code examples
javascriptreactjsformsreact-final-form

React.js Form - How To Include All Values On Final Page?


So I have built a Wizard Form using React-Final-Form that I am using in my sign-up page. I am trying to figure out how I can display all user inputs on the final page as a way for the user to double-check/verify their inputs before submitting. Any help would be greatly appreciated!

(P.S. - I tried researching this before posting, but all I was able to find was storing user inputs in Redux and accessing them from there, which I'd like to avoid, if at all possible.)

Here is an example link that shows what I want to do - Please feel free to fork and play around with it if you are trying to figure out a solution! https://codesandbox.io/s/0332k02x0v

Here is the code, shortened to include only the relevant bits:

My Wizard.js page:

import React, { Component } from "react";
import PropTypes from "prop-types";
import { Form } from "react-final-form";

class Wizard extends Component {
  static propTypes = {
    onSubmit: PropTypes.func.isRequired
  };
  static Page = ({ children }) => children;

  constructor(props) {
    super(props);
    this.state = {
      page: 0,
      values: props.initialValues || {}
    };
  }
  next = values =>
    this.setState(state => ({
      page: Math.min(state.page + 1, this.props.children.length - 1),
      values
    }));

  previous = () =>
    this.setState(state => ({
      page: Math.max(state.page - 1, 0)
    }));

  validate = values => {
    const activePage = React.Children.toArray(this.props.children)[
      this.state.page
    ];
    return activePage.props.validate ? activePage.props.validate(values) : {};
  };

  handleSubmit = values => {
    const { children, onSubmit } = this.props;
    const { page } = this.state;
    const isLastPage = page === React.Children.count(children) - 1;
    if (isLastPage) {
      return onSubmit(values);
    } else {
      this.next(values);
    }
  };

  render() {
    const { children } = this.props;
    const { page, values } = this.state;
    const activePage = React.Children.toArray(children)[page];
    const isLastPage = page === React.Children.count(children) - 1;
    return (
      <Form
        initialValues={values}
        validate={this.validate}
        onSubmit={this.handleSubmit}
      >
        {({ handleSubmit, submitting, values }) => (
          <form onSubmit={handleSubmit}>
            {activePage}
            <div className="buttons">
              {page > 0 && (
                <button type="button" onClick={this.previous}>
                  « Previous
                </button>
              )}
              {!isLastPage && <button type="submit">Next »</button>}
              {isLastPage && (
                <button type="submit" disabled={submitting}>
                  Submit
                </button>
              )}
            </div>

            {/* <pre>{JSON.stringify(values, 0, 2)}</pre> */}
          </form>
        )}
      </Form>
    );
  }
}

export default Wizard;

My index.js page:

import React, { Component } from "react";
import { Field } from "react-final-form";
import formatString from "format-string-by-pattern";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import Wizard from "./Wizard";
import Styles from "./Styles";
import { addUser } from "../../../actions/authActions";

class ReactFinalForm2 extends Component {
  state = {};
  render() {
    const onSubmit = async values => {
      this.props.addUser(values);
      // API query here
    };

    const Error = ({ name }) => (
      // Error handing here
    );

    return (
      <Styles>
        <div>
          <Wizard initialValues={{}} onSubmit={onSubmit}>
            <Wizard.Page
              validate={values => {
                // Page validation here
              }}
            >
              // Page inputs here
            </Wizard.Page>
            <Wizard.Page
              validate={values => {
                // Page validation here
              }}
            >
              // Page inputs here
            </Wizard.Page>
            <Wizard.Page
              validate={values => {
                // Page validation here
              }}
            >
              // Page inputs here
            </Wizard.Page>
            <Wizard.Page>
              {/* *** THIS IS WHERE I WOULD LIKE TO DISPLAY ALL PREVIOUS VALUES (SO THE USER CAN CONFIRM / DOUBLE-CHECK THEIR INPUTS BEFORE SUBMITTING) *** */}
            </Wizard.Page>
          </Wizard>
        </div>
      </Styles>
    );
  }
}

ReactFinalForm2.propTypes = {
  addUser: PropTypes.func.isRequired
};

export default connect(
  null,
  { addUser }
)(ReactFinalForm2);

Solution

  • I have added a state to the parent component. Changing this state on every submit from the child. I have JSON stringify the state in parent component. As you said no need to use redux, this is the workaround I came with. Still your code has a potential for improvements. Please check this working sandbox:

    [ https://codesandbox.io/s/zrvloq4o6x ]

    Wizard.js change

      handleSubmit = values => {
        const { children, onSubmit } = this.props;
        const { page } = this.state;
        const isLastPage = page === React.Children.count(children) - 1;
        if (isLastPage) {
          return onSubmit(values);
        } else {
          this.next(values);
        }
        // Change added
        this.props.onStateChange(values);
      };
    

    Index.js

    import React from "react";
    import { render } from "react-dom";
    import Styles from "./Styles";
    import { Field } from "react-final-form";
    import Wizard from "./Wizard";
    
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    const onSubmit = async values => {
      await sleep(300);
      window.alert(JSON.stringify(values, 0, 2));
    };
    
    const Error = ({ name }) => (
      <Field
        name={name}
        subscribe={{ touched: true, error: true }}
        render={({ meta: { touched, error } }) =>
          touched && error ? <span>{error}</span> : null
        }
      />
    );
    
    const required = value => (value ? undefined : "Required");
    let data = {};
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {};
        this.onStateChange = this.onStateChange.bind(this);
      }
    
      onStateChange = values => {
        this.setState({ data: values });
        console.log(values);
      };
      render() {
        return (
          <Styles>
            <h1>🏁 React Final Form Example</h1>
            <h2>Wizard Form</h2>
            <a href="https://github.com/erikras/react-final-form#-react-final-form">
              Read Docs
            </a>
            <p>
              Notice the mixture of field-level and record-level (or{" "}
              <em>page-level</em> in this case) validation.
            </p>
            <Wizard
              initialValues={{}}
              onStateChange={this.onStateChange}
              onSubmit={onSubmit}
            >
              <Wizard.Page>
                <div>
                  <label>First Name</label>
                  <Field
                    name="firstName"
                    component="input"
                    type="text"
                    placeholder="First Name"
                    validate={required}
                  />
                  <Error name="firstName" />
                </div>
                <div>
                  <label>Last Name</label>
                  <Field
                    name="lastName"
                    component="input"
                    type="text"
                    placeholder="Last Name"
                    validate={required}
                  />
                  <Error name="lastName" />
                </div>
              </Wizard.Page>
              <Wizard.Page
                validate={values => {
                  const errors = {};
                  if (!values.notes) {
                    errors.notes = "Required";
                  }
                  return errors;
                }}
              >
                <div>
                  <label>Best Stooge?</label>
                  <div>
                    <label>
                      <Field
                        name="stooge"
                        component="input"
                        type="radio"
                        value="larry"
                      />{" "}
                      Larry
                    </label>
                    <label>
                      <Field
                        name="stooge"
                        component="input"
                        type="radio"
                        value="moe"
                      />{" "}
                      Moe
                    </label>
                    <label>
                      <Field
                        name="stooge"
                        component="input"
                        type="radio"
                        value="curly"
                      />{" "}
                      Curly
                    </label>
                  </div>
                </div>
                <div>
                  <label>Notes</label>
                  <Field name="notes" component="textarea" placeholder="Notes" />
                  <Error name="notes" />
                </div>
              </Wizard.Page>
              <Wizard.Page>
                <div>
                  <p>
                    <b>Display all previous values here for user verification </b>
                    <br />
                    <i>{JSON.stringify(this.state.data, 0, 2)}</i>
                  </p>
                </div>
              </Wizard.Page>
            </Wizard>
          </Styles>
        );
      }
    }
    
    render(<App />, document.getElementById("root"));