updating Formik initialValues with values from separate component - reactjs

I have a selectInput component made with React select which I have inside a Formik Form. I am trying to get my Formik values to update with the values coming back from React Select. What is the best way to get this done?
Inside my Formik Form:
const initialValues = {
firstName: '',
lastName: '',
jobPosition: '',
email: '',
phoneNumber: '',
languages: [],
files: []
};
<SelectInput
name='languages'
options={languages}
isMulti={true}
onChange={handleChange}
type='select'
/>
my SelectInput component:
import React from 'react'
import Select from "react-select";
import { inputStyles } from './styles';
import PropTypes from 'prop-types';
import { useField } from 'formik';
const SelectInput = ({ options, placeholder, isMulti, ...props }) => {
const [field] = useField(props)
return (
<div style={{ marginTop: '1.5rem' }}>
<Select
{...field}
styles={inputStyles}
className="basic-single"
classNamePrefix="select"
isClearable={true}
isSearchable={true}
placeholder={placeholder}
options={options}
isMulti={isMulti}
/>
</div>
)
}
In its current state when selecting an option I receive a type Error:
Formik.tsx:600 Uncaught TypeError: Cannot read properties of undefined (reading 'type')

I can't find the handleChange function you passed to onChange prop of SelectInput so I can't tell you what the problem in your code is but you can use useFormikContext hook to get setFieldValue method. You can pass this function to SelectInput from the parent component but the better approach is to move this function to your SelectInput component. You will have to read field name from props now. So it will look like this:
const { setFieldValue } = useFormikContext();
const handleChange = (option) => {
setFieldValue(props.name, option);
};
Add this code to your SelectInput component.
Also note that you can use useFormikContext here because SelectInput is used inside form component of formik. If you want to define handleChange outside SelectInput component, you can give your form a ref and use ref.current.setFieldValue in parent component.
In case you need handleChange function outside Formik component, you can do this:
const ref = useRef();
const handleChange = (option) => {
ref.current.setFieldValue("languages", option);
};
return (
<Formik
//other props
innerRef={ref}
>
{/* your inputs */}
</Formik>
);

Related

Accessing value of child component within Formik form

