how to use controller component in react-hook-form - reactjs

I want to validate MuiPhoneInput using react hook form (v7.43.1).
The useForm looks like
const {
register,
handleSubmit,
formState: { errors },
control,
} = useForm();
The controller component looks like
<Controller
name="phone"
control={control}
render={({ field: { onChange, value } }) => (
<MuiPhoneInput
value={value}
onChange={onChange}
defaultCountry={"il"}
variant={"outlined"}
error={!!errors.phone}
helperText={errors?.phone?.message}
/>
)}
rules={{
required: "Phone is required",
}}
/>
But I can't write anything in the input. I tried with other textfields, but nothing works. How to solve this?

You're very close except your MuiPhoneInput is losing reference from the field destructure you're using to get value and onChange. There's an important ref that needs to be set contained inside field. Here's the official example of a Controller component handling a custom masked input.
Here's my working sandbox that demonstrates integrating material-ui-phone-material with react-hook-form.
Since you're setting defaultCountry={"il"} on the MUI component, also set the default value that resolves to inside defaultValues so react-hook-form knows about it.
Adds validation via isNotFilledTel() used inside rules>validate object.
Handling a React.StrictMode issue where ref.focus is undefined.
app.js
import { Controller, useForm } from "react-hook-form";
import MuiPhoneInput from "material-ui-phone-number";
export default function App() {
const {
handleSubmit,
formState: { errors },
control,
} = useForm({
reValidateMode: "onSubmit",
defaultValues: {
phone: "+972",
},
});
const isNotFilledTel = (v: string) => {
return v.length < 15 ? "Phone number is required." : undefined;
};
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="phone"
control={control}
rules={{
required: "Phone is required",
validate: {
inputTelRequired: isNotFilledTel,
},
}}
render={({ field }) => {
return (
<MuiPhoneInput
{...field}
ref={(ref) => {
if (ref && !ref.focus) ref.focus = () => {};
}}
defaultCountry={"il"}
variant={"outlined"}
error={!!errors.phone}
helperText={<>{errors?.phone?.message}</>}
/>
);
}}
/>
{errors.phone && <p>Phone is required.</p>}
<button type="submit">Submit</button>
</form>
);
}

Related

How can I set validation rules when all fields are empty React Hook Form

I'm using React Hook Form V7. I have two input fileds and using Controller to control the input.
The below one is what I did now, each field is required.
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test1"
control={control}
rules={{ required: true }}
/>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test2"
control={control}
rules={{ required: true }}
/>
<input type="submit" />
</form>
My question is how can I set instead of each one is required, I want the error message showing when both fields are empty, if only one is empty is accepted.
Is there any onSubmit validation on React Hook Form? Or I need to do the normal validation on the onSubmit function to check the value then set if error message show?
Edit:
this is what I did now:
const [submitError, setSubmitError] = useState(false)
onSubmit((data) => {
const { test1, test2 } = data
if (!test1 && !test2) {
setSubmitError(true)
} else {
setSubmitError(false)
// do submit action
}
})
const errorEmptyMessage = "one of test1 and test2 should has value"
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test1"
control={control}
rules={{ required: true }}
/>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test2"
control={control}
rules={{ required: true }}
/>
{submitError && emptyMessage}
<input type="submit" />
</form>
)
I wonder if React Hook Form has a built-in function to do this?
Is there any onSubmit validation on React Hook Form? Or I need to do
the normal validation on the onSubmit function to check the value then
set if error message show?
Yes, You have a nice solution which I recommend it to use nested of normal validation, its name schema validation like YUP, Simply what you need to do is add needed rule, for example (from react-hook-form):
import React from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from '#hookform/resolvers/yup';
import * as yup from "yup";
const schema = yup.object({
firstName: yup.string().required(),
age: yup.number().positive().integer().required(),
}).required();
export default function App() {
const { register, handleSubmit, formState:{ errors } } = useForm({
resolver: yupResolver(schema)
});
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<p>{errors.firstName?.message}</p>
<input {...register("age")} />
<p>{errors.age?.message}</p>
<input type="submit" />
</form>
);
}
If you read the above code, you see you are build a schema for each field needed, and you have a lot of options, for example in your case you may use when to handling on x and y is empty and so on..., also you have a lot of validation schema build as an object like int, min and required, you can check this part.
Also you can do that via onSubmit on normal flow, like this:
const onSubmit = () => {
throw new Error('Something is wrong')
}
handleSubmit(onSubmit).catch((e) => {
// you will need to catch that error
})
and the idea here you check what you need and you can throw the error, exampe:
const { register, handleSubmit } = useForm();
const onSubmit = (data, e) => console.log(data, e);
const onError = (errors, e) => console.log(errors, e);
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<button type="submit">Submit</button>
</form>
);
But from my side, I suggest to use schema validation, its really useful base on my experience with react hook form.
Update 1: More Example:
In above is example how you can build conditions to resolve issue, but simply visit yup and check when,
const schema =
object().shape({
a: string().when(["a", "b"], {
is: (a, b) => !a && !b
then: string().required("At least one is to be selected"),
otherwise: string() // unnecessary
}),
a: string().when(["a", "b"], {
is: (a, b) => !a && !b
then: string().required("At least one is to be selected"),
otherwise: string() // unnecessary
})
});

