React Hook Form Multiple Dropdown Validation - reactjs

Currently I am using semantic ui css multiple dropdown validation. How do I validate the dropdown so that user has to choose at least ONE option before submitting ?
Right now the current output is that if I submit the form WITHOUT choosing any option, the error message appears.
However, if I choose an option and the submit, the error message still appears.
Following is my code.
import React, { useState } from 'react'
import { Button, Checkbox, Form, Dropdown } from 'semantic-ui-react'
//validation dependencies
import { useForm, Controller } from "react-hook-form";
import { yupResolver } from '#hookform/resolvers/yup';
import * as Yup from 'yup';
function TestForm() {
const validationSchema = Yup.object().shape({
firstName: Yup.string()
.required('firstName is required')
.min(6, 'firstName must be at least 6 characters')
,
options: Yup
.object()
.shape({
key: Yup.number().required("key is required (from label)"),
value: Yup.string().required("Value is required")
})
.nullable() // for handling null value when clearing options via clicking "x"
.required("options is required (from outter null check)")
});
const formOptions = { mode: 'all', reValidateMode: 'onChange', resolver: yupResolver(validationSchema) };
const { register, handleSubmit, reset, formState: { errors }, watch, setValue, control } = useForm(formOptions);
const [stateOptions, setStateOptions] = useState([
{
key: 1,
text: 'aaaa',
value: 'a'
},
{
key: 2,
text: 'bbbb',
value: 'b'
},
{
key: 3,
text: 'cccc',
value: 'c'
},
]);
console.log(watch())//For Debugging
const onSubmitHandler = (data) => {
console.log({ data });
setValue('firstName', 'dash', { shouldDirty: true, shouldValidate: true })
}
return (
<div>
<Form reply onSubmit={handleSubmit(onSubmitHandler)}>
<Form.Field>
<label>First Name</label>
<input placeholder='First Name' name="firstName" {...register('firstName')} />
<div className="invalid-feedback">{errors.firstName?.message}</div>
</Form.Field>
<Form.Field>
<Controller
name="options"
control={control}
render={({ field }) => (
<Dropdown
{...field}
placeholder='State'
fluid
multiple
search
selection
options={stateOptions}
/>
)}
/>
<div className="invalid-feedback">{errors.options?.message || errors.options?.value.message}</div>
</Form.Field>
<Form.Field>
<Button type='submit'>Submit</Button>
</Form.Field>
</Form>
</div>
)
}
export default TestForm

Since Dropdown component in semantic-ui-react doesn't support ref attribute, so you need to controll the value of DropDown by yourself, here is an example you can try on it:
const options = [
{
key: 1,
text: 'aaaa',
value: 'a',
},
{
key: 2,
text: 'bbbb',
value: 'b',
},
{
key: 3,
text: 'cccc',
value: 'c',
},
]
function TestForm() {
const validationSchema = Yup.object().shape({
firstName: Yup.string()
.required('firstName is required')
.min(6, 'firstName must be at least 6 characters'),
options: Yup.array()
.of(Yup.object()
.shape({
key: Yup.number().required('key is required (from label)'),
value: Yup.string().required('Value is required'),
}))
.test(
"required",
"options is required",
(value) => Array.isArray(value) && value.length > 0
),
});
const formOptions = {
mode: 'all',
reValidateMode: 'onChange',
resolver: yupResolver(validationSchema),
};
const {
register,
handleSubmit,
formState: { errors },
setValue,
control,
} = useForm(formOptions);
const onSubmitHandler = (data) => {
console.log(data);
};
return (
<div>
<Form reply onSubmit={handleSubmit(onSubmitHandler)}>
<Form.Field>
<label>First Name</label>
<input
placeholder="First Name"
name="firstName"
{...register('firstName')}
/>
<div className="invalid-feedback">{errors.firstName?.message}</div>
</Form.Field>
<Form.Field>
<Controller
name="options"
control={control}
render={({ field }) => {
let { value, ...other } = field;
return (
<Dropdown
{...other}
placeholder="State"
fluid
multiple
search
selection
options={options}
value={Array.isArray(value) ? value.map(v => v.value) : []}
onChange={(e,{value})=> {
const values = value.map(v => options.find(item => item.value == v));
setValue('options', values)
}}
/>
);
}}
/>
<div className="invalid-feedback">
{errors.options?.message || errors.options?.value.message}
</div>
</Form.Field>
<Form.Field>
<Button type="submit">Submit</Button>
</Form.Field>
</Form>
</div>
);
}

Related

react-hook-form use mutiple resolver

