th3g3ntl3man
th3g3ntl3man

Reputation: 2106

How to manage a multi-step form with React?

Here is the code of my multistep form:

import clsx from 'clsx';
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles, withStyles } from '@material-ui/styles';
import Step from '@material-ui/core/Step';
import Stepper from '@material-ui/core/Stepper';
import StepLabel from '@material-ui/core/StepLabel';
import StepConnector from '@material-ui/core/StepConnector';
import { Container, Row, Col, Button } from 'react-bootstrap';

import Description from '@material-ui/icons/Description';
import AccountCircle from '@material-ui/icons/AccountCircle';
import DirectionsCar from '@material-ui/icons/DirectionsCar';

import Step1 from '../components/Step1';
import Step2 from '../components/Step2';
import Step3 from '../components/Step3';

const styles = () => ({
  root: {
    width: '90%',
  },
  button: {
    marginRight: '0 auto',
  },
  instructions: {
    marginTop: '0 auto',
    marginBottom: '0 auto',
  },
});

const ColorlibConnector = withStyles({ 
  alternativeLabel: {
    top: 22,
  },
  active: {
    '& $line': {
      backgroundColor: '#00b0ff',
    },
  },
  completed: {
    '& $line': {
      backgroundColor: '#00b0ff',
    },
  },
  line: {
    height: 3,
    border: 0,
    backgroundColor: '#eaeaf0',
    borderRadius: 1,
  },
})(StepConnector);

const useColorlibStepIconStyles = makeStyles({
  root: {
    backgroundColor: '#ccc',
    zIndex: 1,
    color: '#fff',
    width: 50,
    height: 50,
    display: 'flex',
    borderRadius: '50%',
    justifyContent: 'center',
    alignItems: 'center',
  },
  active: {
    backgroundColor: '#00b0ff',
    boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)',
  },
  completed: {
    backgroundColor: '#00b0ff',
  },
});

function ColorlibStepIcon(props) {
  const classes = useColorlibStepIconStyles();
  const { active, completed } = props;

  const icons = {
    1: <AccountCircle />,
    2: <DirectionsCar />,
    3: <Description />,
  };

  return (
    <div
      className={clsx(classes.root, {
        [classes.active]: active,
        [classes.completed]: completed,
      })}
    >
      {icons[String(props.icon)]}
    </div>
  );
}

function getSteps() {
  return ['Dati Assicurato', 'Dati Veicolo', 'Dati Assicurazione'];
}

function getStepContent(step) {
  switch (step) {
    case 0:
      return <Step1/>;
    case 1:
      return <Step2/>;
    case 2:
      return <Step3/>;;
    default:
      return 'Unknown step';
  }
}

class HorizontalLinearStepper extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      activeStep: 0,
      agencyData: {}
    };
  }

  static propTypes = {
    classes: PropTypes.object,
  };

  isStepOptional = step => {
    return step === 1;
  };

  handleNext = () => {
    const { activeStep } = this.state;
    this.setState({
      activeStep: activeStep + 1,
    });
  };

  handleBack = () => {
    const { activeStep } = this.state;
    this.setState({
      activeStep: activeStep - 1,
    });
  };

  handleReset = () => {
    this.setState({
      activeStep: 0,
    });
  };

  logout = () => {
    localStorage.clear();
    this.props.history.push('/');
  }

  render() {
    const { classes } = this.props;
    const steps = getSteps();
    const { activeStep } = this.state;

    return (
      <Container fluid>
        <div className={classes.root}>
          <Stepper alternativeLabel activeStep={activeStep} connector={<ColorlibConnector />}>
            {steps.map((label, index) => {
              const props = {};

              return (
                <Step key={label} {...props}>
                  <StepLabel StepIconComponent={ColorlibStepIcon}>{label}</StepLabel>
                </Step>
              );
            })}
          </Stepper>

          <div>
            {activeStep === steps.length ? (
              <div style={{textAlign: 'center'}}>
                <h1 style={{textAlign: 'center', paddingTop: 100, color: '#7fc297'}}>
                  TERMINATO
                </h1>
                <h4 style={{textAlign: 'center', paddingTop: 50}}>
                  Tutti gli step sono stati completati con successo! 
                </h4>  
                <h4 style={{textAlign: 'center'}}>  
                  Procedi con la generazione del QR Code.
                </h4>
                <Row style={{marginTop: '40px'}} className='justify-content-center align-items-center text-center'>
                  <Col md={{span: 3}}>
                    <Button 
                      style={{borderRadius: 30, borderWidth: 0, height: 50, width: 150, backgroundColor: '#f32a19', borderColor: '#f32a19'}}
                      disabled={activeStep === 0} 
                      onClick={this.handleReset} 
                    >
                      Annulla
                    </Button>
                  </Col>
                  <Col md={{span: 3}}>
                    <Button
                        style={{borderRadius: 30, borderWidth: 0, height: 50, width: 150, backgroundColor: '#00b0ff'}}
                        onClick={() => console.log('Click')}
                      >
                      Procedi
                    </Button>
                  </Col>
                </Row>
              </div>
            ) : 
            (
              <Container style={{}}>
                <h2 className={classes.instructions}>{getStepContent(activeStep)}</h2>
                <Row className='justify-content-center align-items-center text-center'>
                  <Col md={{span: 3}}>
                    <Button 
                      style={{marginTop: 10, backgroundColor: 'gold', borderRadius: 30, borderWidth: 0, height: 50, width: 150}}
                      disabled={activeStep === 0} 
                      onClick={this.handleBack} 
                    >
                      Indietro
                    </Button>
                  </Col>
                  <Col md={{span: 3}}>
                    {
                      activeStep === steps.length - 1 ?
                      <Button
                        style={{marginTop: 10, borderRadius: 30, borderWidth: 0, height: 50, width: 150, backgroundColor: '#7fc297'}}
                        onClick={this.handleNext}
                      >
                      Finito
                      </Button>
                      :
                      <Button
                        style={{marginTop: 10, backgroundColor: '#00b0ff', borderRadius: 30, borderWidth: 0, height: 50, width: 150}}
                        onClick={this.handleNext}
                      >
                      Avanti
                      </Button>
                    }
                  </Col>
                </Row>
              </Container>
            )}
          </div>
        </div>
      </Container>
    );
  }
}

