Downshift autocomplete onBlur resetting value with Formik - reactjs

I have a form with a field that needs to show suggestions via an api call. The user should be allowed to select one of those options or not and that value that they type in gets used to submit with the form, but this field is required. I am using Formik to handle the form, Yup for form validation to check if this field is required, downshift for the autocomplete, and Material-UI for the field.
The issue comes in when a user decides not to use one of the suggested options and the onBlur triggers. The onBlur always resets the field and I believe this is Downshift causing this behavior but the solutions to this problem suggest controlling the state of Downshift and when I try that it doesn't work well with Formik and Yup and there are some issues that I can't really understand since these components control the inputValue of this field.
Heres what I have so far:
const AddBuildingForm = props => {
const [suggestions, setSuggestions] = useState([]);
const { values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
modalLoading,
setFieldValue,
setFieldTouched,
classes } = props;
const loadOptions = (inputValue) => {
if(inputValue && inputValue.length >= 3 && inputValue.trim() !== "")
{
console.log('send api request', inputValue)
LocationsAPI.autoComplete(inputValue).then(response => {
let options = response.data.map(erlTO => {
return {
label: erlTO.address.normalizedAddress,
value: erlTO.address.normalizedAddress
}
});
setSuggestions(options);
});
}
setFieldValue('address', inputValue); // update formik address value
}
const handleSelectChange = (selectedItem) => {
setFieldValue('address', selectedItem.value); // update formik address value
}
const handleOnBlur = (e) => {
e.preventDefault();
setFieldValue('address', e.target.value);
setFieldTouched('address', true, true);
}
const handleStateChange = changes => {
if (changes.hasOwnProperty('selectedItem')) {
setFieldValue('address', changes.selectedItem)
} else if (changes.hasOwnProperty('inputValue')) {
setFieldValue('address', changes.inputValue);
}
}
return (
<form onSubmit={handleSubmit} autoComplete="off">
{modalLoading && <LinearProgress/>}
<TextField
id="name"
label="*Name"
margin="normal"
name="name"
type="name"
onChange={handleChange}
value={values.name}
onBlur={handleBlur}
disabled={modalLoading}
fullWidth={true}
error={touched.name && Boolean(errors.name)}
helperText={touched.name ? errors.name : ""}/>
<br/>
<Downshift id="address-autocomplete"
onInputValueChange={loadOptions}
onChange={handleSelectChange}
itemToString={item => item ? item.value : '' }
onStateChange={handleStateChange}
>
{({
getInputProps,
getItemProps,
getMenuProps,
highlightedIndex,
inputValue,
isOpen,
}) => (
<div>
<TextField
id="address"
label="*Address"
name="address"
type="address"
className={classes.autoCompleteOptions}
{...getInputProps( {onBlur: handleOnBlur})}
disabled={modalLoading}
error={touched.address && Boolean(errors.address)}
helperText={touched.address ? errors.address : ""}/>
<div {...getMenuProps()}>
{isOpen ? (
<Paper className={classes.paper} square>
{suggestions.map((suggestion, index) =>
<MenuItem {...getItemProps({item:suggestion, index, key:suggestion.label})} component="div" >
{suggestion.value}
</MenuItem>
)}
</Paper>
) : null}
</div>
</div>
)}
</Downshift>
<Grid container direction="column" justify="center" alignItems="center">
<Button id="saveBtn"
type="submit"
disabled={modalLoading}
className = {classes.btn}
color="primary"
variant="contained">Save</Button>
</Grid>
</form>
);
}
const AddBuildingModal = props => {
const { modalLoading, classes, autoComplete, autoCompleteOptions } = props;
return(
<Formik
initialValues={{
name: '',
address: '',
}}
validationSchema={validationSchema}
onSubmit = {
(values) => {
values.parentId = props.companyId;
props.submitAddBuildingForm(values);
}
}
render={formikProps => <AddBuildingForm
autoCompleteOptions={autoCompleteOptions}
autoComplete={autoComplete}
classes={classes}
modalLoading={modalLoading}
{...formikProps} />}
/>
);
}

Got it to work. Needed to use handleOuterClick and set the Downshift state with the Formik value:
const handleOuterClick = (state) => {
// Set downshift state to the updated formik value from handleOnBlur
state.setState({
inputValue: values.address
})
}
Now the value stays in the input field whenever I click out.

Related

Yup validation on array of checkbox

