Reputation: 41
I'm implementing the multi-step wizard example with Material-UI components and it works well with the useField()
hook but I cannot figure out how to bring setFieldValue()
into scope, so I can use it from a wizard step.
I've seen suggestions to use the connect()
higher-order component but I have no idea how to do that.
Here is a snippet of my code: CodeSandbox, and the use case:
A wizard step has some optional fields that can be shown/hidden using a Material-UI Switch
. I would like the values in the optional fields to be cleared when the switch is toggled off.
I.e.
Hoping someone can help! Thanks.
Upvotes: 1
Views: 8862
Reputation: 41
I came across this answer the other day but discarded it because I couldn't get it working. It does actually work but I'm in two minds as to whether it's the right approach.
const handleOptionalChange = (form) => {
setOptional(!optional)
form.setFieldValue('optionalComments', '', false)
}
<FormGroup>
<FormControlLabel
control={
// As this element is not a Formik field, it has no access to the Formik context.
// Wrap with Field to gain access to the context.
<Field>
{({ field, form }) => (
<Switch
checked={optional}
onChange={() => handleOptionalChange(form)}
name="optional"
color="primary"
/>
)}
</Field>
}
label="Optional"
/>
</FormGroup>
Upvotes: 1
Reputation: 768
I believe this is what you're after: CodeSandbox. I forked your CodeSandbox.
I tried to follow your code as closely as possible and ended up not using WizardStep
. The step
variable is returning a React component that is a child to Formik
. Formik
is rendered with props e.g. setFieldValue
, which can be passed down to its children. In order to pass the setFieldValue
as a prop to step
, I had to use cloneElement()
(https://reactjs.org/docs/react-api.html#cloneelement), which allows me to clone the step
component and add props to it as follows.
// FormikWizard.js
<Formik
initialValues={snapshot}
onSubmit={handleSubmit}
validate={step.props.validate}
>
{(formik) => (
<Form>
<DialogContent className={classes.wizardDialogContent}>
<Stepper
className={classes.wizardDialogStepper}
activeStep={stepNumber}
alternativeLabel
>
{steps.map((step) => (
<Step key={step.props.name}>
<StepLabel>{step.props.name}</StepLabel>
</Step>
))}
</Stepper>
<Box
className={classes.wizardStepContent}
data-cy="wizardStepContent"
>
{React.cloneElement(step, {
setFieldValue: formik.setFieldValue
})}
</Box>
</DialogContent>
<DialogActions
className={classes.wizardDialogActions}
data-cy="wizardDialogActions"
>
<Button onClick={handleCancel} color="primary">
Cancel
</Button>
<Button
disabled={stepNumber <= 0}
onClick={() => handleBack(formik.values)}
color="primary"
>
Back
</Button>
<Button
disabled={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{isFinalStep ? "Submit" : "Next"}
</Button>
</DialogActions>
</Form>
)}
</Formik>
To access the setFieldValue
prop in the child component, in App.js
, I created a new component called StepOne
and used it to wrap around the inputs, instead of using WizardStep
. Now I am able to access setFieldValue
and use it in the handleOptionalChange
function.
// App.js
import React, { useState } from "react";
import "./styles.css";
import { makeStyles } from "@material-ui/core/styles";
import Box from "@material-ui/core/Box";
import CssBaseline from "@material-ui/core/CssBaseline";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormGroup from "@material-ui/core/FormGroup";
import Switch from "@material-ui/core/Switch";
import FormikTextField from "./FormikTextField";
import { Wizard, WizardStep } from "./FormikWizard";
const useStyles = makeStyles((theme) => ({
content: {
display: "flex",
flexFlow: "column nowrap",
alignItems: "center",
width: "100%"
}
}));
const initialValues = {
forename: "",
surname: "",
optionalComments: ""
};
const StepOne = ({ setFieldValue }) => {
const classes = useStyles();
const [optional, setOptional] = useState(false);
const displayOptional = optional ? null : "none";
const handleOptionalChange = () => {
setFieldValue("optionalComments", "");
setOptional(!optional);
};
return (
<Box className={classes.content}>
<FormikTextField
fullWidth
size="small"
variant="outlined"
name="forename"
label="Forename"
type="text"
/>
<FormikTextField
fullWidth
size="small"
variant="outlined"
name="surname"
label="Surname"
type="text"
/>
<FormGroup>
<FormControlLabel
control={
<Switch
checked={optional}
onChange={handleOptionalChange}
name="optional"
color="primary"
/>
}
label="Optional"
/>
</FormGroup>
<FormikTextField
style={{ display: displayOptional }}
fullWidth
size="small"
variant="outlined"
name="optionalComments"
label="Comments"
type="text"
/>
</Box>
);
};
function App(props) {
return (
<>
<CssBaseline />
<Wizard
title="My Wizard"
open={true}
initialValues={initialValues}
onCancel={() => {
return;
}}
onSubmit={async (values) => {
console.log(JSON.stringify(values));
}}
>
<StepOne />
<StepTwo />
</Wizard>
</>
);
}
export default App;
To use setFieldValue
in Formik, the easiest way would be to have the all input elements within the <Formik></Formik
tags. You could conditionally render the input elements based on what step you're on as follows. This gives the inputs a direct access to setFieldValue
so you can call setFieldValue("optionalComments", "")
on the Switch
input which will clear the comments on each toggle. Although this may mean you'll have a longer form, I don't think this is necessarily a bad thing.
<Formik>
<Form>
{step === 1 && <div>
// Insert inputs here
</div>}
{step === 2 && <div>
<TextField
onChange={(event) => setFieldValue("someField", event.target.value)}
/>
<Switch
checked={optional}
onChange={() => {
setFieldValue("optionalComments", "");
setOptional(!optional);
}}
name="optional"
color="primary"
/>
</div>}
</Form>
</Formik>
Upvotes: 0