Oli
Oli

Reputation: 41

How to set values in a multi-step Formik form with components that implement useField()

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.

  1. Toggle switch on.
  2. Enter data in Comments field.
  3. Toggle switch off.
  4. Comments value is cleared.
  5. Toggle switch on.
  6. Comments field is empty.

Hoping someone can help! Thanks.

Upvotes: 1

Views: 8862

Answers (2)

Oli
Oli

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>

CodeSandbox.

Upvotes: 1

jeffreyquan
jeffreyquan

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;

Alternative

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

Related Questions