Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 772
th3g3ntl3man Avatar asked Oct 15 '22 09:10

th3g3ntl3man


2 Answers

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

like image 102
mukuljainx Avatar answered Nov 01 '22 13:11

mukuljainx


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

like image 24
soupette Avatar answered Nov 01 '22 11:11

soupette