I have a form. It dynamically creates checkboxes with string values based on the list. There is also a text field in the form. With yup I need to validate that at least one checkbox is selected or a textbox contains an input. To create a form I use react-hook-form.
export const schema = yup.object({
items: yup.array().notOneOf([false], 'At least one option is required')
})
const {
register,
formState: {
errors = {},
},
reset,
handleSubmit,
} = useForm({
mode: 'onSubmit',
reValidateMode: 'onSubmit',
shouldUnregister: true,
resolver: yupResolver(schema)
});
const input = register('description');
<form onSubmit={submitForm}>
<div>
<div>
{docs.map((doc, i) => (
<div key={doc}>
<label>
<input type="checkbox"
id={doc}
name={`items[${i}]`}
value={doc}
{...register(`items[${i}]`)} />
</label>
</div>
))}
</div>
<TextField {...input}/>
</div>
</form>
I know that you can somehow use useFieldArray, but an error appears with the fact that initially a boolean value is stored in the checkbox, after selection, a text value is already stored there
Related
I am using React Hook Form. I have this simple form:
A simple form
When I enter values in the "quantity "and "price" fields, the third field, "total" shows the result of multiplying them. So far, all fine. But I have noticed that when I click the submit button the value in the "total" field does not update the data form, unless that previously it get the focus by clicking on it.
This is what I get when I don't click the "total" field:
Showing the state form in the console
As you can see in the last image, the value of the "total" field is not reflected in the form state.
This is my code:
import { useForm } from "react-hook-form"
function App() {
const { register, handleSubmit, watch } = useForm({
defaultValues: {
price: 0,
quantity: 0,
total: 0
}
});
const onSubmit = data => console.log(data)
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="quantity">Quantity: </label>
<input type="number"
{...register("quantity")}
/>
</div>
<div>
<label htmlFor="price">Price: </label>
<input type="number"
{...register("price")}
/>
</div>
{
/** 'total' is the result of multiplying the two previous fields.
* It only updates the form data when it get the focus.
*/
}
<div>
<label htmlFor="total">Total: </label>
<input type="number"
{...register("total")}
value={watch('price') * watch('quantity')}
readOnly
/>
</div>
<input type="submit" value='Submit' />
</form>
</div>
)
}
export default App
I was expecting the form state to update regardless of whether or not the "total" field got focus.
Thanks in advance everyone for your help.
Solved. Thank you #Anurag Srivastava (sorry I'm not allowed to vote yet). This is the final code:
import { useEffect } from "react";
import { useForm } from "react-hook-form"
function App() {
const { register, handleSubmit, setValue, watch } = useForm({
defaultValues: {
price: 0,
quantity: 0,
total: 0
}
});
// I had to add these two lines
const price = watch("price")
const quantity = watch("quantity")
useEffect( ()=> {
setValue('total', price * quantity)
}, [price, quantity, setValue])
const onSubmit = data => console.log(data)
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="quantity">Quantity: </label>
<input type="number"
{...register("quantity")}
/>
</div>
<div>
<label htmlFor="price">Price: </label>
<input type="number"
{...register("price")}
/>
</div>
{
/** 'total' is the result of multiplying the two previous fields.
* It only updates the form data when it get the focus.
*/
}
<div>
<label htmlFor="total">Total: </label>
<input type="number"
{...register("total")}
//value={watch('price') * watch('quantity')}
readOnly
/>
</div>
<input type="submit" value='Submit' />
</form>
</div>
)
}
export default App
Edit: You would need to watch the fields as well, to get their values in the app:
const price = watch("price")
const quantity = watch("quantity")
It might be easier for you to use setValue combined with useEffect:
const { register, handleSubmit, watch, setValue } = useForm({
defaultValues: { price: 0, quantity: 0, total: 0 }
});
...
useEffect(() => {
setValue('total', quantity * price)
}, [quantity, price])
...
<input type="number"
{...register("total")} // No need to watch
readOnly
/>
https://codesandbox.io/s/nice-cohen-k3kdtq?file=/src/App.js Here is the codesandbox example of my code
What I need to do is when I click the 'Preview' button I want to disable validation on last two fields (price, category) and when I click 'Submit' I want to validate all the fields. I tried to change react-hook-form resolver depending on state but it doesn't let me and had an idea about making fields not required when boolean variable from component changes but I don't know how can I send this variable to yup schema
const nftSchema = yup.object().shape({
NFTCollectionAddress: yup
.string()
.required("Collection address is required")
.test("len", "Not a valid address", (val) => val.length === 42)
.matches("0x", "Not a valid address"),
NFTTokenID: yup
.number()
.typeError("You must specify a number")
.required("Token ID is required"),
price: yup
.string()
.required("Price is required")
.test("inputEntry", "The field should have digits only", digitsOnly)
.test(
"maxDigitsAfterDecimal",
"Number cannot have more than 18 digits after decimal",
(number) => /^\d+(\.\d{1,18})?$/.test(number)
),
category: yup.string().required("Category is required")
});
export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm({
resolver: yupResolver(nftSchema),
});
const onSubmit = (data) => {
};
const handlePreview = (data) => {
};
return (
<form>
<h4>Token ID</h4>
<input
name="NFTTokenID"
type="text"
{...register("NFTTokenID")}
/>
<h4>Collection</h4>
<input
name="NFTCollectionAddress"
type="text"
{...register("NFTCollectionAddress")}
/>
<h4>Price</h4>
<input
name="price"
type="text"
{...register("price")}
/>
<h4>Category</h4>
<input
name="category"
type="text"
{...register("category")}
/>
<button onClick={handleSubmit(onSubmit)}>Submit</button>
<button onClick={handleSubmit(handlePreview)}>Preview</button>
</form>
</div>
);
}
What about creating a different schema for preview, and changing the schema passed to yupResolver based on isPreview? On Preview button also would just set the isPreview state, not use the handleSubmit function.
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();
I want to have different validation modes for different inputs. Currently, the form validation uses Yup with the mode: "onTouched", but the problem is this mode applies to all inputs in the form.
I've built a demo with the same principles to demonstrate this: https://codesandbox.io/s/react-playground-forked-8qb0k?file=/Pokemon.js - if you click in any of the 2 inputs and then click away, the error will show (if validation fails that is). I want one of the inputs to not have an onTouched mode. How can this be achieved?
Currently it's set up as follows:
const schema = Yup.object().shape({
name: Yup.string()
.required("Required")
.min(3, "Enter at least 3 characters"),
test: Yup.string()
.required("Required Test")
.min(2, "Enter at least 2 characters")
});
const {
register,
handleSubmit,
setError,
formState: { errors },
trigger,
setValue,
watch,
clearErrors
} = useForm({
resolver: yupResolver(schema),
mode: "onTouched"
// reValidateMode: "onChange"
});
And the form:
<form onSubmit={handleSubmit(onSubmit /*, onError*/)}>
<input
{...register("name", { required: true })}
name="name"
placeholder="Enter a pokemon"
onChange={onNameChange}
/>
{errors.name && <p>{errors.name.message}</p>}
<input
{...register("test", { required: true })}
name="test"
placeholder="test"
/>
{errors.test && <p>{errors.test.message}</p>}
<button type="submit" onClick={onSubmit}>
Show Pokemon
</button>
{errors.namey && <p>{errors.namey.message}</p>}
</form>
Thanks
What's the best practice when doing input fields match validation when dealing with React-hook-form? For example, when matching email inputs, etc.
While looking into email match validation with React-hook-form found an issue while trying to separate error messages from "coupled elements" through their validation method. The ref only takes one argument that is used for React-hook-form register, while needing to use useRef to access the current.value for value matching, as follows:
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
function App() {
const { register, handleSubmit, errors } = useForm();
const inputEmail = useRef(null)
const onSubmit = data => {
console.log('onSubmit: ', JSON.stringify(data))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input
name="email"
type="email"
ref={inputEmail}
/>
{/* desired: show `email` error message */}
<label htmlFor="email">Email confirmation</label>
<input
name="emailConfirmation"
type="email"
ref={register({
validate: {
emailEqual: value => (value === inputEmail.current.value) || 'Email confirmation error!',
}
})}
/>
{errors.emailConfirmation && <p>{errors.emailConfirmation.message}</p>}
<input type="submit" />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
While this pattern seems to be an option when doing input field matching it does not play well with React-hook-form!
For example, the error message is coupled with one input case only and has no separate messages for each independent field, or one of the input fields does not have the register assigned to it, this means that the property required is not set, etc.
So, I'm looking into a good practice or pattern that solves:
Keeping error messages separated by the input field
The validation method, when testing the match should be able to reference the twin field value in a React compliant way and not
through the DOM (document.querySelector, etc)
You shouldn't need the manual ref for inputEmail. Instead, use the getValues method to fetch the current value of your whole form.
const { register, getValues } = useForm()
Then you register both inputs and call getValues from your custom validation.
<input
name="email"
type="email"
ref={register}
/>
<input
name="emailConfirmation"
type="email"
ref={register({
validate: {
emailEqual: value => (value === getValues().email) || 'Email confirmation error!',
}
})}
/>
For this you could use Yup library, which is great:
Add validationSchema to your config object when instantiating useForm and pass a valid Yup schema. Like so:
const Schema = yup.object().shape({
email: yup.string().required('Required field'),
emailConfirmation: yup
.string()
.oneOf([yup.ref('email')], 'Emails must match')
.required('Required field'),
});
// How to add it to your useForm
const { register } = useForm({
validationSchema: Schema
})
Your component should look something like this:
function App() {
const { register, handleSubmit, errors } = useForm({
validationSchema: Schema
});
const onSubmit = data => {
console.log('onSubmit: ', JSON.stringify(data))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input
name="email"
type="email"
ref={register}
/>
{/* desired: show `email` error message */}
<label htmlFor="email">Email confirmation</label>
<input
name="emailConfirmation"
type="email"
ref={register}
/>
{errors.emailConfirmation && <p>{errors.emailConfirmation.message}</p>}
<input type="submit" />
</form>
);
}