i am trying to create a general input like this
const TextInput = ({
name,
register = () => {},
errors,
serverErrors,
...props
}) => {
return (
<div >
<input
type="text"
{...register(name, {
pattern: { value: /^[A-Za-z]+$/i, message: "Invalid Input" },
})}
{...props}
/>
{errors?.[name] && (
<span className="text-errorColor">{errors?.[name]?.message}</span>
)}
</div>
);
};
I will use this input in form and use Yup to validate this form
const schema = yup
.object({
first_name: yup
.string()
.required("This field is required")
.max(20, "max 20 characters"),
})
.required();
const SignupForm = ({ signUpContact, signUpType }) => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<TextInput
name="first_name"
register={register}
errors={errors}
serverErrors={error}
placeholder="First Name"
/>
</form>
);
};
but the problem is that the validations in TextInput Competent aren't running
i think i can't use Register Validation with Yup validation.
as you see I won't duplicate validation [A-Za-z] every time I use TextInput, is there any way to do this?
For input just register the field using react-hook-form
<input
type="text"
{...register(name)}
{...props}
/>
Use yup to handle all the validation logic
yup
.string()
.trim()
.matches(/^[A-Za-z]+$/i , 'Invalid Input')
.max(20, "max 20 characters"),
.required();

How to show error message using react-hook-form in react

How to add validation, I want show error message on for phone number field (helperText, min 10 , max 10 ) and also show error message if submitted without 10 numeric digit
import ReactPhoneInput from "react-phone-input-2"
import {TextField,Button}from "#material-ui/core"
const {register, handleSubmit,formState: { errors }} = useForm()
const getData= (data) => {
console.log(data.username)
console.log(data.phone);
}
<form onSubmit={handleSubmit(getData)} >
<Controller
control={control}
name="phone"
render={({ field: { ref, ...field } }) => (
<ReactPhoneInput {...field}
inputExtraProps={{ref, required: true, autoFocus: true }}
country={"in"}
onlyCountries={["in"]}
countryCodeEditable={false}
specialLabel={"Player Mobile Number"}
/>
)}
/>
<Button type='submit>Submit</Button>
</form>
You can use the rules prop of the <Controller /> component to define your validation rules. Check the rules section here for more info.
To display the errors you have to use formState object returned by useForm.
export default function App() {
const {
control,
handleSubmit,
formState: { errors }
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
const isNumber = (number) => !isNaN(number) || "Must be a number";
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="phone"
rules={{
required: { value: true, message: "Required" },
minLength: { value: 12, message: "Min Length" },
maxLength: { value: 12, message: "Max Length" },
validate: isNumber
}}
render={({ field: { ref, ...field } }) => (
<ReactPhoneInput
{...field}
inputExtraProps={{
ref,
required: true,
autoFocus: true
}}
country={"in"}
onlyCountries={["in"]}
countryCodeEditable={false}
specialLabel={"Player Mobile Number"}
/>
)}
/>
{errors.phone && <p>{errors.phone.message}</p>}
<input type="submit" />
</form>
);
}

Move to next input after maxlength react functional component

