Material-ui: Any example for autocomplete with formik

Created on 12 Nov 2019  路  13Comments  路  Source: mui-org/material-ui

Hi guy!
I need example for autocomplete with formik.
I tried the following efforts but encountered some problems.

This is my form:

import React from 'react';
import { Formik, Field, Form } from 'formik';
import AutoSelect from "./Select";
import Button from "@tui/Button"

const options = [
  { value:"all", label:"all poeple" },
  { value:"fans", label:"my fans" },
  { value:"follow", label:"my followers" }
]

export default ((props) => {

  return (
    <div
      css={`
        width:500px;
        margin:30px auto;
      `}
    >
      <Formik
        initialValues={{ uType: '',}}
        onSubmit={(values, actions) => {
          console.log(values);
        }}
        render={(props) => (
          <Form>
            <Field 
              type="select"
              name="uType" 
              placeholder="select..." 
              component = { AutoSelect }
              options = { options }
            />
            <Button 
              type="submit" 
              variant="contained" 
              color="primary" 
            >
              鎻愪氦
            </Button>
          </Form>
        )}
      />
    </div>
  )
})

The Select component base on autocomplete:

import React from 'react'
import PropTypes from 'prop-types'
import Autocomplete from '@material-ui/lab/Autocomplete';
import TextField from '@material-ui/core/TextField';

const AutoSelect = ({
  required,
  type,
  label,
  form,
  field,
  options,
  fullWidth,
  margin,
  placeholder,
  ...props
}) =>{
  const { name, onChange, value } = field;
  return (
    <Autocomplete
      {...props}
      type = { type }
      name = { name }
      value = { value }
      onChange = { onChange }
      getOptionLabel={option =>option.label}
      options = { options }
      renderInput={params =>{
        return (
          <TextField 
            {...params}             
            variant="outlined"  
            fullWidth 
            placeholder={ placeholder }
          />
        )
      }}
    />
  )
}

I get a weird result when I select an option and click the submit button.

{
   mui-autocomplete-27892-option-1: 0
   uType: ""
}

I get a wrong form value, uType is not updated when I select an option. And mui-autocomplete-27892-option-1: 0 this value does not know where to come from

Autocomplete discussion

Most helpful comment

In case someone is looking for a full working solution

import React from 'react';
import { Autocomplete } from '@material-ui/lab';
import { TextField } from '@material-ui/core';
import { fieldToTextField } from 'formik-material-ui';


const FormikAutocomplete = ({ textFieldProps, ...props }) => {

  const { form: { setTouched, setFieldValue } } = props;
  const { error, helperText, ...field } = fieldToTextField(props);
  const { name } = field;

  return (
    <Autocomplete
      {...props}
      {...field}
      onChange={ (_, value) => setFieldValue(name, value) }
      onBlur={ () => setTouched({ [name]: true }) }
      renderInput={ props => (
        <TextField {...props} {...textFieldProps} helperText={helperText} error={error} />
      )}
    />
  );
}

export default FormikAutocomplete;

All 13 comments

@sessionboy What do you think of moving this concern to https://github.com/stackworx/formik-material-ui? It likely requires the same handling than https://github.com/stackworx/formik-material-ui/blob/29848beb73bc4aa53c01e227376e835f58f036bc/src/Select.tsx#L29.
The change event can come from a click, Formik won't need where to find the new value if we unless we tell him.

@oliviertassinari Thank you for your answer. I have tried it.

const onChange = React.useCallback(
    (event: React.ChangeEvent<{ value: unknown }>) => {
      // Special case for multiple and native
      if (props.multiple && props.native) {
        const { options } = event.target as HTMLSelectElement;
        const value: string[] = [];
        for (let i = 0, l = options.length; i < l; i += 1) {
          if (options[i].selected) {
            value.push(options[i].value);
          }
        }

        setFieldValue(field.name, value);
      } else {
        field.onChange(event);
      }
    },
    [field.name, props.multiple, props.native]
  );

But the select component is not multiple or native, so it run to field.onChange(event);.
The result is still the same

I noticed the second parameter of the onChange method, it is currently option of selected, so I did the following:

const onChange = React.useCallback((event,item) => {
      setFieldValue(field.name, item.value);
    },
    [field.name]
);

Awesome, ow it works. But I have another problem from getOptionLabel.
It must be the following:

getOptionLabel = (value)=>value

This is not what I expected.I expect it to be label instead of value.
So I changed onChange and getOptionLabel again:

const onChange = React.useCallback((event,item) => {
      setFieldValue(field.name, item);
    },
    [field.name]
);

```js
getOptionLabel = (option)=>option.label

Awesome, it works.  
I tried to click the submit button, I got a weird result. 
The result is the currently selected option object, not the value.
```js
 {
    value:"fans",
    label:" my fans "
}

I want to get the value instead of an object.
This has been bothering me. I expect to get a complete example.

@sessionboy Thank you for sharing your experience and your frustration. From what I understand you have found the solution. Regarding your issue with the value, if you want to use a different interface with Formik, you have to adapt it in the both ends of the spectrum (the value and the change event).