I want to change the state of the checkbox through useForm and I have an array of checkbox. the number of checkbox I get from the server. See my code please i have selectionAll and when it is true then all other checkbox should be true . And when it is false, then all the rest should be false. But it doesn’t work for me Look at what the problem is And what I wrote wrong.
but when i refresh the page the checkbox state is always undefined
type OperationSelect = {
selections: boolean;
};
export type OperationsData = {
selectionAll: boolean;
selection: OperationSelect[];
};
const defaultValues = {
selectionAll: false,
selections: [false],
};
export const DrawerEditFinancial: React.FC = () => {
const {
handleSubmit,
control,
reset,
watch,
setValue,
getValues,
formState: { errors, isValid },
} = useForm<OperationsData>({ defaultValues, resolver: yupResolver(schema), mode: 'onSubmit' });
const money = watch('sum');
const selectionAll = watch('selectionAll');
const selections = watch('selection');
I wrote the conditions here when selectionAll is true then the rest of the checkbox is true
useEffect(() => {
if (selectionAll) {
selections?.map((selection, index) => {
setValue(`selections.${index}`, true);
});
}
}, [selections, selectionAll]);
return (
<form>
<Grid item xs={0.3}>
<Controller
control={control}
name={'selectionAll'}
render={({ field: { value, onChange } }) => (
<FormControlLabel
value={value}
onChange={(e, checked) => onChange(checked)}
css={labelTextCSS}
control={<Checkbox />}
label=""
/>
)}
/>
</Grid>
{operationsView?.map((operation, index) => (
<Accordion
key={operation.expenseId}
expanded={expanded === `panel_${operation.expenseId}`}
onChange={handleChangeExpanded(`panel_${operation.expenseId}`)}
>
<AccordionSummary expandIcon={<SvgArrowFinancial />}>
<Grid css={pl(12)} container justifyContent="flex-start" alignItems="center">
<Grid item xs={0.3}>
<Controller
control={control}
name={`selections.${index}`}
render={({ field: { value, onChange } }) => (
<FormControlLabel
value={value}
onChange={(e, checked) => onChange(checked)}
css={labelTextCSS}
control={<Checkbox />}
label=""
/>
)}
/>
</Grid>
</Accordion
))}
</form>
)

React Formik Mui TextField with custom input loosing focus

I have a Formik form which contains a few Material UI TextField components. All of them works fine, except for one who looses focus when onChange() is executed. It has a PhoneInput (react-phone-number-input) component as a custom input.
Everything works fine (data is being saved, etc.), but I have to keep clicking on the input to continue writing on it.
I have a feeling it has something to do with the ref. When logging inputRef (PhoneInputRef, provided by TextField) it doesn't look like a normal ref.
Formik component (Parent)
{...}
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleClickRegister}>
{(props) => <RegisterForm {...props} />}
</Formik>
{...}
Form (Children)
{...}
const {
values: {
firstName,
lastName,
email,
confirmEmail,
password,
confirmPassword,
phoneNumber,
},
//Formik methods passed as props
errors,
touched,
handleChange,
handleSubmit,
setFieldValue,
setFieldTouched,
} = props;
{...}
//Formik onChange handler (used by every TextField besides the one containing PhoneInput)
const change = (name, e) => {
e.persist();
handleChange(e);
setFieldTouched(name, true, false);
};
//Custom onChange handler for Formik (used by PhoneInput)
const handleOnChange = (name, value) => {
setFieldValue(name, value);
setFieldTouched(name, true, false);
};
{...}
{/*NORMAL TEXTFIELD*/}
<TextField
className={classes.inputField}
id="confirmPassword"
name="confirmPassword"
label={t("header.registerDialog.form.labelConfirmPassword")}
value={confirmPassword}
onChange={change.bind(null, "confirmPassword")}
error={touched.confirmPassword && Boolean(errors.confirmPassword)}
helperText={
touched.confirmPassword && errors.confirmPassword ? (
errors.confirmPassword
) : (
<>
<br />
</>
)
}
/>
{/*CUSTOM INPUT TEXTFIELD*/}
<TextField
className={classes.textFieldPhoneInput}
key="textFieldPhone"
ref={textFieldRef}
id="phoneNumber"
name="phoneNumber"
label={t("header.registerDialog.form.labelPhone")}
InputLabelProps={{
className: phoneNumber
? classes.inputFieldPhoneLabel
: classes.inputFieldPhoneLabelEmpty,
}}
InputProps={{
inputComponent: ({ inputRef, ...rest }) => (
<PhoneInput
{...rest}
key="phoneInput"
ref={inputRef}
international
countryCallingCodeEditable={false}
name="phoneNumber"
defaultCountry="AR"
onChange={(phone) =>
handleOnChange("phoneNumber", phone)
}
labels={phoneLanguage}
value={phoneNumber}
/>
),
}}
error={touched.phoneNumber && Boolean(errors.phoneNumber)}
helperText={
touched.phoneNumber && errors.phoneNumber ? (
errors.phoneNumber
) : (
<>
<br />
</>
)
}
/>
{...}