react-hook-form not triggering onSubmit when using Controller

I have the following form:
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="voucherPrice"
control={control}
defaultValue={false}
rules={{ required: true }}
render={({ field: { onChange, value, ref } }) => (
<Input {...field} onChange={(ev) => handlePriceInputChange(ev)} value={price} type="number" innerRef={ref} />
)}
/>
<p>{errors.voucherPrice?.message}</p>
<Button
variant="contained"
sx={{ mt: 1, mr: 1 }}
type="submit"
>
{"Continue"}
</Button>
</form>
and with this configuration:
function PriceSelection(props) {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
console.log("does not work?", data);
};
const classes = useStylesPriceSelection();
const [selected, setSelected] = useState(false);
const [price, setPrice] = useState("");
const handlePriceInputChange = (ev) => {
console.log("change", price);
setPrice(parseInt(ev.target.value));
};
The function onSubmit does not trigger when I press the submit button. Also I would like the input field to be filled by default by the state price and its value to be sent with the parameter data on the function onSubmit when I push the submit button.
You are mixing useState with react-hook-form and are not updating react-hook-form's internal form state. You don't need to declare a useState for your field.
In your example you are destructering onChange from the field object of <Controller /> but you are never using it for your <Input /> component. Therefore react-hook-form can't update it's form state. As you set your field to be required the onSubmit callback won't get triggered because react-hook-form will never receive an update or value for it.
The correct way would be:
<Controller
name="voucherPrice"
control={control}
rules={{ required: true }}
render={({ field: { ref, onChange, value, ...field } }) => (
<Input {...field} onChange={onChange} value={value} type="number" innerRef={ref} />
)}
/>
Or even shorter:
<Controller
name="voucherPrice"
control={control}
rules={{ required: true }}
render={({ field: { ref, ...field } }) => (
<Input {...field} type="number" innerRef={ref} />
)}
/>
UPDATE
If you need to have access to the value outside of the <Controller />, you should use react-hook-form's watch method. This will allow you to subscribe to the latest value of the voucherPrice field and use it inside your component -> Docs
If you want to set or update the value programmatically you can use the setValue method from react-hook-form -> Docs
const { control, handleSubmit, watch, setValue } = useForm();
const voucherPrice = watch("voucherPrice");
const onButtonClick = () => {
setValue("voucherPrice", <newValue>);
}
If you really need to have a separate useState for your value and want to update it additionally to your react-hook-form field update, you could do the following:
<Controller
name="voucherPrice"
control={control}
rules={{ required: true }}
render={({ field: { ref, onChange, value, ...field } }) => (
<Input
{...field}
onChange={(v) => {
onChange(v);
handlePriceInputChange(v);
}}
value={value}
type="number"
innerRef={ref}
/>
)}
/>
But i would suggest to use the react-hook-form only solution, as it has all the functionality you need to manage your form state.

How to set value of a Select in react-hook-form?