I'm moving the concern to #15585 as a global effort. For your very issue, head to https://github.com/stackworx/formik-material-ui.

In case someone is looking for a full working solution

import React from 'react';
import { Autocomplete } from '@material-ui/lab';
import { TextField } from '@material-ui/core';
import { fieldToTextField } from 'formik-material-ui';


const FormikAutocomplete = ({ textFieldProps, ...props }) => {

  const { form: { setTouched, setFieldValue } } = props;
  const { error, helperText, ...field } = fieldToTextField(props);
  const { name } = field;

  return (
    <Autocomplete
      {...props}
      {...field}
      onChange={ (_, value) => setFieldValue(name, value) }
      onBlur={ () => setTouched({ [name]: true }) }
      renderInput={ props => (
        <TextField {...props} {...textFieldProps} helperText={helperText} error={error} />
      )}
    />
  );
}

export default FormikAutocomplete;

Hi @keyvanm thanks for sharing your FormikAutocomplete component. Is there any chance that you could also share what you pass in as 'textFieldProps' and 'props' to the component? I am getting a "Cannot read property 'setTouched' of undefined" error. Thanks.

@TheBlinkOfAnEye

The way you want to use this component is to use it in conjunction with a formik <Field> component.

import { Field } from 'formik';

<Field name='owner' component={FormikAutocomplete} label="Owner"
  options={users}
  textFieldProps={{ fullWidth: true, margin: 'normal', variant: 'outlined' }}
/>

Formik Field will inject the rest of the props including form.setTouched into the component.

textFieldProps is any prop you can pass into a <TextField /> as per https://material-ui.com/api/text-field/

Thank you so much @keyvanm

Update:
Hi again @keyvanm. I am not in any way expecting you to 'do my home work' but when I run the code from the working solution you kindly supplied,
I get a "TypeError: Object(...) is not a function
that appears to happen on the line
const { error, helperText, ...field } = fieldToTextField(props);

and seems to be with the ...field spread.

Did you come across this? Just wondering if I am doing something obviously wrong? Thank you again for your help.

Update 2
Apologies, I was importing the component incorrectly.
I was importing like this:
import FormikAutocomplete from "./FormikAutocomplete";
when it needed to be imported like so:
import { FormikAutocomplete } from "./FormikAutocomplete";

Yea I couldn't get this working as presented above. I keep getting the error:

index.jsx:14 Uncaught TypeError: Cannot read property 'setTouched' of undefined

I had a couple issues with @keyvanm's solution

  1. Blurring the autocomplete would reset the touched status of all other fields. I fixed it by spreading the existing touched fields like this: onBlur={() => setTouched({ ...touched, [name!]: true })}

  2. This bug seems to be more a part of Material UI itself. I was using the freeSolo option but custom values weren't being propagated to formik. I had to add an event listener to onInputChange in addition to onChange.

Here is my modified version:

import { TextField, TextFieldProps } from "@material-ui/core";
import { Autocomplete, AutocompleteInputChangeReason, AutocompleteProps } from "@material-ui/lab";
import { FieldProps } from "formik";
import { fieldToTextField } from "formik-material-ui";
import React from "react";

const AnyAutocomplete = Autocomplete as any;

export interface FormikAutocompleteProps<V extends any = any, FormValues extends any = any>
  extends FieldProps<V, FormValues>,
    AutocompleteProps<V> {
  textFieldProps: TextFieldProps;
}

const noOp = () => {};

const FormikAutocomplete = <V extends any = any, FormValues extends any = any>({
  textFieldProps,
  ...props
}: FormikAutocompleteProps<V, FormValues>) => {
  const {
    form: { setTouched, setFieldValue, touched },
  } = props;
  const { error, helperText, ...field } = fieldToTextField(props as any);
  const { name } = field;
  const onInputChangeDefault = props.onInputChange ?? noOp;
  const onInputChange = !props.freeSolo
    ? props.onInputChange
    : (event: React.ChangeEvent<{}>, value: string, reason: AutocompleteInputChangeReason) => {
        setFieldValue(name!, value);
        onInputChangeDefault(event, value, reason);
      };

  return (
    <AnyAutocomplete
      {...props}
      {...field}
      onChange={(_, value) => setFieldValue(name!, value)}
      onInputChange={onInputChange}
      onBlur={() => setTouched({ ...touched, [name!]: true })}
      renderInput={(props) => <TextField {...props} {...textFieldProps} helperText={helperText} error={error} />}
    />
  );
};

export default FormikAutocomplete;

Hello! I feel like I'm really close! As per @keyvanm, I'm trying:

import React, { useState, useEffect } from 'react'
import { Formik, Field, Form, useField, FieldProps } from 'formik'
import { TextField, Select, MenuItem, FormControl } from '@material-ui/core'
import * as yup from 'yup'; 
import { Button, Row, Col } from 'reactstrap';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import Tooltip from '@material-ui/core/Tooltip';
import { Autocomplete } from '@material-ui/lab';
import { fieldToTextField } from 'formik-material-ui';