I have a form that uses Formik and contains two fields - email and siteID
The email field uses a TextInput whereas siteID comes from my own component called DropDown that uses react-native-dropdown-picker
When my form is submitted I can see the value of email within handleSubmit() but siteID remains as 0 and not the selected value.
How can I access the selected value from my DropDown child component when submitting the parent form?
My form
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.loginContainer}>
<Formik
validationSchema={loginValidationSchema}
initialValues={{
siteID: 0,
email: "",
}}
onSubmit={(values) => handleSubmit(values)}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
isValid,
}) => (
<>
<Text style={styles.label}>Email</Text>
<TextInput
name="email"
style={styles.textInput}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
value={values.email}
keyboardType="email-address"
/>
<DropDown name="siteID" />
</>
)}
</Formik>
</View>
</SafeAreaView>
);
DropDown component
import * as React from 'react';
import {StyleSheet} from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import {usePostRequest} from '../../client';
import {useState, useEffect} from 'react';
const DropDown = () => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickervalue, pickersetValue] = useState(null);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={pickervalue}
items={pickeritems}
setOpen={pickersetOpen}
setValue={pickersetValue}
setItems={pickersetItems}
/>
);
};
Based on the docs, to use your own component, you should wrap your component in a Field as follows:
const DropDownField = <Field name="siteId" component={DropDown} />
When using a Field formik will inject props (name, value, onChange, onBlur) into your component allowing you to control the form state.
So you can then redeclare your component with those props as follows:
import * as React from 'react';
import {StyleSheet} from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import {usePostRequest} from '../../client';
import {useState, useEffect} from 'react';
const DropDown = ({ field: { name, value, onChange }) => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={value}
items={pickeritems}
setOpen={pickersetOpen}
setValue={onChange}
setItems={pickersetItems}
name={name}
/>
);
};
Essentially you wire your component up to the onChange and value props. Since you assigned a name of siteId to the DropDownField component (which you should now use in your form), any time you call onChange site Id will update.
Link to docs
You can use setFieldValue provided by Formik to set a field manually.
A good example of usage is provided in this example
You can pass setFieldValue to your DropDown Component:
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.loginContainer}>
<Formik
validationSchema={loginValidationSchema}
initialValues={{
siteID: 0,
email: "",
}}
onSubmit={(values) => handleSubmit(values)}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
isValid,
setFieldValue,
}) => (
<>
<Text style={styles.label}>Email</Text>
<TextInput
name="email"
style={styles.textInput}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
value={values.email}
keyboardType="email-address"
/>
<DropDown name="siteID" setSiteID={setFieldValue} />
</>
)}
</Formik>
</View>
</SafeAreaView>
);
And trigger it from inside of the DropDown Component:
const DropDown = ({ setSiteID }) => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickervalue, pickersetValue] = useState(null);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
// You can use useEffect or create a custom handleValueChange
// and pass it to DropDownPicker
useEffect(() => {
setSiteID('siteID', pickervalue);
}, [pickersetValue])
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={pickervalue}
items={pickeritems}
setOpen={pickersetOpen}
setValue={pickersetValue}
setItems={pickersetItems}
/>
);
};
try this,
get setFieldValue from formik props.
add onChangeDropdown to <Dropdown /> like,
<DropDown name="siteID" onChangeDropdown={(value)=>{
setFieldValue("siteID",value)
}}/>
get the props inside Dropdown component,
const DropDown = (props) => {
add onChangeValue prop to your DropDownPicker
onChangeValue={props.onChangeDropdown}

How to pass useFrom methods to child component using typescript

const methods = useForm<Module>({
defaultValues: defaultValues,
mode: "onChange",
shouldFocusError: true,
shouldUnregister: true,
resolver: yupResolver(module) });
enter image description here which props do I need to use indeed of UseFormProps.
How to pass this methods props to the child component using typescript
In order to pass the methods to a child component inside a form you need to use useFormContext see documentation here: react hook form doc
import React from "react";
import { useForm, FormProvider, useFormContext } from "react-hook-form";
export default function App() {
const methods = useForm();
const onSubmit = data => console.log(data);
return (
<FormProvider {...methods} > // pass all methods into the context
<form onSubmit={methods.handleSubmit(onSubmit)}>
<NestedInput />
<input type="submit" />
</form>
</FormProvider>
);
}
function NestedInput() {
const { register } = useFormContext(); // retrieve all hook methods
return <input {...register("test")} />;
}

Struggling to get MaterialUI 5 checkbox to work with Formik 2

I'm having trouble getting Formik 5 and MUI 5 checkboxes to work together. In the Codesandbox below, you'll see a form with two unchecked checkboxes: "with MUI" and "without MUI".
The "without MUI" checkbox behaves as expected: I can check and uncheck it, and if I submit, I see withoutMui in the console with a value of true or false.
However, the "With MUI" checkbox exhibits different behavior. Rather than setting true or false, it creates an array of data. I get that this is done to handle multiple checkboxes grouped together. However, what I don't see is anywhere I can transform that array value into true or false before handing it off to the MUI checkbox. it looks like Typescript expects field.checked that comes back from useField to be a boolean rather than an array, so I'm not able to do any manipulation on that value without getting a Typescript error.
Interestingly, if I check the "with MUI" checkbox once, I see an array with 1 value (on). If I click the checkbox again, then I get an array with no values in it, but the checkbox UI does not update. So it looks like the checkbox data is getting properly updated, but the MUI checkbox can't deal with array data.
I feel like I'm doing something stupid here, and would appreciate being shown the error of my ways.
Thanks in advance!
https://codesandbox.io/s/wizardly-https-bb7up6
non-MUI Checkbox code from sandbox:
import { useField } from "formik";
const CheckboxWithoutMUI = ({ label, ...rest }) => {
const [field, meta] = useField({ ...rest, type: "checkbox" });
return (
<div>
<label>
<input type="checkbox" {...field} {...rest} />
{label}
</label>
</div>
);
};
export default CheckboxWithoutMUI;
MUI Checkbox code from sandbox:
import { useField } from "formik";
import FormControlLabel from "#mui/material/FormControlLabel";
import Checkbox from "#mui/material/Checkbox";
interface IProps {
label: string;
name: string;
}
const CheckboxField = ({ label, ...rest }: IProps) => {
const [field, meta] = useField({ ...rest, type: "checkbox" });
return (
<div>
<FormControlLabel
label={label}
control={
<Checkbox
checked={field.checked}
onChange={field.onChange}
id={field.name}
/>
}
/>
</div>
);
};
export default CheckboxField;
Software
Version(s)
Formik
2.2.9
React
17.0.2
TypeScript
4.5.5
Browser
Chrome (Mac) 101.0.4951.64
npm/Yarn
8.5.0
Operating System
macOS 12.4
The difference in behavior between the two checkboxes is due to not giving the MUI checkbox a value. This causes Formik to manage the checkbox value differently.
If you remove the value prop from the non-MUI checkbox, it will behave the same as the MUI checkbox in your sandbox:
import { useField } from "formik";
const CheckboxWithoutMUI = ({ label, ...rest }) => {
const [field, meta] = useField({ ...rest, type: "checkbox" });
const { value, ...fieldExceptValue } = field;
return (
<div>
<label>
<input type="checkbox" {...fieldExceptValue} {...rest} />
{label}
</label>
</div>
);
};
export default CheckboxWithoutMUI;
If you change the syntax of the MUI Checkbox to be more similar to your non-MUI version (pass all of the field props to it), it works as desired:
import { useField } from "formik";
import FormControlLabel from "#mui/material/FormControlLabel";
import Checkbox from "#mui/material/Checkbox";
interface IProps {
label: string;
name: string;
}
const CheckboxField = ({ label, ...rest }: IProps) => {
const [field] = useField({ ...rest, type: "checkbox" });
return (
<div>
<FormControlLabel label={label} control={<Checkbox {...field} />} />
</div>
);
};
export default CheckboxField;
It is also sufficient to just add value to what you already had:
import { useField } from "formik";
import FormControlLabel from "#mui/material/FormControlLabel";
import Checkbox from "#mui/material/Checkbox";
interface IProps {
label: string;
name: string;
}
const CheckboxField = ({ label, ...rest }: IProps) => {
const [field] = useField({ ...rest, type: "checkbox" });
return (
<div>
<FormControlLabel
label={label}
control={
<Checkbox
checked={field.checked}
onChange={field.onChange}
id={field.name}
value={field.value}
/>
}
/>
</div>
);
};
export default CheckboxField;

Material-UI Autocomplete, React Hook Form - Changing InputValue in Material UI Autocomplete with Use State in an OnChange?

I've been customising a Material UI Autocomplete within a Controller from React Hook Form, as part of a much larger form, with some difficulty.
The dropdown lists suggestions drawn from the database (props.items, represented here as objects) and if the suggestion is not there, there's the option to add a new one in a separate form with a button from the dropdown. This 'secondComponent' is opened with conditional rendering.
As it gets passed to the second form, the data is stored in state (heldData) and then passed back into the form via React Hook Form's reset, here as reset(heldData).
This updates the value of the form perfectly, as I have an onChange event that sets the value according to what was passed in. React Hook Form handles that logic with the reset and gives the full object to the onChange.
However, I also want to set the InputValue so that the TextField is populated.
In order to create a dynamic button when there are no options ('Add ....(input)... as a guest'), I store what is typed into state as 'texts'. I thought that I could then use the OnChange event to use the same state to update the inputValue, as below. However, when I setTexts from the onChange, the change isn't reflected in the inputValue.
Perhaps this is because the useState is async and so it doesn't update the state, before something else prevents it altogether. If so, it's much simpler than the other code that I have included, but wasn't certain. I have excluded most of the form (over 500 lines of code) but have tried to keep any parts that may be appropriate. I hope that I have not deleted anything that would be relevant, but can update if necessary.
Apologies. This is my first question on Stack Overflow and I'm quite new to React (and coding) and the code's probably a mess. Thank you
**Form**
import React, { useState, useEffect} from "react";
import AutoCompleteSuggestion from "../general/form/AutoCompleteSuggestion";
import SecondComponent from './SecondComponent'
import { useForm } from "react-hook-form";
const items = {
id: 2,
name: "Mr Anderson"
}
const items2 = {
id: 4,
name: "Mr Frog"
}
const defaultValues = {
guest: 'null',
contact: 'null',
}
const AddBooking = () => {
const { handleSubmit, register, control, reset, getValues} = useForm({
defaultValues: defaultValues,
});
const [secondComponent, setSecondComponent] = useState(false);
const [heldData, setHeldData] = useState(null)
const openSecondComponent = (name) => {
setSecondComponent(true)
const data = getValues();
setHeldData(data);
}
useEffect(() => {
!secondComponent.open?
reset(heldData):''
}, [heldData]);
const onSubmit = (data) => {
console.log(data)
};
return (
<>
{!secondComponent.open &&
<form onSubmit={handleSubmit(onSubmit)}
<AutoCompleteSuggestion
control={control}
name="guest"
selection="id"
label="name"
items={items}
openSecondComponent={openSecondComponent}
/>
<AutoCompleteSuggestion
control={control}
name="contact"
selection="id"
label="name"
items={items2}
openSecondComponent={openSecondComponent}
/>
</form>
};
{secondComponent.open?
<SecondComponent/>: ''
};
</>
);
};
And this is the customised AutoComplete:
**AutoComplete**
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete, from "#material-ui/lab/Autocomplete";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";
import { Controller } from "react-hook-form";
import Button from "#material-ui/core/Button";
const AutoCompleteSuggestion = (props) => {
const [texts, setTexts] = useState('');
return (
<>
<Controller
name={props.name}
control={props.control}
render={({ onChange }) => (
<Autocomplete
options={props.items}
inputValue={texts} //NOT GETTING UPDATED BY STATE
debug={true}
getOptionLabel={(value) => value[props.label]}
noOptionsText = {
<Button onClick={()=> props.opensSecondComponent()}>
Add {texts} as a {props.implementation}
</Button>}
onChange={(e, data) => {
if (data==null){
onChange(null)
} else {
onChange(data[props.selection]); //THIS ONCHANGE WORKS
setTexts(data[props.label]) //THIS DOESN'T UPDATE STATE
}}
renderInput={(params) => (
<TextField
{...params}
onChange = { e=> setTexts(e.target.value)}
/>
)}
renderOption={(option, { inputValue }) => {
const matches = match(option[props.label1, inputValue);
const parts = parse(option[props.label], matches);
return (
<div>
{parts.map((part, index) => (
<span
key={index}
style={{ fontWeight: part.highlight ? 700 : 400 }}
>
{part.text}
</span>
))}
</div>
);
}}
/>
)}
/>
</>
);
};
export default AutoCompleteSuggestion;

jest + enzyme: Doesn't update material input value

I am testing a material-UI TextField using jest and enzyme. After simulating the change event on the text field, the value is not getting updated. Am I missing something while testing in a stateless component?
textfield.spec.js
it("on input change should call onChange function passed through props",()=>{
const handleChange = jest.fn();
let props = {
label: 'Test Label',
type: 'text',
name: 'email',
value: "Hello World",
index: 0,
input: {},
defaultValue:'default',
meta: {
touched: true,
error: 'error'
},
onChange:handleChange,
}
const wrapper = mount(<Textfield {...props}/>);
wrapper.find('input').simulate('change',{target:{name:'email',value:"hello"}});
wrapper.update();
expect(handleChange).toHaveBeenCalled();
expect(wrapper.find('input').prop('value')).toBe("hello")
})
Textfield.js
import React from 'react';
import TextField from '#material-ui/core/TextField';
import './style.scss';
const Textfield = (props) => {
const {label,value,onChange,className,name,id,onKeyDown,multiline,index,error,inputProps,errorMsg,isDisabled} = props;
return (
<TextField
error={error}
id={id}
label={error ? "Incorrect Field" : label}
variant="filled"
value={value}
onChange={onChange}
classname={className}
name={name}
onKeyDown={onKeyDown}
multiline={multiline}
helperText={error && "Incorrect Field."}
inputProps={{
...inputProps,
'data-testid': id
}}
disabled={isDisabled}
/>
);
};
export default Textfield;
I'd say that a proper way to test any material-ui component is to change its props, in this case, the value prop.
Also, as #UKS pointed out, you have mocked the onChange function, so don't be surprised that the value doesn't change.

Resources