Using Material UI's Autocomplete using Formik to display different value in dropdown but set different value in formik state

I am trying to use Material UI's Autocomplete with Formik. Here is a custom Autocomplete component I wrote to use with Formik.
import React from "react";
import Autocomplete from "#material-ui/lab/Autocomplete";
import TextField from "#material-ui/core/TextField";
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;
Here is how components callled
<Field
name="title"
component={FormikAutocomplete}
options={gender}
getOptionLabel={(option) => option.title_name_long}
textFieldProps={{
label: "Title",
required: true,
variant: "outlined",
margin: "dense",
}}
/>
Now what I intend to do is: If I have a object like
gender=[{gender_name_short:"F",gender_name_long:"Female},{gender_name_short:"M",gender_name_long:"Male}]
I want the Autocomplete dropdown to show options male,female.
But I want the formik state to save M,F respectively once selected from dropdown. Currently the entire object gets saved.
How can this be done?
In FormikAutocomplete component,
use setFieldValue in the onChange of autocomplete
use gender_name_long in getOptionLabel to display Male , Female
use gender_name_short in getOptionSelected to use M or F
With that, finally, when you submit, you will get to see M/F not Male/Female
Working demo
const gender = [
{ gender_name_short: "F", gender_name_long: "Female" },
{ gender_name_short: "M", gender_name_long: "Male" }
];
const validationSchema = object().shape({
// genderObj: array().required("At least one gender is required")
});
const initialValues = {
username: "abc",
country: "",
gender: "M",
birthdate: null
};
const FormikAutocomplete = ({ textFieldProps, ...props }) => {
const {
form: { setTouched, setFieldValue }
} = props;
const { error, helperText, ...field } = fieldToTextField(props);
const { name } = field;
return (
<Autocomplete
{...field}
{...props}
onChange={(_, data) => {
setFieldValue("gender", data.gender_name_short);
}}
onBlur={() => setTouched({ [name]: true })}
// getOptionLabel={item => item.gender_name_long} //<----see here
getOptionLabel={item => {
// console.log( '====>' , typeof item === "string" ? props.options.find(i => i.gender_name_short === item).gender_name_long : item.gender_name_long)
return typeof item === "string"
? props.options.find(i => i.gender_name_short === item)
.gender_name_long
: item.gender_name_long;
}}
// getOptionLabel={item => typeof item === 'string' ? props.options.find(i => i.gender_name_short === item).gender_name_long : item.gender_name_long}
getOptionSelected={(item, current) => {
return item.gender_name_short === current;
}}
// defaultValue={'hi'}
renderInput={props => (
<TextField
{...props}
{...textFieldProps}
helperText={helperText}
error={error}
/>
)}
/>
);
};
const SimpleFormExample = () => (
<div>
<h1>Simple Form Example</h1>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
validateOnBlur={false}
validateOnChange
onSubmit={(values, { resetForm, setSubmitting }) => {
console.log(values);
resetForm();
// setSubmitting(false);
}}
>
{formik => (
<Form>
<Field
name="gender"
component={FormikAutocomplete}
label="gender"
options={gender}
textFieldProps={{
fullWidth: true,
margin: "normal",
variant: "outlined"
}}
// multiple
/>
<button type="submit">Submit</button>
</Form>
)}
</Formik>
</div>
);
export default SimpleFormExample;

How do I access current value of a formik field without submitting?

