Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Any example for autocomplete with formik #18331

Closed
sessionboy opened this issue Nov 12, 2019 · 13 comments
Closed

Any example for autocomplete with formik #18331

sessionboy opened this issue Nov 12, 2019 · 13 comments
Labels
component: autocomplete This is the name of the generic UI component, not the React module! discussion

Comments

@sessionboy
Copy link

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

@oliviertassinari
Copy link
Member

@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.

@sessionboy
Copy link
Author

@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]
);
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.

 {
    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.

@oliviertassinari
Copy link
Member

@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.

@keyvanm
Copy link
Contributor

keyvanm commented Dec 31, 2019

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;

@TheBlinkOfAnEye
Copy link

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.

@keyvanm
Copy link
Contributor

keyvanm commented Jan 22, 2020

@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/

@TheBlinkOfAnEye
Copy link

TheBlinkOfAnEye commented Jan 22, 2020

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";

@simkessy
Copy link

simkessy commented Feb 10, 2020

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

@sparebytes
Copy link

sparebytes commented Mar 24, 2020

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;

@bWgibb
Copy link

bWgibb commented Apr 5, 2020

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

@hpatel-git
Copy link

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' }}
                  />

@SahilChaudhary25
Copy link

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

@oliviertassinari oliviertassinari added the component: autocomplete This is the name of the generic UI component, not the React module! label Apr 24, 2020
@oliviertassinari
Copy link
Member

Let's move the discussion to stackworx/formik-mui#126

@mui mui locked as resolved and limited conversation to collaborators Apr 24, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
component: autocomplete This is the name of the generic UI component, not the React module! discussion
Projects
None yet
Development

No branches or pull requests

9 participants