jest + enzyme: Doesn't update material input value - reactjs

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.

Related

React currency input field with customInput example

I am trying to render customInput in React currency input field, What I have so far is:
import { TextField, TextFieldProps } from '#mui/material';
import React from 'react';
import CurrencyInput from 'react-currency-input-field';
import { Controller, RegisterOptions, useFormContext } from 'react-hook-form';
type IProps = {
name: string;
rules?: RegisterOptions;
defaultValue?: string;
};
type Props = IProps & TextFieldProps;
export default function RHFCurrencyField({ name, rules, defaultValue, ...other }: Props) {
const { control } = useFormContext();
return (
<Controller
name={name}
rules={rules}
control={control}
render={({ field, fieldState: { error } }) => (
<CurrencyInput
name={name}
groupSeparator=" "
defaultValue={defaultValue}
decimalsLimit={2}
onValueChange={(value, name) => {
field.onChange(value);
}}
customInput={() => <CurrencyTextField {...other} {...field} />}
/>
)}
/>
);
}
export const CurrencyTextField = React.forwardRef((props: Props, ref: any) => {
return <TextField {...props} ref={ref} />;
});
And I am getting a warning, and input i
react-dom.development.js:67 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of CurrencyInput.
What am I missing ?

How to set focus when using React hook form Controller component

I have a CustomTextBox component that is wrapped in react-hook-form Controller component and all works fine including validation and displaying the error message with ErrorMessage component.
The only thing left to-do is to setFocus on the fields when there is errors in the form. This is my first TypeScript project and i'm struggling to find solutions which are similar to mine.
I tried useRef but this only give compile time error => "Property 'ref' does not exist on type 'IntrinsicAttributes".
Below is my Custom component. Please guys all help will be appreciated. Thanks in advance
import React, { useRef } from "react";
import TextField from '#material-ui/core/TextField';
import { Control, Controller } from "react-hook-form";
import { Keys } from '../Profile/interfaces';
interface Props {
id: string;
label: string,
variant: "filled" | "standard" | "outlined",
disabled?: boolean,
control: Control<any,any>
required?: boolean,
name: Keys,
requiredMsg?: string
}
const CustomTextBox: React.FC<Props> = ({id, label, variant,disabled=false, control,
required=false, name, requiredMsg}) => {
const inputRef = useRef<React.RefObject<HTMLButtonElement>>();
return (
<Controller
ref={inputRef}
name={name}
control={control}
rules={{required: required ? requiredMsg : null}}
render={({ field }) =>
<TextField inputRef={field.ref} InputLabelProps={{ shrink: true }} id={id} label={label} variant={variant}
disabled={disabled} {...field}
style={{marginTop: '10px', marginBottom: '10px', minWidth: '250px'}} /> }
/>
);
}
export default CustomTextBox;
So thanks to #Victor Luft i was able to get it right with the below code. Also this is not in the component itself but rather on the page/component that uses Custom Components. This will focus on any element that has an error in your react-hook-form form tag. I hope that makes sense.
Thanks again Victor
useEffect(() => {
const firstErrorKey = Object.keys(errors).find((key) => errors[key]);
if (firstErrorKey) {
(document.querySelector(
`input[name="${firstErrorKey}"]`
) as HTMLInputElement | null)?.focus();
}
}, [Object.keys(errors)]);
You are using field.ref as inputRef for the TextField component. It is very likely that this will be assigned to the native <input /> element. And that is the one, you want to be able to call focus() on.
import React, { useRef } from "react";
import TextField from '#material-ui/core/TextField';
import { Control, Controller } from "react-hook-form";
import { Keys } from '../Profile/interfaces';
interface Props {
id: string;
label: string,
variant: "filled" | "standard" | "outlined",
disabled?: boolean,
control: Control<any,any>
required?: boolean,
name: Keys,
requiredMsg?: string
}
const CustomTextBox: React.FC<Props> = ({id, label, variant,disabled=false, control,
required=false, name, requiredMsg}) => {
const inputRef = useRef<React.RefObject<HTMLButtonElement>>();
return (
<Controller
ref={inputRef}
name={name}
control={control}
rules={{required: required ? requiredMsg : null}}
render={({ field, fieldState }) => {
// don't know who to compute this state as I don't know this lib
const hasError = fieldState === 'error';
if (field.ref.current && fieldState === 'error') field.ref.current.focus();
return (
<TextField inputRef={field.ref} InputLabelProps={{ shrink: true }} id={id} label={label} variant={variant}
disabled={disabled} {...field}
style={{marginTop: '10px', marginBottom: '10px', minWidth: '250px'}} />);
}}
/>
);
}
export default CustomTextBox;

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;

updating Formik initialValues with values from separate component

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

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;

Resources