I am trying to load async data and use it to populate material-ui components in a form with react-hook-form. I have a TextField that seems to work fine, but I can't seem to figure out how to get the Select to show the correct value.
Here's a codesandbox to demo my problem.
I am using Controller to manage the Select as seems to be recommended in the docs:
const { register, handleSubmit, control, reset, setValue } = useForm()
<TextField name="name" inputRef={register} />
<Controller
name="color_id"
control={control}
register={register}
setValue={setValue}
as={
<Select>
{thingColors.map((tc, index) => (
<MenuItem key={index} value={tc.id}>
{tc.name}
</MenuItem>
))}
</Select>
}
/>
I'm trying to populate the fields with reset from useForm(), which seems to work for the TextField.
useEffect(() => {
getData().then((result) => {
reset({
color_id: 3,
name: 'Bill'
});
});
}, [reset]);
This seems to correctly set the values for the form, and when I submit my form it seems to have the correct values for name and for color_id. It seems like I'm not correctly hooking up the Select and the control is not showing the selected value that I set.
How can I get my material UI Select to show my applied value here?
In the version 7 of react hook form you can use setValue() setvalue API
useEffect(() => {
getData().then((result) => {
setValue('color_id', '3', { shouldValidate: true })
setValue('name', 'Bill', { shouldValidate: true })
});
}, []);
Note than I use the shouldValidate,this is becuase I use the isValidated in the button like this:
<Button
handler={handleSubmit(handlerSignInButton)}
disable={!isValid || isSubmitting}
label={"Guardar"}
/>
With shouldValidate I revalidate the inputs, There is also isDirty.
In version 7 of react hook form, you should use render instead of Controller API
<Controller
control={control}
name="test"
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, isTouched, isDirty, error },
formState,
}) => (
<Checkbox
onBlur={onBlur}
onChange={onChange}
checked={value}
inputRef={ref}
/>
)}
/>
Or you can use reset reset API
useEffect(() => {
getData().then((result) => {
reset({
'color_id': '3',
'name': 'Bill'
)
});
}, []);
I have not used Material UI with react hook form, but hope this is helpful.
A example of my select component, in Ionic React Typescript:
import { ErrorMessage } from "#hookform/error-message";
import { IonItem, IonLabel, IonSelect, IonSelectOption } from
"#ionic/react";
import { FunctionComponent } from "react";
import { Controller } from "react-hook-form";
type Opcion = {
label: string;
value: string;
};
interface Props {
control: any;
errors: any;
defaultValue: any;
name: string;
label: string;
opciones: Opcion[];
}
const Select: FunctionComponent<Props> = ({
opciones,
control,
errors,
defaultValue,
name,
label
}) => {
return (
<>
<IonItem className="mb-4">
<IonLabel position="floating" color="primary">
{label}
</IonLabel>
<Controller
render={({ field: { onChange, value } }) => (
<IonSelect
value={value}
onIonChange={onChange}
interface="action-sheet"
className="mt-2"
>
{opciones.map((opcion) => {
return (
<IonSelectOption value={opcion.value}
key={opcion.value}
>
{opcion.label}
</IonSelectOption>
);
})}
</IonSelect>
)}
control={control}
name={name}
defaultValue={defaultValue}
rules={{
required: "Este campo es obligatorio",
}}
/>
</IonItem>
<ErrorMessage
errors={errors}
name={name}
as={<div className="text-red-600 px-6" />}
/>
</>
);
};
export default Select;
And its implementation:
import React, { useEffect } from "react";
import Select from "components/Select/Select";
import { useForm } from "react-hook-form";
import Server from "server";
interface IData {
age: String;
}
let defaultValues = {
age: ""
}
const rulesEdad= {
required: "Este campo es obligatorio",
}
const opcionesEdad = [
{value: "1", label: "18-30"},
{value: "2", label: "30-40"},
{value: "3", label: "40-50"},
{value: "4", label: "50+"}
]
const SelectExample: React.FC = () => {
const {
control,
handleSubmit,
setValue,
formState: { isSubmitting, isValid, errors },
} = useForm<IData>({
defaultValues: defaultValues,
mode: "onChange",
});
/**
*
* #param data
*/
const handlerButton = async (data: IData) => {
console.log(data);
};
useEffect(() => {
Server.getUserData()
.then((response) => {
setValue('age', response.age, { shouldValidate: true })
}
}, [])
return (
<form>
<Select control={control} errors={errors}
defaultValue={defaultValues.age} opciones={opcionesEdad}
name={age} label={Edad} rules={rulesEdad}
/>
<button
onClick={handleSubmit(handlerSignInButton)}
disable={!isValid || isSubmitting}
>
Guardar
</button>
</form>
In React Hook Form the Select field have a "key/value" response.
So you should use:
setValue(field-name, {label: 'your-label' , value: 'your-value'});
Referring to https://github.com/react-hook-form/react-hook-form/discussions/8544
You need the Select to be wrapped with Controller and be sure to put a defaultValue on the Controller.
Example: https://codesandbox.io/s/admiring-curie-stss8q?file=/src/App.js
You can do something like this:
const Form: FC = () => {
const { register, handleSubmit, control, reset, setValue } = useForm();
const [color, setColor] = useState({name:"", color_id:-1})
useEffect(() => {
getData().then((result) => {
console.log("Got thing data", { result });
reset({
color_id: result.optionId,
name: result.name
});
setColor( {color_id: result.optionId,
name: result.name});
});
}, [reset]);
const onSubmit = (data: any) => console.log("Form submit:", data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div style={{ width: "200px" }}>
<div>
<TextField
fullWidth
name="name"
placeholder="Name"
inputRef={register}
/>
</div>
<div>
<Controller
name="color_id"
control={control}
register={register}
setValue={setValue}
defaultValue={color.name}
as={
<Select value="name" name="color_id" fullWidth>
{thingColors.map((tc, index) => (
<MenuItem key={index} value={tc.id}>
{tc.name}
</MenuItem>
))}
</Select>
}
/>
</div>
<p></p>
<button type="submit">Submit</button>
</div>
</form>
);
};
you can use a useState() to control the default value that you fetch with the getData() method and then pass the state to defaultValue param in the Controller.

Returning correct value using react-select and react-hook-form

I'm using react-hook-forms Controller api around AsyncSelect from react-select to load options as the user types from an external API. Everything works fine except the returned value is coming back as the string "[object Object]" instead of the fullName property from the object.
My component:
<Controller
control={control}
name="businessCategory"
as={
<AsyncSelect
className="react-select-container"
loadOptions={v => handleAutocompleteLookup(v)}
onChange={handleCategoryInputChange}
getOptionLabel={option => option.name}
getOptionValue={option => option.fullName}
/>
}
/>
My handleChange function. SetValue is from react-hook-form:
const handleCategoryInputChange = newValue => {
return setValue('businessCategory', newValue, true);
};
Any my data is an array of objects with the following shape:
{
fullName: "DJ service"
id: "gcid:dj"
name: "DJ service"
publisher: "GMB"
}
Any clues on this would be appreciated, thank you!
Update your code in following way
In your import
import { useForm, Controller } from 'react-hook-form';
import Select from 'react-select';
In your hook component
function Yourcomponent(props){
const methods = useForm();
const { handleSubmit } = methods;
const options = [
{ value: '1', label: 'Apple'},
{ value: '2', label: 'Ball'},
{ value: '3', label: 'Cat'},
];
const default_value = 1; // you can replace with your default value
// other codes of the component
function submitHandler(formData){
// values are available in formData
}
return(
<div>
{* other part of your component *}
<form onSubmit={handleSubmit(submitHandler)} >
{* other part of your form *}
<Controller
control={methods.control}
defaultValue={default_value}
name="field_name_product"
render={({ onChange, value, name, ref }) => (
<Select
inputRef={ref}
classNamePrefix="addl-class"
options={options}
value={options.find(c => c.value === value)}
onChange={val => onChange(val.value)}
/>
)}
/>
{* other part of your form *}
</form>
{* other part of your component *}
</div>
)
}
For multi-select (works with react-hook-form v7):
import Select from "react-select";
import { useForm, Controller } from "react-hook-form";
const {
control
} = useForm();
<Controller
control={control}
defaultValue={options.map(c => c.value)}
name="options"
render={({ field: { onChange, value, ref }}) => (
<Select
inputRef={ref}
value={options.filter(c => value.includes(c.value))}
onChange={val => onChange(val.map(c => c.value))}
options={options}
isMulti
/>
)}
/>
I made it work like this:
const options = [
{value: '', label: ''}
]
import { useForm, Controller } from "react-hook-form";
import { ErrorMessage } from "#hookform/error-message";
import Select from "react-select";
const methods = useForm();
<section>
<label>Label</label>
<Controller
as={Select}
name="Name"
options={options}
isMulti
control={methods.control}
/>
<ErrorMessage errors={methods.errors} name="Name" />
</section>
well, actually most of these answers don't work for situations where you have a custom onChange function. The trick (seems like a bug) is to set the "onChange(val.value)" inline first and then set your state variable. Here is the full code I have that works;
import { useForm, Controller } from "react-hook-form";
const {
register,
control,
formState: { errors },
handleSubmit,
} = useForm();
const handleChangeType = (option) => {
setItemType(option);
var options = getOptions(option.value);
setList(options);
setGender(null);
};
<Controller
control={control}
name="itemType"
rules={{
required: {
value: assetType.value == "item",
message: "Item type is required.",
},
}}
render={({ field: { onChange, value, ref, name } }) => (
<Select
className={"react-select"}
classNamePrefix={"react-select"}
placeholder={"Item type"}
options={itemTypeList}
onChange={val => {
onChange(val.value);
handleChangeType(val);
}}
/>
)}
/>
{errors.item?.message && <div class="validationText">{errors.item?.message}</div>}
import Select from "react-select";
import { useForm, Controller } from "react-hook-form";
const options = [
{ value: '1', label: 'Apple'},
{ value: '2', label: 'Ball'},
{ value: '3', label: 'Cat'},
];
const {control} = useForm();
<Controller
control={control}
name="AnyName"
render={({ field: { onChange, value, name, ref } }) => (
<Select
inputRef={ref}
classNamePrefix="addl-class"
options={options}
value={options.find((c) => c.value === value)}
onChange={(val) => onChange(val.map((c) =>c.value))}
isMulti
/>
)}
/>
Fixed this by approaching it with a custom register via react-hook-form as seen here:
https://react-hook-form.com/get-started#Registerfields
I wanted to post what worked for me. I used react-select, react-hook-form, and #hookform/error-message:
I created a function as follows
const handleCategoryInputChange = (newValue: any) => {
console.log(newValue.value);
return setValue('contract', newValue.value);
};
My react select looks like this:
<Controller
name="contract"
control={control}
rules={{ required: true }}
render={({ field: { onChange, value, name, ref } }) => (
<ReactSelect
// value={contractData.find((c) => c.value === value)}
// onChange={(val) => onChange(val)}
onChange={handleCategoryInputChange}
options={contractData}
ref={ref}
name={name}
defaultValue={contractData[0]}
theme={customTheme}
isSearchable={false}
className="block w-full min-w-0 flex-1 sm:text-sm"
/>
)}
/>
<ErrorMessage errors={errors} name="contract" message="Select a Contract" />
I don't think my className is working properly you can ignore that part. It is working for my specific set up though lol. The commented out code did not work the way I wanted because when i console logged the data I got it as follow:
contract: {label: 'contract name', value: 'contract name'}
optionPeriods: "dfd"
performanceEndDate: "2022-12-08"
performanceStartDate: "2022-12-08"
proposalDueDate: "2022-12-08T12:54"
requirements: "dfd"
trackingNumber: "dfd"
so I commented out that specific solution and did it the way you see now and i get the exact data i want for my contract as you can see below:
contract: "contract name"
optionPeriods: "dfd"
performanceEndDate: "2022-12-08"
performanceStartDate: "2022-12-08"
proposalDueDate: "2022-12-08T12:54"
requirements: "dfd"
trackingNumber: "dfd"
I hope this helps

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;

Resources