How do I access value of the SelectField named countryCode in my React component? Use case is that validation scheme should change according to the countryCode.
<Formik
onSubmit={(values, actions) => this.onSubmit(values, actions.setFieldError)}
validationSchema={() => this.registrationValidationSchema()}
enableReinitialize={true}
initialValues={this.props.initialData}
>
<Form>
<Field
name="countryCode"
component={SelectField}
label="Country"
labelClassName="required"
options={Object.entries(sortedCountryList).map(x => ({
value: x[1][1],
label: x[1][0]
}))}
/>
</Form>
</Formik>
I have tried to access it via a ref, then via this.props.values (as suggested in getFieldValue or similar in Formik) but both return just undefined or null. My props don't have any "values" field.
EDIT: Found an ugly way: document.getElementsByName("countryCode")[0].value. A better way is appreciated.
You can use ref, if you need the values outside formik
const ref = useRef(null);
const someFuncton = () => {
console.log(ref.current.values)
}
<Formik
innerRef={ref}
onSubmit={(values, actions) => this.onSubmit(values,
actions.setFieldError)}
validationSchema={() => this.registrationValidationSchema()}
enableReinitialize={true}
initialValues={this.props.initialData}
/>
<form></form>
</Formik>
You can access values like this:
<Formik
onSubmit={(values, actions) => this.onSubmit(values,
actions.setFieldError)}
validationSchema={() => this.registrationValidationSchema()}
enableReinitialize={true}
initialValues={this.props.initialData}
>
{({
setFieldValue,
setFieldTouched,
values,
errors,
touched,
}) => (
<Form className="av-tooltip tooltip-label-right">
// here you can access to values
{this.outsideVariable = values.countryCode}
</Form>
)}
</Formik>
you can get it from formik using the Field comp as a wrapper
import React, { ReactNode } from 'react';
import { Field, FieldProps } from 'formik';
(...other stuffs)
const CustomField = ({
field,
form,
...props
}) => {
const currentError = form.errors[field.name];
const currentField = field.name; <------ THIS
const handleChange = (value) => {
const formattedDate = formatISODate(value);
form.setFieldValue(field.name, formattedDate, true);
};
const handleError = (error: ReactNode) => {
if (error !== currentError) {
form.setFieldError(field.name, `${error}`);
}
};
return (
<TextField
name={field.name}
value={field.value}
variant="outlined"
helperText={currentError || 'happy helper text here'}
error={Boolean(currentError)}
onError={handleError}
onChange={handleChange}
InputLabelProps={{
shrink: true,
}}
inputProps={{
'data-testid': `${field.name}-test`, <---- very helpful for testing
}}
{...props}
/>
</MuiPickersUtilsProvider>
);
};
export default function FormikTextField({ name, ...props }) {
return <Field variant="outlined" name={name} component={CustomField} fullWidth {...props} />;
}
it is very simple just do console.log(formik.values) and you will get all the values without submitting it.

React-datepicker with a Formik form

