Passing data across Material-UI Stepper using React Hooks

五迷三道 提交于 2020-05-17 09:25:07

问题


I have a multi-step form that I want to implement in React using Formik, Material-ui, functional components, and the getState hook.

import React, { useState, Fragment } from 'react';
import { Button, Stepper, Step, StepLabel } from '@material-ui/core';
import FormPartA from './FormPartA';
import FormPartB from './FormPartB';
import FormPartC from './FormPartC';

function MultiStepForm(props) {

  const steps = ['Part A', 'Part B', 'Part C'];
  const passedValues = props.values || {};

  const [activeStep, setActiveStep] = useState(0);
  const [values, setValues] = useState({
    field1:(( typeof passedValues.field1 === 'undefined' || passedValues.field1 === null ) ? '1' : passedValues.field1 ),
    field2:(( typeof passedValues.field2 === 'undefined' || passedValues.field2 === null ) ? '2' : passedValues.field2 ),
    field3:(( typeof passedValues.field3 === 'undefined' || passedValues.field3 === null ) ? '3' : passedValues.field3 ),
    field4:(( typeof passedValues.field4 === 'undefined' || passedValues.field4 === null ) ? '4' : passedValues.field4 ),
    field5:(( typeof passedValues.field5 === 'undefined' || passedValues.field5 === null ) ? '5' : passedValues.field5 ),
    field6:(( typeof passedValues.field6 === 'undefined' || passedValues.field6 === null ) ? '6' : passedValues.field6 )
  });

  const handleNext = () => {
    alert({...props.values, ...values});
    setValues({...props.values, ...values});
    setActiveStep(activeStep + 1);
  };

  const handleBack = () => {
    setActiveStep(activeStep - 1);
  };

  function thisStep(step) {
    switch (step) {
      case 0:
        return <FormPartA values={values} setValues={setValues}/>;
      case 1:
        return <FormPartB values={values} setValues={setValues}/>;
      case 2:
        return <FormPartC values={values} setValues={setValues}/>;
      default:
        throw new Error('Mis-step!');
    }
  }

  return (
    <div className="MultiStepForm">
      <Stepper activeStep={activeStep} className={classes.stepper}>
        {steps.map(label => (
          <Step key={label}>
            <StepLabel>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
      <Fragment>
        {activeStep === steps.length ? ( 
          <p>You're done!<p>
          ) : (
          <Fragment>
            {thisStep(activeStep)}
            <div className={classes.buttons}>
              {activeStep !== 0 && (
                <Button onClick={handleBack} > Back </Button>
              )}
              <Button onClick={handleNext} >
                {activeStep === steps.length - 1 ? 'Done' : 'Next'}
              </Button>
            </div>
          </Fragment>
        )}
      </Fragment>
    </div>
  );
}

Each of the sub-forms, for sake of argument, look roughly like this, with just 2 fields per sub-form:

import React from 'react';

import {Formik, useField, Field, Form} from 'formik';
import { TextField } from 'formik-material-ui';
import * as Yup from "yup";
import { Button } from '@material-ui/core';

export default function BasicForm(props) {

  const field1 = ( typeof props.values.field1 === 'undefined' || props.values.field1 === null ) ? '' : props.values.field1;
  const field2 = ( typeof props.values.field2 === 'undefined' || props.values.field2 === null ) ? '' : props.values.field2;

  return (
    <div>

      <h3>Part A</h3>

      <Formik
        initialValues={{
          field1,
          field2
        }}
        validationSchema={Yup.object({
          field1: Yup.string()
            .required('Required'),
          field2: Yup.string()
            .required('Required'),
        })}
      >
      {({submitForm, isSubmitting, values, setFieldValue}) => (
        <Form>
          <Field name="field1" type="text" label="Field 1" variant="outlined" 
            margin="normal" fullWidth multiline component={TextField} />
          <Field name="field2" type="text" label="Field 2" variant="outlined" 
            margin="normal" fullWidth multiline component={TextField} />
        </Form>
        )}
      </Formik>
    </div>
  );
}

The bit that eludes me is the updating of state. How do I make sure that the child state from each sub-form is saved when stepping between forms? Also, the (( typeof passedValues.field1 === 'undefined' || passedValues.field1 === null ) ? '1' : passedValues.field1 ) construction seems clumsy?


回答1:


OK, I got it to work, which was lots of fun (for small values of fun). Half the issue was recognising that the activeStep value, handleNext(), and handleBack() functions needed to be passed in to the sub-forms, as well as pre-calculating whether this isLastStep:

import React, { useState, Fragment } from 'react';
import { Button, Stepper, Step, StepLabel } from '@material-ui/core';
import FormPartA from './FormPartA';
import FormPartB from './FormPartB';
import FormPartC from './FormPartC';

const steps = ['Part A', 'Part B', 'Part C'];

function MultiStepForm(props) {

  const { field1, field2, field3, field4, field5, field6, } = props;

  const [activeStep, setActiveStep] = useState(0);
  const [formValues, setFormValues] = useState({
    field1, field2, field3, field4, field5, field6
  });

  const handleNext = (newValues) => {
    setFormValues({ ...formValues, ...newValues });
    setActiveStep(activeStep + 1);
  };

  const handleBack = (newValues) => {
    setFormValues({ ...formValues, ...newValues });
    setActiveStep(activeStep - 1);
  };

  function getStepContent(step) {
    const isLastStep = (activeStep === steps.length - 1);
    switch (step) {
      case 0:
        return <BasicFormA {...formValues} activeStep={activeStep} isLastStep={isLastStep} handleBack={handleBack} handleNext={handleNext}/>;
      case 1:
        return <BasicFormB {...formValues} activeStep={activeStep} isLastStep={isLastStep} handleBack={handleBack} handleNext={handleNext}/>;
      case 2:
        return <BasicFormC {...formValues} activeStep={activeStep} isLastStep={isLastStep} handleBack={handleBack} handleNext={handleNext}/>;
      default:
        throw new Error('Mis-step!');
    }
  }

  return (
    <div className="MultiStepForm">
      <Stepper activeStep={activeStep} className={classes.stepper}>
        {steps.map(label => (
          <Step key={label}>
            <StepLabel>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
      <Fragment>
        {activeStep === steps.length ? (
           <p>You're done!<p>
        ) : (
        <Fragment> {getStepContent(activeStep)} <Fragment>
        )}
      <Fragment>
    </div>
  );
}

export default MultiStepForm;

At this point, the sub-form can check that its fields are valid, before going to the next step:

import React from 'react';

import {Formik, useField, Field, Form} from 'formik';
import { TextField } from 'formik-material-ui';
import * as Yup from "yup";
import { Button } from '@material-ui/core';

export default function BasicForm(props) {

  const { values, field1, field2, activeStep, isLastStep, handleBack, handleNext } = props;

  return (
    <div>
      <Formik
        initialValues={{
          field1,
          field2
        }}
        validationSchema={Yup.object({
          field1: Yup.string()
            .required('Required'),
          field2: Yup.string()
            .required('Required'),
        })}
      >
      {({submitForm, validateForm, setTouched, isSubmitting, values, setFieldValue}) => (
      <Form>
        <Field name="field1" type="text" label="Field 1" variant="outlined" margin="normal" fullWidth multiline component={TextField} />
        <Field name="field2" type="text" label="Field 2" variant="outlined" margin="normal" fullWidth multiline component={TextField} />
      </Form>
      <div>
        {activeStep !== 0 && (
          <Button onClick={() => { handleBack(values) } } className={classes.button}> Back </Button>
        )}
        <Button className={classes.button} variant="contained" color="primary" 
          onClick={
            () => validateForm()
              .then((errors) => {
                if(Object.entries(errors).length === 0 && errors.constructor === Object ) {
                  handleNext(values);
                } else {
                  setTouched(errors);
                }
              })
          } >
          {isLastStep ? 'Submit Draft' : 'Next'}
        </Button>
      </div>
      )}
    </Formik>
  </div>
  );
}

The only other trick is remembering to setTouched(errors) when the sub-form is invalid, so that untouched fields get their validation errors displayed.



来源:https://stackoverflow.com/questions/59035989/passing-data-across-material-ui-stepper-using-react-hooks

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!