I am Using react-Hook-form, I have a scenario for a phone number that has three Input Field XXX-XXX-XXXX. I want to shift focus from the first input to the next after it reaches maxlength===3.please suggest a solution to auto Focus on the next phone input after reaches max length. working on react functional component.
Here is the code,
import React from 'react';
import { useForm } from "react-hook-form";
export function PhoneInput(props) {
const {
register,
handleSubmit,
watch,
clearErrors,
formState: { errors }
} = useForm();
return (
<form className="review-your-info-form" id="purchase-form" onSubmit={handleSubmit(onSubmit)} noValidate>
<div className="field">
{ (errors.phone1 || errors.phone2 || errors.phone3) && <div className="error-msg"><span className="required">{errors.phone1?.message || errors.phone2?.message || errors.phone3?.message}</span></div>}
<label htmlFor="phone1">Phone *</label>
<div className="phone-group flex">
<input
id="phone1"
type="tel"
className="phone1"
maxLength="3"
onClick={() => clearErrors("phone1")}
{...register("phone1",{
required: 'phone-error',
minLength: {
value: 3,
message: 'phone-error'
},
pattern: {
value: /[2-9][0-9]{2}$/,
message: 'phone-error'
},
})}
/>
<input
type="tel"
className="phone2"
maxLength="3"
onClick={() => clearErrors("phone2")}
{...register("phone2",{
required: 'phone-error',
minLength: {
value: 3,
message: 'phone-error'
},
pattern: {
value: /[2-9][0-9]{2}$/,
message: 'phone-error'
},
})}
/>
<input
type="tel"
className="phone3"
maxLength="4"
onClick={() => clearErrors("phone3")}
{...register("phone3",{
required: 'phone-error',
minLength: {
value: 4,
message: 'phone-error'
},
pattern: {
value: /^[0-9]*$/i,
message: 'phone-error'
},
})}
/>
</div>
</div>
</form>
)}```
To move focus in React you could use useRef hook:
import React, { useState, useRef } from 'react';
import { useForm } from "react-hook-form";
export function PhoneInput(props) {
const [input, setInput] = useState();
const {
register,
handleSubmit,
watch,
clearErrors,
formState: { errors }
} = useForm();
const inputToFocus = useRef(); //<-- create ref
const handleChange = (e) => {
setInput(e.target.value);
if (e.target.value.length >= 3) inputToFocus.current.focus(); //<--- focus second input
}
return (
<form className="review-your-info-form" id="purchase-form" onSubmit={handleSubmit(onSubmit)} noValidate>
<div className="field">
{ (errors.phone1 || errors.phone2 || errors.phone3) && <div className="error-msg"><span className="required">{errors.phone1?.message || errors.phone2?.message || errors.phone3?.message}</span></div>}
<label htmlFor="phone1">Phone *</label>
<div className="phone-group flex">
<input
value={input}
onChange={handleChange} //<-- edit input
id="phone1"
type="tel"
className="phone1"
maxLength="3"
onClick={() => clearErrors("phone1")}
{...register("phone1",{
required: 'phone-error',
minLength: {
value: 3,
message: 'phone-error'
},
pattern: {
value: /[2-9][0-9]{2}$/,
message: 'phone-error'
},
})}
/>
<input
ref={inputToFocus} //<-- assing ref
type="tel"
className="phone2"
maxLength="3"
onClick={() => clearErrors("phone2")}
{...register("phone2",{
required: 'phone-error',
minLength: {
value: 3,
message: 'phone-error'
},
pattern: {
value: /[2-9][0-9]{2}$/,
message: 'phone-error'
},
})}
/>
<input
type="tel"
className="phone3"
maxLength="4"
onClick={() => clearErrors("phone3")}
{...register("phone3",{
required: 'phone-error',
minLength: {
value: 4,
message: 'phone-error'
},
pattern: {
value: /^[0-9]*$/i,
message: 'phone-error'
},
})}
/>
</div>
</div>
</form>
)}
Explanation: the first input has to pass the focus to the second input when value reach length of 3 so:
I create a inputToFocus ref and assign to the second input;
then I create handleChange function to: set the first input value and, in case of value length >= 3, move the focus to second input using ref previously created.

Controller submitting undefined with React Select

I'm trying to use React Select with useForm and got stuck when submitting form values.
My goal is a multi value react select with a default value and onChange function to do some validations when changing (like limit the number of itens to 3).
I already tried to search the solution in others posts and did make some changes in my code but unfortunately I did not succeed.
Everything seems to work perfectly but when I submit the form, my controller results in undefined value.
import React, {useEffect, useState, useContext} from 'react'
import {useForm, Controller} from 'react-hook-form'
import axios from 'axios';
import Select from 'react-select';
import BeeUtils from '../../../utils/BeeUtils'
export default function EditCategory2({place, goBack}){
var messageFieldRequired = 'Campo Obrigatório';
const audienceOptions = [
{ value: 'Lésbicas', label: 'Lésbicas' },
{ value: 'Gays', label: 'Gays' },
{ value: 'Bissexuais', label: 'Bissexuais' },
{ value: 'Transexuais', label: 'Transexuais' },
{ value: 'Queer', label: 'Queer' },
{ value: 'Intersexo', label: 'Intersexo' },
{ value: 'Assexual', label: 'Assexual' },
{ value: 'Héteros', label: 'Héteros' },
{ value: 'Todxs', label: 'Todxs' }
]
const handleAudienceSelector = (e) => {
console.log('OK');
console.log(e);
if(e.length > 3){
e.pop();
alert('max of 3 selected');
}
}
const {register , handleSubmit, errors, setValue, getValues, setError, control} = useForm();
const requestUpdate = async (data) => {
data.createdBy = place.createdBy;
data._id = place._id;
data.recordUpdatedType = 'audience';
console.log(data);
return;
}
const selectRequired = (e) => {
console.log(e);
console.log('OK-2');
//var error = e.length == 0? messageFieldRequired : '';
//return error;
}
const onSubmit = data => {
console.log(data)
requestUpdate(data);
}
return (
<div className='cad-form'>
<form onSubmit={handleSubmit(onSubmit)}>
<div className='cad-tit-container'>
<span className='cad-titulo'> Edit Here</span>
</div>
<div className='cad-container'>
<label htmlFor='test-audience'>Audience</label>
<Controller
name="test-audience"
control={control}
rules={{ validate: selectRequired }}
render={() => (
<Select
defaultValue={[audienceOptions[0], audienceOptions[1]]}
isMulti
onChange={handleAudienceSelector}
placeholder='Select Itens'
options={audienceOptions}
className="basic-multi-select selectCustom"
classNamePrefix="select"
/>
)}
/>
{errors?.targetAudience && <p>{errors.targetAudience.message}</p>}
</div>
<div className='btn-container'>
<div className='cad-btn'><button onClick={(e) => goBack('initial')} className="btn waves-effect yellow darken-2">Voltar</button></div>
<div className='cad-btn'><button type='submit' className="btn waves-effect yellow darken-2">Salvar Alterações</button></div>
</div>
</form>
</div>
)
}
After some changes (thanks to help of the answer) I tried this code
import React, {useEffect, useState, useContext} from 'react'
import {useForm, Controller} from 'react-hook-form'
import axios from 'axios';
import Select from 'react-select';
import BeeUtils from '../../../utils/BeeUtils'
export default function EditCategory2({place, goBack}){
var messageFieldRequired = 'Campo Obrigatório';
const audienceOptions = [
{ value: 'Lésbicas', label: 'Lésbicas' },
{ value: 'Gays', label: 'Gays' },
{ value: 'Bissexuais', label: 'Bissexuais' },
{ value: 'Transexuais', label: 'Transexuais' },
{ value: 'Queer', label: 'Queer' },
{ value: 'Intersexo', label: 'Intersexo' },
{ value: 'Assexual', label: 'Assexual' },
{ value: 'Héteros', label: 'Héteros' },
{ value: 'Todxs', label: 'Todxs' }
]
const handleAudienceSelector = (e) => {
console.log('OK');
console.log(e);
if(e.length > 3){
e.pop();
alert('max of 3 selected');
}
}
const {register , handleSubmit, errors, setValue, getValues, setError, control} = useForm();
const requestUpdate = async (data) => {
data.createdBy = place.createdBy;
data._id = place._id;
data.recordUpdatedType = 'audience';
console.log(data);
return;
}
const onSubmit = data => {
console.log(data)
requestUpdate(data);
}
return (
<div className='cad-form'>
<form onSubmit={handleSubmit(onSubmit)}>
<div className='cad-tit-container'>
<span className='cad-titulo'> Edit Here</span>
</div>
<div className='cad-container'>
<label htmlFor='test-audience'>Audience</label>
<Controller
name="targetAudience"
control={control}
defaultValue={[audienceOptions[0], audienceOptions[1]]}
rules={{ required: messageFieldRequired }}
render={({ field: { onChange, value } }) => (
<Select
value={value}
onChange={onChange}
isMulti
placeholder="Select Itens"
options={audienceOptions}
className="basic-multi-select selectCustom"
classNamePrefix="select"
/>
)}
/>
</div>
<div className='btn-container'>
<div className='cad-btn'><button onClick={(e) => goBack('initial')} className="btn waves-effect yellow darken-2">Voltar</button></div>
<div className='cad-btn'><button type='submit' className="btn waves-effect yellow darken-2">Salvar Alterações</button></div>
</div>
</form>
</div>
)
}
But now I got the error: TypeError: Cannot read property 'onChange' of undefined
The reason why it isn't working is because you forgot to to link the <Controller /> component with the <Select /> component via the value and onChange properties of the render prop function from <Controller />.
<Controller
name="targetAudience"
control={control}
defaultValue={[audienceOptions[0], audienceOptions[1]]}
rules={{ required: "Campo obrigatório", validate: isOnly3Values }}
render={({ field: { onChange, value } }) => (
<Select
value={value}
onChange={onChange}
isMulti
placeholder="Select Itens"
options={audienceOptions}
className="basic-multi-select selectCustom"
classNamePrefix="select"
/>
)}
/>
You also don't need to use useState here for handling the error state as RHF already provides this functionality. For setting a field to be required, you can just set the required property of the validation object which can be passed to <Controller /> via the rules prop. Check here for more information about <Controller />. I would suggest to also use the validate function from RHF to check if the user added more than 3 items to your <Select /> and display an error message instead of using an alert.
I made some overall small changes and corrected some minor issues (e.g. the errors object is located in the formState property since v7). Feel free to write a comment if something isn't clear.

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.

Resources