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
@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
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 })}
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
- Blurring the autocomplete would reset the
touched
status of all other fields. I fixed it by spreading the existingtouched
fields like this:onBlur={() => setTouched({ ...touched, [name!]: true })}
- 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 toonInputChange
in addition toonChange
.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
Let's move the discussion to https://github.com/stackworx/formik-material-ui/issues/126
Most helpful comment
In case someone is looking for a full working solution