const FormikAutocomplete = ({ textFieldProps, ...props }) => {

  const { form: { setTouched, setFieldValue } } = props;
  const { error, helperText, ...field } = fieldToTextField(props);
  const { name } = field;

  return (
    <Autocomplete
      {...props}
      {...field}
      onChange={ (_, value) => setFieldValue(name, value) }
      onBlur={ () => setTouched({ [name]: true }) }
      renderInput={ props => (
        <TextField {...props} {...textFieldProps} 
        // helperText={helperText} error={error} 
        />
      )}
    />
  );
}

{({ values, errors, handleChange, isSubmitting }) => (
<div className='container'>
<Form >
<Field name='manufacturer' component={FormikAutocomplete} 
                    label="Manufacturer"
                    options={manufacturers}
                    textFieldProps={{ fullWidth: true, 
                    margin: 'normal', variant: 'outlined' }}
                  />

But I'm getting error:
_useAutocomplete.js:51 Uncaught TypeError: candidate.toLowerCase is not a function_

Hello! I feel like I'm really close! As per @keyvanm, I'm trying:

import React, { useState, useEffect } from 'react'
import { Formik, Field, Form, useField, FieldProps } from 'formik'
import { TextField, Select, MenuItem, FormControl } from '@material-ui/core'
import * as yup from 'yup'; 
import { Button, Row, Col } from 'reactstrap';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import Tooltip from '@material-ui/core/Tooltip';
import { Autocomplete } from '@material-ui/lab';
import { fieldToTextField } from 'formik-material-ui';

const FormikAutocomplete = ({ textFieldProps, ...props }) => {

  const { form: { setTouched, setFieldValue } } = props;
  const { error, helperText, ...field } = fieldToTextField(props);
  const { name } = field;

  return (
    <Autocomplete
      {...props}
      {...field}
      onChange={ (_, value) => setFieldValue(name, value) }
      onBlur={ () => setTouched({ [name]: true }) }
      renderInput={ props => (
        <TextField {...props} {...textFieldProps} 
        // helperText={helperText} error={error} 
        />
      )}
    />
  );
}

{({ values, errors, handleChange, isSubmitting }) => (
<div className='container'>
<Form >
<Field name='manufacturer' component={FormikAutocomplete} 
                    label="Manufacturer"
                    options={manufacturers}
                    textFieldProps={{ fullWidth: true, 
                    margin: 'normal', variant: 'outlined' }}
                  />

But I'm getting error:
_useAutocomplete.js:51 Uncaught TypeError: candidate.toLowerCase is not a function_

You need to pass getOptionLabel to resolve label. Here is the example

<Field name='manufacturer' component={FormikAutocomplete} 
                    label="Manufacturer"
                    options={manufacturers}
                    getOptionLabel={(option) => option.title}
                    textFieldProps={{ fullWidth: true, 
                    margin: 'normal', variant: 'outlined' }}
                  />

I had a couple issues with @keyvanm's solution

  1. Blurring the autocomplete would reset the touched status of all other fields. I fixed it by spreading the existing touched fields like this: onBlur={() => setTouched({ ...touched, [name!]: true })}
  2. This bug seems to be more a part of Material UI itself. I was using the freeSolo option but custom values weren't being propagated to formik. I had to add an event listener to onInputChange in addition to onChange.

Here is my modified version:

import { TextField, TextFieldProps } from "@material-ui/core";
import { Autocomplete, AutocompleteInputChangeReason, AutocompleteProps } from "@material-ui/lab";
import { FieldProps } from "formik";
import { fieldToTextField } from "formik-material-ui";
import React from "react";

const AnyAutocomplete = Autocomplete as any;

export interface FormikAutocompleteProps<V extends any = any, FormValues extends any = any>
  extends FieldProps<V, FormValues>,
    AutocompleteProps<V> {
  textFieldProps: TextFieldProps;
}

const noOp = () => {};

const FormikAutocomplete = <V extends any = any, FormValues extends any = any>({
  textFieldProps,
  ...props
}: FormikAutocompleteProps<V, FormValues>) => {
  const {
    form: { setTouched, setFieldValue, touched },
  } = props;
  const { error, helperText, ...field } = fieldToTextField(props as any);
  const { name } = field;
  const onInputChangeDefault = props.onInputChange ?? noOp;
  const onInputChange = !props.freeSolo
    ? props.onInputChange
    : (event: React.ChangeEvent<{}>, value: string, reason: AutocompleteInputChangeReason) => {
        setFieldValue(name!, value);
        onInputChangeDefault(event, value, reason);
      };

  return (
    <AnyAutocomplete
      {...props}
      {...field}
      onChange={(_, value) => setFieldValue(name!, value)}
      onInputChange={onInputChange}
      onBlur={() => setTouched({ ...touched, [name!]: true })}
      renderInput={(props) => <TextField {...props} {...textFieldProps} helperText={helperText} error={error} />}
    />
  );
};

export default FormikAutocomplete;

In that case you should use setFieldTouched API

Was this page helpful?
0 / 5 - 0 ratings

Related issues

finaiized picture finaiized  路  3Comments

anthony-dandrea picture anthony-dandrea  路  3Comments

newoga picture newoga  路  3Comments

ghost picture ghost  路  3Comments

ryanflorence picture ryanflorence  路  3Comments