Using Material-UI's Autocomplete component with Formik - reactjs

Currently trying to use Material UI's Autocomplete component with Formik. So far things like text fields and traditional selects from Material-UI play very nice with Formik. Implementing Autocomplete is not the case. Formik's onChange handler doesn't seem to update the value for my city_id. I know Autocomplete is still not apart of Material-UI's core library but was still seeing if something like this was a possibility at the moment.
import React from "react";
import ReactDOM from "react-dom";
import { Formik, Form } from 'formik';
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import Button from '#material-ui/core/Button';
import { cities } from '../data/cities';
import "./styles.css";
const initialValues = {
city_id: '',
};
const submit = params => {
alert(`Value for city_id is: ${params.city_id}`);
};
function App() {
return (
<Formik
initialValues={ initialValues }
onSubmit={ submit }
>
{({
handleChange,
values,
}) => (
<Form>
<Autocomplete
id="city_id"
name="city_id"
options={ cities }
groupBy={ option => option.state }
getOptionLabel={ option => option.name }
style={{ width: 300 }}
renderInput={params => (
<TextField
{ ...params }
onChange={ handleChange }
margin="normal"
label="Cities"
fullWidth
value={ values.city_id }
/>
)}
/>
<Button
variant="contained"
color="primary"
type="submit"
>
Submit
</Button>
</Form>
)}
</Formik>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Your problem is that handleChange won't work the way you are doing.
If you take a look at the handleChange docs:
General input change event handler. This will update the values[key] where key is the event-emitting input's name attribute. If the name attribute is not present, handleChange will look for an input's id attribute. Note: "input" here means all HTML inputs.
Which should work fine, but the problem is that the TextField inside Autocomplete will only trigger handleChange when you type something on it, and the value will be the text, not the id or other property you want, so you need to move handleChange to the Autocomplete.
And there is another problem, you can't use handleChange in the Autocomplete because it doesn't references the input you want and it also have different parameters from the normal onChange of the input, as you can see in the docs.
onChange
func
Callback fired when the value changes.
Signature:
function(event: object, value: any) => void
event: The event source of the callback
value: null
So what you need to do is use setFieldValue and pass it to Autocomplete like
onChange={(e, value) => setFieldValue("city_id", value)}
You need to pass the name of your field and what value you want to get.
Here is a working example

#vencovsky has provided the correct answer that is still working for me with Material UI 14.10.1.
I'm adding a bit more to it as I have my field set to required in using Yup validation.
To get this to work correctly I have the following:
Yup config:
validationSchema = {
Yup.object().shape({
contact: Yup.string().max(255).required('Contact is required'),
})
}
react:
<Autocomplete
id="contact-autocomplete"
options={contacts}
getOptionLabel={(contact) => `${contact?.firstName} ${contact?.lastName}`}
onChange={(e, value) => setFieldValue("contact", value?.id || "")}
onOpen={handleBlur}
includeInputInList
renderInput={(params) => (
<TextField
{...params}
error={Boolean(touched.contact && errors.contact)}
fullWidth
helperText={touched.contact && errors.contact}
label="Contact Person"
name="contact"
variant="outlined"
/>
)}
/>
When the user click on the Autocomplete element, it fires the onOpen which runs the Formik onBlur and marks the field as touched. If an item is then not picked, Formik flags the field and displays the Contact is required validation message.

You have to add onChange = {(event, value) => handleChange(value)} in Autocomplete tag as
import React from "react";
import ReactDOM from "react-dom";
import { Formik, Form } from 'formik';
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import Button from '#material-ui/core/Button';
import { cities } from '../data/cities';
import "./styles.css";
const [cityId,setCityId]=React.useState({city_id:''});
const handleChange=(value)=>{
// Here is the value is a selected option label or the new typed value
setCityId({city_id:value});
}
function App() {
return (
<Formik
initialValues={ cityId }
onSubmit={() => {
alert(`Value for city_id is: ${cityId.city_id}`);
}}
>
{({
handleChange,
values,
}) => (
<Form>
<Autocomplete
id="city_id"
name="city_id"
options={ cities }
groupBy={ option => option.state }
getOptionLabel={ option => option.name }
style={{ width: 300 }}
onChange = {(event, value) => handleChange(value)}
renderInput={params => (
<TextField
{ ...params }
onChange={ handleChange }
margin="normal"
label="Cities"
fullWidth
value={ values.city_id }
/>
)}
/>
<Button
variant="contained"
color="primary"
type="submit"
>
Submit
</Button>
</Form>
)}
</Formik>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
If onChange don't work you can use onInputChange as well.

I had the same issue recently and solved. Sharing my experience
Updating formik values directly on OnChange method solved the issue
onChange={(event, value) => (formik.values.country = value!)}
Here is the full code
Formik settings
const formik = useFormik({
initialValues: {
id: user.id || "",
name: user.name || "",
country: user.country,
email: user.email || "",
submit: null,
},
validationSchema: Yup.object({
email: Yup.string()
.email("Must be a valid email")
.max(255)
.required("Email is required"),
name: Yup.string().max(255).required("Name is required"),
}),
onSubmit: async (values, helpers): Promise<void> => {
console.log("Updating user...");
try {
let userData: UserDetails = {
id: values.id,
email: values.email,
name: values.name,
country: values.country,
};
await userApi.registerUser(userData);
helpers.setStatus({ success: true });
helpers.setSubmitting(false);
toast.success("User updated!");
} catch (err) {
console.error(err);
toast.error("Something went wrong!");
helpers.setStatus({ success: false });
helpers.setErrors({ submit: err.message });
helpers.setSubmitting(false);
}
},
});
Autocomplete
<Autocomplete
getOptionLabel={(option): string => option.text}
options={countries}
value={formik.values.country}
defaultValue={formik.values.country}
onChange={(event, value) => (formik.values.country = value!)}
renderInput={(params): JSX.Element => (
<TextField
{...params}
fullWidth
label="Country"
name="country"
error={Boolean(
formik.touched.country && formik.errors.country
)}
helperText={formik.touched.country && formik.errors.country}
onBlur={formik.handleBlur}
/>
)}
/>

Related

Issue in validation in react autocomplete in multiple select using formik

I am trying to validate the autocomplete multiple select using the formik. First time it is working on button click but when I remove the selected value then it is not validation.
import React from "react";
import ReactDOM from "react-dom";
import { Formik, Form } from "formik";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import Button from "#material-ui/core/Button";
import * as Yup from 'yup';
const cities = [{
state: "Illinois",
name: "Chicago",
id: 3,
}, {
state: "Texas",
name: "Houston",
id: 2
}, {
state: "California",
name: "Los Angeles",
id: 1
}, {
state: "New York",
name: "New York City",
id: 4
}];
const initialValues = {
city_id: '',
};
const submit = values => {
let city=JSON.parse(values.city_id);
};
const SignupForm = () => {
return (
<Formik initialValues={initialValues}
validationSchema = {
Yup.object().shape({
city_id: Yup.string().max(255).required('City is required')
})
}
onSubmit={submit}>
{({ handleChange,touched,errors, values, setFieldValue }) => (
<Form>
<Autocomplete
multiple
id="city_id"
name="city_id"
options={cities}
getOptionLabel={option => option.name}
style={{ width: 300 }}
onChange={(e, value) => {
console.log(value);
let val=JSON.stringify(value);
setFieldValue(
"city_id",
value !== null ? val : initialValues.city_id
);
}}
renderInput={params => (
<TextField
error={Boolean(touched.city_id && errors.city_id)}
helperText={touched.city_id && errors.city_id}
margin="normal"
label="Cities"
fullWidth
name="city_id"
{...params}
/>
)}
/>
<Button variant="contained" color="primary" type="submit">
Submit
</Button>
</Form>
)}
</Formik>
);
}
export default SignupForm;
Please provide better validation option if I have made mistake.
Validation will not run because value is never empty.
If you want to run validation, do:
setFieldValue(
"city_id",
value
);
above code will trigger validation because value will be "" when you remove.

How to display initialValues for material-ui autocomplete field?

I use the autocomplete field of material-ui (v5) and formik to generate my forms.
On this form, I have some lists defined as constants.
I use an api to get the default value of this list.
This api returns only the "code" of the option but not its label.
<Formik
enableReinitialize
initialValues={initialFormValues}
validationSchema={Yup.object().shape({
[...]
<Autocomplete
error={Boolean(touched.civility && errors.civility)}
helperText={touched.civility && errors.civility}
label="Civility"
margin="normal"
name="civility"
onBlur={handleBlur}
onChange={(e, value) => setFieldValue('civility', value)}
options={civilities}
value={values.civility}
getOptionLabel={(option) =>
option.name ? option.name : ''
}
isOptionEqualToValue={(option, value) => option.code === value}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label={<Trans>Civility</Trans>}
/>
)}
/>
My parameter isOptionEqualToValue is good because even if the value is not displayed in the input, it is well selected in the list.
You can see that the input text field is empty:
But if I unroll the list, I can see that my "ms" value has been selected:
What should I do to make the input text contain the default value?
After cloned your snippet code above, the problem was in getOptionLabel, the option argument is a string value, so it hasn't a name property and appears empty string. Here is an online example codesandbox.
import { useState } from "react";
import { Formik, Form } from "formik";
import Autocomplete from "#material-ui/lab/Autocomplete";
import TextField from "#material-ui/core/TextField";
export default function App() {
const civilities = ["Mr", "Ms", "Other"];
const [values, setValues] = useState({
civility: "Ms"
});
const handleBlur = (e) => {
console.log("Blur:", e.target.value);
};
const setFieldValue = (type, value) => {
setValues((oldValues) => ({ ...oldValues, [type]: value }));
};
return (
<Formik err>
{({ errors, touched }) => (
<Form>
<Autocomplete
error={Boolean(touched.civility && errors.civility)}
helperText={touched.civility && errors.civility}
label="Civility"
margin="normal"
name="civility"
onBlur={handleBlur}
onChange={(e, value) => setFieldValue("civility", value)}
options={civilities}
value={values.civility}
isOptionEqualToValue={(option, value) => option.code === value}
renderInput={(params) => (
<TextField {...params} variant="outlined" label="Civility" />
)}
/>
</Form>
)}
</Formik>
);
}

Material-UI AutoComplete freeSolo in Form

https://codesandbox.io/s/naughty-bogdan-hvhsd?file=/src/searchfield.tsx
As you can see in this SandBox, I'm using Material AutoComplete as a multiple input with free options. The component should return to Formik ["term1","term2","term3"] and the user can see each string as a label in a Chip. This will be used as filters in a search.
This all happens, but only if the input is already in InitialValues. If the user does an input manually and press enter or tab it craches in a error "value.map" is not a function.
The error points to this line in the material autocomplete component code
"getInputProps = _useAutocomplete.getInputProps,"
Does anyone has any ideia on how to make AutoComplete and Forms work together?
Something like this (searchfield.tsx):
import React, { useState } from "react";
import { TextField } from "#material-ui/core";
import { Formik, Form } from "formik";
import Autocomplete from "#material-ui/lab/Autocomplete";
export default function SearchBar() {
const [searchValues] = useState(["ola", "como"]);
return (
<Formik
initialValues={{
search: []
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
JSON.stringify(values, null, 2);
console.log(values);
setSubmitting(false);
}, 400);
}}
>
{({ values, handleChange, handleBlur, handleSubmit }) => (
<Form onSubmit={handleSubmit}>
<Autocomplete
autoSelect
freeSolo
id="search"
limitTags={4}
multiple
onBlur={handleBlur}
onChange={handleChange}
options={searchValues}
getOptionLabel={option => option}
filterSelectedOptions
renderInput={params => (
<TextField
{...params}
id="search"
name="search"
variant="outlined"
label="Busca"
placeholder="Termos de Busca"
/>
)}
/>
<h6>{searchValues}</h6>
</Form>
)}
</Formik>
);
}

React phone input 2 with react hook form

I am using PhoneInput along with react hook form, I want to enable save button only if phone number is valid
Code:
<form onSubmit={handleSubmit(onSubmitRequest)}>
.....................other code..............
<Controller
as={
<PhoneInput
id="pNum"
placeholder="Enter phone number"
className={classes.phoneInput}
inputRef={register({required: true})}
isValid={(inputNumber, onlyCountries) => {
return onlyCountries.some((country) => {
return startsWith(inputNumber, country.dialCode) || startsWith(country.dialCode, inputNumber);
});
}}
/>
}
name="phoneNumber"
control={control}
/>
........................other code...................
<Button
fullWidth
type="submit"
variant="contained"
color={'primary'}
className={classes.submitBtn}
data-testid="customerFormButton"
disabled={!formState.isValid}
>
Save
</Button>
</form>
Here I used PhoneInput as controller along with isValid for it. How can I disable Save button for invalid phone number input?
How are you? I believe that your problem is because you are not configuring the rules for the controller.
You need to change your controller to something like this:
<Controller
as={
<PhoneInput
id="pNum"
placeholder="Enter phone number"
className={classes.phoneInput}
inputRef={register}
isValid={(inputNumber, onlyCountries) => {
return onlyCountries.some((country) => {
return startsWith(inputNumber, country.dialCode) || startsWith(country.dialCode, inputNumber);
});
}}
/>
}
name="phoneNumber"
control={control}
rules= {{required: true}}
/>
ref cannot be currently used on this element. react-phone-input-2.
Until its supported, you can provide a hidden input field which updates its value when the phone updates its value and put the ref on that
Example:
import React, { FC, useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import PhoneInput from 'react-phone-input-2';
import 'react-phone-input-2/lib/style.css';
interface Props {
handleChange: (name: string, val: string) => void;
defaultValue: string;
name: string;
}
const MyComponent: FC<Props> = ({ defaultValue, name, handleChange }) => {
const { register, setValue, watch } = useFormContext(); // Note: needs <FormProvider> in parent for this to be acessible
const nameHidden = `${name}Hidden`;
const handleChangePhone = useCallback(
(val: string) => {
setValue(nameHidden, val, { shouldValidate: true });
handleChange(name, val);
},
[handleChange]
);
return (
<>
<PhoneInput value={defaultValue as string} country="gb" onChange={handleChangePhone} />
<input
type="hidden"
name={nameHidden}
defaultValue={defaultValue}
ref={register({
// validate stuff here...
})}
/>
</>
);
};
export default MyComponent;

Formik Material UI and react testing library

I am finding it hard to work with react testing library and understand the queries I need to use in order to select the components I need to test. The queries are too simplistic when the DOM gets more and more verbose with frameworks like material ui and formik.
I have created a code sandbox to illustrate the problem. You can check the failing test there.
https://codesandbox.io/embed/px277lj1x
The issue I am getting is, the query getLabelTextBy() does not return the component. It looks like the aria label by or the for attribute is not being rendered by material ui. Not sure how to fix this error.
The code is below here for reference too
//Subject under test
import React from "react";
import { Button } from "#material-ui/core";
import { TextField } from "formik-material-ui";
import { Field, Form, Formik } from "formik";
import * as yup from "yup";
const validationSchema = yup.object().shape({
name: yup.string().required("Office name is required"),
address: yup.string().required("Address is required")
});
export default () => (
<Formik
initialValues={{
name: "",
address: ""
}}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(false);
console.log("form is submitted with", values);
}}
render={({ submitForm, isSubmitting, isValid }) => (
<Form>
<Field
label="Office Name"
name="name"
required
type="text"
component={TextField}
/>
<Field
label="Address Line 1"
name="addressLine1"
type="text"
component={TextField}
/>
<Button
variant="contained"
color="primary"
fullWidth={false}
size="medium"
disabled={isSubmitting || !isValid}
onClick={submitForm}
data-testid="submitButton"
>
Submit
</Button>
</Form>
)}
/>
);
// Test
import React from "react";
import { render, fireEvent } from "react-testing-library";
import App from "./App";
describe("Create Office Form tests", () => {
it("button should not be disabled when all required fields are filled up", () => {
const { getByLabelText, getByTestId, debug } = render(<App />);
const values = {
"office name": "office",
address: "address 1"
};
for (const key in values) {
const input = getByLabelText(key, { exact: false, selector: "input" });
fireEvent.change(input, { target: { value: values[key] } });
}
const button = getByTestId("submitButton");
expect(button.disabled).not.toBeDefined();
});
});
You must add an id to your Field because the label's for attribute expect the ID of the input element it refers to:
<Field
id="myName"
label="Office Name"
name="name"
required
type="text"
component={TextField}
/>
A working example:
Few things. The use of { getByLabelText, getByTestId, debug } is not advised by the creator of the library. You should use screen.getByLabelText().
If there is a change, it's possible that it doesn't wait for the new render, so it would be better to await it or wrap the expect in a waitFor method.
Also, expect(button.disabled).not.toBeDefined() should not be correct. I think you're just checking if the button is disabled or not, right?
So you can use expect(button).toBeDisabled() or not.toBeDisabled()
At least to test it I think you should take off the for loop and check it normally. You can use screen.debug(component) to have the HTML shown after some action.

Resources