How to set focus when using React hook form Controller component - reactjs

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;

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 ?

Material-UI pickers 3.3.10 not displaying properly

I am using DatePicker in a custom component and everything works fine including react-hook-form Controllers component for validation. But the top-part of the DatePicker does not display properly. Below is a preview of how it displays.
Here is is how I am using this component to create my own re-useable component. I am at a loss to explain why this is happening. Please any help would be very well appreciated.
Thanks in advance.
import React, { Fragment } from "react";
import { makeStyles } from '#material-ui/core/styles';
import DateFnsUtils from '#date-io/date-fns';
import { MuiPickersUtilsProvider, DatePicker} from '#material-ui/pickers';
import 'date-fns';
import { Control, Controller } from "react-hook-form";
import { Keys } from "../Profile/interfaces";
import { alpha } from '#material-ui/core/styles'
const useStyles = makeStyles((theme) => ({
formControl: {
marginTop: "10px",
marginBottom: "10px",
minWidth: 220,
},
}));
interface Props {
id: string,
label: string,
control: Control<any,any>
required?: boolean,
name: Keys,
requiredMsg?: string,
disabled?: boolean
}
const CustomDate: React.FC<Props> = ({id,label,control, required=false, name, requiredMsg,
disabled = false}) => {
const classes = useStyles();
return(
<div className={classes.formControl}>
<Controller
name={name}
control={control}
rules={{required: required ? requiredMsg : null}}
render={({ field }) =>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<DatePicker
disabled={disabled}
format="dd/MM/yyyy"
inputVariant="filled"
id={id}
autoOk
label={label}
clearable
disableFuture
{...field}
/>
</MuiPickersUtilsProvider>
}
/>
</div>
);
}
export default CustomDate;

I am having errors while using input component in my next.js project

I am using stripe payment in my next.js project. I am having a material ui textfield like this
import StripeInput from './StripeInput';
import {useStripe, useElements,CardExpiryElement} from '#stripe/react-stripe-js';
import {TextField, Grid, Container,} from "#mui/material";
<Grid item xs={6} sm={6}>
<TextField
label="Expiration Date"
name="ccexp"
variant="outlined"
required
fullWidth
InputProps={{
inputProps: {
component: CardExpiryElement
},
inputComponent: StripeInput
}}
InputLabelProps={{ shrink: true }}
/>
</Grid>
But am getting this error below on my page
We got an error "A cross-origin error was thrown. React does'nt have access to the actual error object in development"
The cause of the error is inputComponent: StripeInput anytime I comment it no error is thrown.
This is the StripeInput Component below
import { useRef, useImperativeHandle } from 'react';
function StripeInput ({ component: Component, inputRef, ...props }) {
const elementRef = useRef();
useImperativeHandle(inputRef, () => ({
focus: () => elementRef.current.focus
}));
return (
<Component
onReady={element => (elementRef.current = element)}
options={{
style: {
base: {
color: '#FFF'
}
}
}}
{...props}/>
)
}
export default StripeInput;
I have tried clearing my localstorage, cookies etc, based on my search online, but still no change.

React Hook Forms How to pass the errors as a props using Typescript

I'm defining a useForm.
const { handleSubmit, control, errors } = useForm<{email: string}>();
Now I'm creating a seperate component that will the input and I'm going to pass the useForm props i created above.
This how that Components look like.
type Props<T> = {
name: FieldName<T>;
control: Control<T>;
errors: FieldErrors<T>;
};
const ControlTextInput = <T extends {}>({
name,
control,
errors,
}: Props<T>) => {
return (
<Controller
name={name}
control={control}
rules={{
required:'this is required',
}}
render={({ onChange }) => (
<>
<TextInput
onChangeText={(text) => {
onChange(text);
}}
/>
{/* Show my error here */}
{errors.email && (
<Text style={{ color: "red" }}>
{errors.email?.message}
</Text>
)}
</>
)}
/>
);
};
I want to use the component like this.
<ControlTextInput<AnyObject>
name="email"
errors={errors}
control={control}
/>
I get this error when i hover over the errors.email
React Hook Form exposes type UseControllerProps which accepts generic type T which infers your input value types or in other words the FieldValues type. Initially you define FieldValues type by passing type about your fields to useForm hook (see MyInputTypes below).
interface MyInputTypes {
email: string;
password: string;
}
const { register, handleSubmit, watch, formState: { errors } } = useForm<MyInputTypes>();
This means you can create interface which extends UseControllerProps and has your generic T interface Props<T> extends UseControllerProps<T> {}. Type UseControllerProps already includes type definitions for name and control and therefore you will not need to define them separately in your interface (unless you want to or there is a particular requirement / reason to do so). Regarding errors appropriate solution Imo (as you require only error about single field) would be to pass that particular error directly which has type FieldError | undefined. The result looks like below code.
interface Props<T> extends UseControllerProps<T> {
error: FieldError | undefined
}
Then you can simply use your ControlTextInput as below.
<ControlTextInput name="email" error={errors.email} />
In the Component (which uses ControlTextInput) your generic T must extend FieldValues as eventually, it is this type which infers types about the fields.
As an example ControlTextInput
import React from 'react';
import { Controller, FieldError, FieldValues, UseControllerProps } from 'react-hook-form';
interface Props<T> extends UseControllerProps<T> {
error: FieldError | undefined;
}
const ControlTextInput = <T extends FieldValues>({ name, control, error }: Props<T>) => {
return (
<Controller
name={name}
control={control}
rules={{
required: 'This is required',
}}
render={({ field: { onChange } }) => (
<>
<input
onChange={(text) => {
onChange(text);
}}
/>
{error && <span style={{ color: 'red' }}>{error?.message}</span>}
</>
)}
/>
);
};
export default ControlTextInput;
As an example Component which uses ControlTextInput
import React, { FunctionComponent } from 'react';
import { useForm } from 'react-hook-form';
import ControlTextInput from './ControlTextInput';
interface InputTypes {
email: string;
password: string;
}
const Foo: FunctionComponent = () => {
const {
formState: { errors },
} = useForm<InputTypes>();
return <ControlTextInput name='email' error={errors.email} />;
};
export default Foo;
Here are screenshots with ready code which mimics more or less your approach and solution (same as code above as new to StackOverflow).
ControlTextInput
Component which uses ControlTextInput
The way I found a solution is to use error type any
errors?: any;
Use the lodash get function
const errName = get(errors, name);
Then i can get the error has follow.
{errName && <Text style={{ color: "red" }}>{errName.message}</Text>}

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