I'm trying to use react-datepicker in a Formik form.
I have:
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
class Fuate extends React.Component {
state = {
dueDate: new Date()
}
<Formik
initialValues={initialValues}
validationSchema={Yup.object().shape({
title: Yup.string().required("A title is required "),
})}
onSubmit={this.handleSubmit}
render={({
errors,
status,
touched,
setFieldValue,
setFieldTouched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
dirty,
values
}) => {
return (
<div>
...
<DatePicker
name={'dueDate'}
value={values['dueDate']}
onChange={e => setFieldValue('dueDate', e)}
/>
<DatePicker
style={{ width: 180 }}
date={values.dueDate}
mode="date"
format="YYYY-MM-DD"
minDate={Date.now.toString()}
maxDate="2050-06-01"
confirmBtnText="Confirm"
cancelBtnText="Cancel"
showIcon={false}
customStyles={{
dateInput: {
marginLeft: 0,
borderColor: "#fff"
}
}}
onDateChange={date => setFieldValue("dueDate", date)}
onTouch={setFieldTouched}
/>
For both of these options, the form renders, I can select a date on the calendar but it does not appear in the box and the state value is not updated with the selection.
There are no errors in the console, but the warning says:
Starting with v2.0.0-beta.1 date-fns doesn't accept strings as
arguments. Please use parseISO to parse strings. See:
toDate # index.js:45
I tried making the initial state:
dueDate: new Date().toISOString(),
but it makes no difference.
I've seen lots of posts about setting this up with Antd's date picker, but can't find instructions for how to do it with react-datepicker.
Update to Dani Vijay's answer.
This uses useField and useFormikContext from Formik v2, to simplify usage of the component.
DatePicker.jsx:
import React from "react";
import { useField, useFormikContext } from "formik";
import DatePicker from "react-datepicker";
export const DatePickerField = ({ ...props }) => {
const { setFieldValue } = useFormikContext();
const [field] = useField(props);
return (
<DatePicker
{...field}
{...props}
selected={(field.value && new Date(field.value)) || null}
onChange={val => {
setFieldValue(field.name, val);
}}
/>
);
};
Usage (see Dani's answer for the complete form declaration):
...
<DatePickerField name="date" />
...
Code at codesandbox
react-datepicker can used with Formik by utilising setFieldValue,
const DatePickerField = ({ name, value, onChange }) => {
return (
<DatePicker
selected={(value && new Date(value)) || null}
onChange={val => {
onChange(name, val);
}}
/>
);
};
const App = () => (
<Formik
initialValues={{ date: "" }}
...
>
{props => {
const {
values,
handleSubmit,
setFieldValue
...
} = props;
return (
<form onSubmit={handleSubmit}>
<DatePickerField
name="date"
value={values.date}
onChange={setFieldValue}
/>
...
CodeSandbox demo here
Update to Dani Vijay and ToolmakerSteve's answers:
Use setValue directly instead of setFieldValue. Based on #asologor's comment.
import React from "react";
import { useField } from "formik";
import DatePicker from "react-datepicker";
export const DatePickerField = ({ ...props }) => {
const [field, , { setValue }] = useField(props);
return (
<DatePicker
{...field}
{...props}
selected={(field.value && new Date(field.value)) || null}
onChange={(val) => {
setValue(val);
}}
/>
);
};
Note: This method lets you to use Formik with react-datepicker, but it also works for other UI Libraries like Material-UI and Ant Design too.
What I see from your code is DatePicker is not inside of Formik. In React.js we always create reusable components so keep the code clean and easy to test. To be able to use DatePicker in your code, create a separate file, write your code for Datepicker and render it as a component of the Field. here is how you should implement.
//First let's start with structure of Formik.
import PortDate from "./form/PortDate";
//define your components in a different directory. I named it form
const CreateForm = props => (
<div>
<Formik
initialValues={{"your initial values"}}
validate={your validation function here}
onSubmit={values => {
return props.onSubmit(values);
}}
>
{({ isSubmitting }) => (
<Form>
<Field name="startDate" component={DatePicker} label="Start Date" />
<Field
name="endDate"
component={DatePicker}
label="End Date"
canBeDisabled={true}
/>
</Form>
)}
</Formik>
</div>
);
now in a different component let's implement our Datepicker logic.
//Use reactstrap instead of bootstrap when you want to add style
import { FormGroup, Label } from "reactstrap";
class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
dueDate: new Date(),
};
this.handleChange = this.handleChange.bind(this);
}
setFieldValueAndTouched(date, touched) {
const { setFieldValue, setFieldTouched } = this.props.form;
const { name } = this.props.field;
setFieldValue(name, date, true); //field,value,shouldValidate
setFieldTouched(name, touched, true); //field,touched,shouldValidate
}
handleChange(date) {
this.setState(() => ({ dateValue: date }));
this.setFieldValueAndTouched(date, true); }
render() {
const { dueDate } = this.state;
const {
label,
field,
form: { touched, errors },
} = this.props;
// field and form props are passed to this component automatically because we render this inside component of the Field.
return (
<FormGroup>
<Label>{label}</Label>
<div className="input-group">
<DatePicker
selected={dueDate}
onChange={this.handleChange}
peekNextMonth
showMonthDropdown
showYearDropdown
maxDate={new Date()}
dropdownMode="select"
/>
</div>
</FormGroup>
);
}
}
export default DatePicker;
How to make this to work for the date range picker:
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const { setFieldValue } = useFormikContext();
const [field] = useField({ name: name, value: startDate });
const [field2] = useField({ name: name2, value: endDate });
<GroupContainer>
<DatePicker
{...field}
onFocus={() => setFocusStart(true)}
onCalendarClose={() => setFocusStart(false)}
selected={(field.value && new Date(field.value)) || null}
onChange={(val) => {
setStartDate(val);
setFieldValue(field.name, val);
}}
dateFormat="dd.MM.yyyy"
selectsStart
minDate={new Date()}
/>
</GroupContainer>
<GroupContainer>
<DatePicker
{...field2}
onFocus={() => setFocusEnd(true)}
onCalendarClose={() => setFocusEnd(false)}
selected={(field2.value && new Date(field2.value)) || null}
onChange={(val) => {
setEndDate(val);
setFieldValue(field2.name, val);
}}
dateFormat="dd.MM.yyyy"
selectsEnd
minDate={new Date()}
/>
</GroupContainer>
Then:
<ReactDatePicker name="arrivalDate" name2="departureDate" />
const formik = useFormik({
initialValues: {
date:""
},
onSubmit: (values) => {
alert(JSON.stringify(values, null, 2));
},
});
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
disableToolbar
variant="inline"
TextFieldComponent={(params) => {
return <TextField className={classes.TextField}
{...params} variant="outlined" />;
}}
format="dd MMM yyyy"
margin="normal"
name="installDate"
value={formik.values.date}
onChange={(newDate) => {
//use this here
formik.setFieldValue("date", newDate);
}}
KeyboardButtonProps={{
"aria-label": "change date",
}}
/>
</MuiPickersUtilsProvider>

Resources