使用 React Hooks 在 Material-UI Stepper 之间传递数据

Passing data across Material-UI Stepper using React Hooks

我有一个多步骤表单,我想使用 FormikMaterial-ui、功能组件和 getState 挂钩在 React 中实现。

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>
  );
}

为了便于讨论,每个子表单大致如下所示,每个子表单只有 2 个字段:

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>
  );
}

让我难以理解的一点是状态的更新。在表单之间切换时,如何确保保存每个子表单的子状态?另外,(( typeof passedValues.field1 === 'undefined' || passedValues.field1 === null ) ? '1' : passedValues.field1 ) 的构造看起来很笨拙?

好的,我让它工作了,这很有趣(对于小值的乐趣)。一半的问题是认识到需要将 activeStep 值、handleNext()handleBack() 函数传递给子表单,以及预先计算此 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;

此时,子表单可以检查其字段是否有效,然后再进行下一步:

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>
  );
}

唯一的其他技巧是在子表单无效时记住 setTouched(errors),以便显示未触及的字段的验证错误。

这实际上是我在 Whosebug 上的第一个 post,但这对我帮助很大,我不得不添加它。

在我的例子中,我没有使用 sub-Forms,所以我转而使用 useState 来获得一个对象来放置来自输入的答案。我承认这有点蛮力,但一旦我在钩子中实例化对象后,感觉更容易管理。

const [ edit, setEdit ] = useState({1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: ''});
const [activeStep, setActiveStep] = useState(0);

const handleInputChange = (e) => {
    setEdit({...edit, [e.target.name]: e.target.value});
}
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);};

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

我知道动态对象会更好,但通过此设置,我可以使用 MUI 输入仅更改名称和值,如下所示:

<Input
  name={`${activeStep + 1}`}
  variant="standard"
  value={edit[`${activeStep + 1}`]}
  onChange={(e) => handleInputChange(e)}
  />

这样每一步输入值都会移动,希望对大家有帮助!