Formik and Material-UI

Viewed 40121

I am trying to use Formik with Material-UI text field. Like so:

import TextField from '@material-ui/core/TextField';
import {
  Field,
  FieldProps,
  Form,
  Formik,
  FormikErrors,
  FormikProps
} from 'formik';
import React, { Component } from 'react';

interface IMyFormValues {
  firstName: string;
}

class CreateAgreementForm extends Component<{}> {
  public render() {
    return (
      <div>
        <h1>My Example</h1>
        <Formik
          initialValues={{ firstName: '' }}
          // tslint:disable-next-line:jsx-no-lambda
          onSubmit={(values: IMyFormValues) => alert(JSON.stringify(values))}
          // tslint:disable-next-line:jsx-no-lambda
          validate={(values: IMyFormValues) => {
            const errors: FormikErrors<IMyFormValues> = {};
            if (!values.firstName) {
              errors.firstName = 'Required';
            }
            return errors;
          }}
          // tslint:disable-next-line:jsx-no-lambda
          render={(formikBag: FormikProps<IMyFormValues>) => (
            <Form>
              <Field
                name="firstName"
                render={({ field, form }: FieldProps<IMyFormValues>) => (
                  <TextField
                    error={Boolean(
                      form.errors.firstName && form.touched.firstName
                    )}
                    helperText={
                      form.errors.firstName &&
                      form.touched.firstName &&
                      String(form.errors.firstName)
                    }
                  />
                )}
              />
            </Form>
          )}
        />
      </div>
    );
  }
}

export default CreateAgreementForm;

I want Formik to be responsible for validation and Material-UI for looks. I want to pass errors.firstName to TextField component but the error doesn't display correctly. How can I fix it so it still will be clear to read? I don't want to write my own TextField component.

7 Answers

I don't think you need another library or even create your own wrapper, I think you need to tweek your code a bit.

One problem you have is that you don't pass an onChange function in the Material TextField so the form value of firstName is always null and so you always get the error, even if you have entered a name. Try adding a name or id on your TextField and an onChange function like so:

<Field
    validateOnBlur
    validateOnChange
    name="firstName"
    render={({ field, form }) => (
    <TextField
        name={"firstName"}
        error={
            Boolean(form.errors.firstName && form.touched.firstName)
        }
        onChange={formikBag.handleChange}
        onBlur={formikBag.handleBlur}
        helperText={
            form.errors.firstName &&
            form.touched.firstName &&
            String(form.errors.firstName)
        }
    />
    )}
/>

As mentionned in comments, it may actually be a good idea to implement "wrapper" components, like they did in this samples from Formik or ReactFinalForm :

The idea is the same : implement custom "wrapper" components to wrap Material-UI components and map Formik or ReactFinalForm APIs props.

The advantages of this approach is to centralize in one place the mapping between the two frameworks, so that you do not repeat the mapping each time, and if one of the framework introduces breaking changes you just have to change those custom "wrapper" components.

You can try this: https://github.com/daixianceng/formik-material-fields

Installation:

npm install --save formik-material-fields

Usage:

import React, { Component } from 'react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import { FormikTextField } from 'formik-material-fields';

const validationSchema = Yup.object().shape({
  username: Yup.string().required(),
});

const initialValues = {
  username: '',
};

class MyForm extends Component {
  render() {
    return (
      <Formik
        initialValues={initialValues}
        validationSchema={validationSchema}
        onSubmit={this.props.onSubmit}
      >
        {({ isValid }) => (
          <Form autoComplete="off">
            <FormikTextField
              name="username"
              label="Username"
              margin="normal"
              fullWidth
            />
          </Form>
        )}
      </Formik>
    );
  }
}

Check out the formik docs for <Field /> here: https://jaredpalmer.com/formik/docs/api/field

As an example you could use Material's OutlinedInput to style your input:

<Field as={OutlinedInput} />

If you need to pass more props to OutlinedInput, simply add them to Field and it will pass them on to OutlinedInput:

<Field as={OutlinedInput} color="primary" InputProps={{ startAdornment: <InputAdornment position="start"><AccountCircle /></InputAdornment> }} />

You can use setFieldValue method useful for creating custom input change handlers

<Formik
  initialValues={{
    name: "",
  }}
  onSubmit={(values: any) => console.log(values)}
>
  {({  handleSubmit, setFieldValue }) => (
    <Form noValidate autoComplete="off" onSubmit={handleSubmit}>
      <TextField
        onChange={(event) => setFieldValue("name", event.target.value)} 
        type="text"
        label="Name"
      />
    </Form>
  )}
</Formik>

You could also try this library, which does the heavy-lifting for you and implements the wrapper code around Material-UI components (including <TextField />): https://github.com/stackworx/formik-material-ui.

Installation:

yarn add formik-material-ui

In your Formik form component, pass the <TextField /> component as the component prop of the Formik <Field /> component.

import { Formik, Field, Form } from 'formik';
import { TextField } from 'formik-material-ui';

<Field
  name="email"
  label="Email"
  type="email"
  component={TextField}
/>

Formik will continue to handle the validation as expected and will render the Material UI component and error message. There are additional details in the docs for other Mui input components and to aid with customization.

Related