问题
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