export default withStyles(styles)(HorizontalLinearStepper);

It is composed of three steps and at each step I ask for many data.


This is the code of one of the Step (they are all the same, the difference are the contents of the input fields):

import React from 'react';
import { Container, Row, Col, Form } from 'react-bootstrap';

export default function Step2(props) {

  return(
    <Container>
      <Row style={{marginTop: '30px'}} className='h-100 justify-content-center align-items-center'>
        <Col md={{ span: 6 }} className='text-center my-auto'>
          <h3 style={{marginBottom: '1rem'}}>Dati Veicolo</h3>
          <Form>
            <Form.Row>
              <Form.Group as={Col}>
                <Form.Control
                  type='text' 
                  placeholder='Marca' 
                  required
                />
              </Form.Group>
              <Form.Group as={Col}>
                <Form.Control
                  type='text' 
                  placeholder='Targa' 
                  required
                />
              </Form.Group>
            </Form.Row>
            <Form.Group>
              <Form.Control
                type='text' 
                placeholder='Paese immatricolazione' 
                required
              />
            </Form.Group>
            <h6 style={{marginBottom: '1rem'}}>Possiede un rimorchio?</h6>              
            <Form.Group>
              <Form.Control
                type='text' 
                placeholder='Targa'
              />
            </Form.Group>
            <Form.Group>
              <Form.Control
                type='text' 
                placeholder='Paese immatricolazione'
              />
            </Form.Group>
          </Form>
        </Col>
      </Row>
    </Container>
  );
}

What I need to do is to check for errors at each step before the users pass to the following one, so that they can start fulfilling the second step of the form only in the case they have completed the first step correctly, and so on ... How can I do this check step by step?

Moreover how can I collect all the data that I ask for in the main component of the form so that I can work with all those data after the users have finished fulfilling the whole form?

At this link there is the example

Upvotes: 5

Views: 13365

Answers (4)

mukuljainx
mukuljainx

Reputation: 716

You Component hierarchy seems good, I would like to add one thing to keep things together, like for steps you can create an Array inside a main component state like this

class Main extends React.Component{
    state = {
      ...OTHER_STATE_PROPERTIES,
      activeStep: 0 | 1| 2,
      steps: [{
        name: 'step-name',
        icon: 'icon-name',
        content: Form1 | Form2 | Form3, // this is optional, you can use getContent too
        data: {}
      }]
    }


    // pass this function as prop to every Form Component
    // We will talk about this function soon
    handleStepSubmit = (stepIndex, data) => {
      this.setState((prevState) => ({
        ...prevState,
        activeIndex: prevState.activeIndex + 1,
        steps: prevState.map((step, index) => {
          if(stepIndex !== index){
            return step; 
          }
          return {
            ...step,
            data
          }
        })
      }))
    }
    //... Other stuff

}

Now each Form should have its own state (so that only the form gets re-render on input changes) and form, where you can handle input and validate them and send it to parent component step using props, so we need to add a function in the parent component. handleStepSubmit function will only be called after validation of data on onSubmit call of form

How to validate data is up to you, you can use

default input attributes like,

  1. required
  2. min, max for number
  3. type (email)
  4. pattern

Validate State using JS logic

Use yup

Use Formik with yup

This is what I prefer by using formik you don't have to worry about onChange, validation and many more things, you can provide it validate prop where you can write validation logic yourself or validationSchema prop where just yup schema should be given, it will not allow if onSubmit to trigger if validation fails, you can also input attributes with it, if form is simple. We have to call handleStepSubmit on onSubmit

P.S.: It maintains a local state

What we have

At step 0 we will have

// I am omitting other stuff for understanding
state:{
   activeStep: 0,
   steps: [
     {data: {}},
     {data: {}},
     {data: {}},
   ]
}

When user submits Form 1 we will have

// I am omitting other stuff for understanding
state:{
   activeStep: 1,
   steps: [
     {data: formOneData},
     {data: {}},
     {data: {}},
   ]
}

and so one now when activeStep is 3 you will have all the validated data in the state, Horray!! High-Five! You can also data from previous steps in further step if required as we have it all in our parent component state.

Example:

Edit formik-example

Upvotes: 4

ChandraKumar
ChandraKumar

Reputation: 537

Basically, if I understand your question you would like

  • Create a multi-step form and do some validation of data on every step if fails then let the user refill the necessary data else move to next form data.

  • At every stage store in the state or store and once the user completes all step then you would like to work locally with that data.

Since you chose to work locally with those data I prefer redux-store to state because for obvious reasons like using the form data in business logic and other parts of the react

I hope Edit inspiring-babbage-81ejq this might be useful...

However, this is a basic structure and even I know there are lots of caveats and many things could be abstracted...

Upvotes: 0

soupette
soupette

Reputation: 1280

I would use a routing that points to the same component so you could store the old form in either your state/reducer/localStorage (in fact anything you want) and once the current step is ok I would simply navigate the user to the next one.

I have made a demo codesandbox with this logic.

I hope it helps

Upvotes: 1

Nima Ka
Nima Ka

Reputation: 163

Save all form data to your states (even completed steps like current step). For validating your given data you should arrange a handleChange function for each field.

Upvotes: -2

Related Questions