I have yup validation in my form. I want to validate if user compleate minimum 2 of 3 inputs. When user not compleate i want change only a color of text to red. How can i setup my schema to do that. Here's my form:
<form>
{!loading ? (
<Paper title={t('common:cardTitles.extendDevice')} {...register('extendDevice')}>
<div className="flex md:flex-row flex-col justify-center items-center md:justify-between border-b-[1px] md:pb-6 pb-[17px]">
<h3 className="font-bold subheading">{t('common:deviceData')}</h3>
<div className="flex flex-row gap-1">
<ErrorIcon className="text-[#80858F] w-6" />
<p
className={mergeClasses(
'md:text-sm text-xs text-[#80858F]',
errors.extendDevice ? 'text-red-600' : 'text-[#80858F]',
)}
>
{t('common:errors.atLeastTwoFields')}
</p>
</div>
</div>
<div className="md:pt-6 pt-[17px] grid md:grid-cols-2 gap-4">
<TextInput
{...register('serialNumber')}
label={t('common:labels.serialNumber')}
name="serialNumber"
placeholder={t('common:placeholders.typeSerialNumber')}
/>
<TextInput
{...register('businessNumber')}
label={t('common:labels.businessNumber')}
name="business-number"
placeholder={t('common:placeholders.typeBusinessNumber')}
/>
</div>
<div className="pt-4">
<NumberInput
{...register('pinNumber')}
label={t('common:labels.pin')}
name="pin-number"
placeholder={t('common:placeholders.typePinNumber')}
/>
</div>
<div className="flex w-full justify-end pt-4">
<Button
className="py-3.5 px-6 bg-[#336BB3] text-white md:w-[115px] w-full"
label={t('common:labels.verify')}
onClick={handleSubmit(onSubmit)}
type="submit"
/>
</div>
</Paper>
) : (
<VerificationInProgress />
)}
</form>
You can write a test in YUP to check if specific fields have been entered.
const formValidation = Yup.object().shape({
name: Yup.string().test(
"oneOfRequired",
"One of the three fields need to be filled in",
function (item) {
return this.parent.name || this.parent.surname || this.parent.country;
}
),
surname: Yup.string().test(
"oneOfRequired",
"One of the three fields need to be filled in",
function (item) {
return this.parent.name || this.parent.surname || this.parent.country;
}
),
country: Yup.string().test(
"oneOfRequired",
"One of the three fields need to be filled in",
function (item) {
// Check if one of them have been filled in
return this.parent.name || this.parent.surname || this.parent.country;
}
)
});
Here is a codesandbox : https://codesandbox.io/s/goofy-bell-ji28jy?file=/src/App.js:279-302
Related
The below code is checking the checkboxes one at a time each time select-all is clicked.
I've used the same code in SelectAllStudents elsewhere in my application not using Formik and it works fine. It processes each item in checkboxes.forEach but only checks the last item in the list on each click and I need to know how to get this to check all on a single click
const selectAllStudents = (contactType: string) => {
const checkboxes = document.querySelectorAll("#studentEmail");
var nativeInputValueSetter = Object?.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"checked"
)?.set;
checkboxes.forEach((checkbox) => {
nativeInputValueSetter?.call(checkbox, true);
let event = new Event("click", { bubbles: true })
checkbox.dispatchEvent(event);
});
})
this is the return markup from <StudentCheckBoxes / >
{students!.map((student, idx) => {
return (
<div key={idx+student.personId}>
<div className="inline-flex items-center">
<Field
type="checkbox"
className="w-4 h-4"
value={student.studentId.toString()}
name={"students"}
id={"studentEmail"}
/>
<span className="ml-2 text-sm">
{student.preferredName} {student.surname}
</span>
</div>
</div>
);
})}
Initial part of the form up to the select-all button
return (
<div>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validateOnChange={true}
validationSchema={validationSchema}
className="text-gray-700"
>
{({ values, errors }) => (
<Form className="container pl-1 pr-3 pb-3 mx-auto">
{/* uncomment below to output form and error values */}
<pre>{JSON.stringify(values, null, 2)}</pre>
<pre>{JSON.stringify(errors, null, 2)}</pre>
<div className="flex-col text-center w-full mb-6 hidden 2xl:block">
<h1 className="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">
Email
</h1>
<p className="">Send E-Mail to Group</p>
</div>
{!loadingStudents && filteredStudents.length > 0 && (
<div className="grid grid-cols-2 mb-3">
<StudentCheckboxes students={students} contactType={contactType} />
</div>
)}
<div className="mb-2 ml-3 text-error-red error">
{errors.students}
</div>
<p onClick={() => selectAllStudents(contactType)} className="mb-4 text-sm underline cursor-pointer">Select All</p>
I am building a component where I am grouping cards by regions. I have a checkbox by the region name and when its checked I want to be able to select all the cards in the region.
This is how my cards are displayed by region:
<div className="pl-14 text-black font-nunito mt-8 ml-3 text-2xl">
{_.map(_.keysIn(groups), (region) => {
return (
<>
<div className="flex justify-between">
<div className="flex">
<Checkbox
className="bg-transparent shadow-none text-black hover:bg-transparent"
color="primary"
/>
<p className="mt-1">{_.capitalize(region)}</p>
</div>
<div className="pr-16">
<Button variant="text" className="text-lightBlue">
View More
</Button>
</div>
</div>
<UserCards groupedUser={groups[region]} />
</>
);
})}
</div>
And my cards look like this:
const UserCards = ({ groupedUser }) => {
return groupedUser.map((user) => (
<div className="cardContainer">
<div className="card shadow-customShadow">
<Checkbox
key={user.email}
className="bg-transparent shadow-none text-black mb-14 hover:bg-transparent"
color="primary"
onChange={(event) => {
if (event.target.checked) {
const data = [...userEmails];
data.push(user.email);
setUserEmails(data);
} else {
const data = [...userEmails];
const index = data.indexOf(user.email);
data.splice(index, 1);
setUserEmails(data);
}
}}
checked={_.includes(userEmails, user.email)}
/>
<div>
<div className="mt-2 text-lg">
{information stored here}
</div>
<div className="mt-2 text-lg">
{information stored here}
</div>
</div>
</div>
</div>
));
};
How can I tell the region check box to check all the cards?
I am trying to implement reCAPTCHA using the react-hook-form in combination with react-hook-recaptcha Not sure what is the reason but I am getting the following error for window saying it's undefined:
ReferenceError: window is not defined
> 33 | const { recaptchaLoaded, recaptchaWidget } = useRecaptcha({
Code:
import React, { useState } from "react";
import { useRecaptcha } from "react-hook-recaptcha";
import { useForm, SubmitHandler } from "react-hook-form";
import { urlFor } from "#lib/sanity";
import { dateFormat } from "#lib/helpers";
import { PortableText } from "#portabletext/react";
import { portableTextComponents } from "#components/portable-text";
import { FormError, FormSuccess } from "#components/Form";
import { ArticleProps, Comment } from "types/article";
const sitekey = "6Ld-*********"; // change to your site key
const containerId = "recaptcha"; // this id can be customized
declare global {
interface Window {
grecaptcha: any;
}
}
const ArticleSingle = ({ page }: ArticleProps) => {
const [submitted, setSubmitted] = useState(false);
const { _id, body, featuredImage, title, author, publishedAt, comments } =
page;
const successCallback = (response: any) => {
console.log("YUP");
};
const { recaptchaLoaded, recaptchaWidget } = useRecaptcha({
containerId,
successCallback,
sitekey,
size: "invisible",
});
const executeCaptcha = () => {
if (recaptchaWidget !== null) {
window.grecaptcha.reset(recaptchaWidget);
window.grecaptcha.execute(recaptchaWidget);
}
};
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Comment>();
const onSubmit: SubmitHandler<Comment> = async (e, data) => {
e.preventDefault();
executeCaptcha();
fetch("/api/create-comment", {
method: "POST",
body: JSON.stringify(data),
})
.then(() => {
console.log(data);
setSubmitted(true);
})
.catch((error) => {
console.log(error);
setSubmitted(false);
});
};
return (
<div id="article">
<div className="relative mb-4 md:mb-0" style={{ height: "45rem" }}>
<div
className="absolute bottom-0 left-0 z-10 w-full h-full"
style={{
backgroundImage:
"linear-gradient(180deg,transparent,rgba(0,0,0,.75))",
}}
></div>
{featuredImage && featuredImage.asset && (
<img
alt={featuredImage?.alt || ""}
src={urlFor(featuredImage).quality(85).url()}
className="absolute top-0 left-0 z-0 object-cover w-full h-full"
/>
)}
<div className="absolute left-0 right-0 z-20 max-w-screen-lg p-4 mx-auto bottom-2">
{title && (
<h1 className="text-4xl font-semibold leading-tight text-gray-100 md:text-5xl lg:text-7xl">
{title}
</h1>
)}
<div className="flex gap-5 mt-5 place-items-center">
{author?.featuredImage && (
<img
className="object-cover object-center w-12 h-12 border-2 border-white rounded-full shadow-lg md:w-16 md:h-16"
alt={author?.featuredImage?.alt || ""}
src={urlFor(author.featuredImage).quality(85).url()!}
/>
)}
<div>
{author && (
<p className="font-semibold text-gray-200 sm:text-md md:text-xl">
{author.name}
</p>
)}
{publishedAt && (
<time className="font-semibold text-bubblegum sm:text-md md:text-xl">
{dateFormat(publishedAt)}
</time>
)}
</div>
</div>
</div>
</div>
<div className="max-w-screen-lg px-4 pb-8 mx-auto mt-12 text-navyBlue text-md md:text-xl md:leading-relaxed">
<PortableText value={body} components={portableTextComponents} />
{/* Comment Form */}
<div id="article-comments">
<hr className="my-5 mb-10 border border-yellow-500" />
{submitted ? (
<FormSuccess
title="Thank you for submitting your comment!"
message="Once it has been approved, it will appear below!"
/>
) : (
<>
<h3 className="text-md text-bubblegum">Enjoyed this article?</h3>
<h4 className="text-3xl font-bold">Leave a comment below!</h4>
<hr className="py-3 mt-2" />
<form
className="flex flex-col pb-5 mx-auto"
onSubmit={handleSubmit(onSubmit)}
>
<input
{...register("_id")}
type="hidden"
name="_id"
value={_id}
/>
<input
{...register("name", {
required: "The Name Field is required",
})}
className="block w-full px-3 py-2 mt-4 border rounded shadow outline-none form-input ring-bubblegum focus:ring"
placeholder="Name"
type="text"
/>
{errors.name && <FormError message={errors.name.message} />}
<input
{...register("email", {
required: "The Email Field is required",
pattern: {
value: /^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: "Enter a valid e-mail address",
},
})}
className="block w-full px-3 py-2 mt-4 border rounded shadow outline-none form-input ring-bubblegum focus:ring"
placeholder="Email"
type="email"
/>
{errors.email && <FormError message={errors.email.message} />}
<textarea
{...register("comment", {
required: "The Comment Field is required",
minLength: {
value: 40,
message: "A Min of 40 characters is required",
},
})}
className="block w-full px-3 py-2 mt-4 border rounded shadow outline-none resize-none form-textarea ring-bubblegum focus:ring"
placeholder="Comment"
rows={7}
/>
{errors.comment && (
<FormError message={errors.comment.message} />
)}
<input
disabled={!recaptchaLoaded}
type="submit"
className="flex-none w-32 px-4 py-2 mt-6 text-xs font-bold text-center uppercase duration-300 ease-in-out bg-transparent border-2 rounded-full cursor-pointer text-navyBlue border-navyBlue hover:bg-navyBlue hover:text-bubblegum"
></input>
</form>
{/* Comments */}
{comments.length > 0 && (
<div className="flex flex-col my-10">
<h3 className="text-4xl">Comments</h3>
<hr className="mt-2 mb-5 border-2 border-yellow-500" />
{comments.map(({ name, _id, _createdAt, comment }) => (
<div
className="px-4 py-6 bg-white rounded-sm shadow-md"
key={_id}
>
<p>
<time className="block text-sm font-bold">
{dateFormat(_createdAt)}{" "}
</time>
<span className="text-bubblegum">{name}</span> :
<span className="pl-2">{comment}</span>
</p>
</div>
))}
</div>
)}
</>
)}
</div>
</div>
</div>
);
};
export default ArticleSingle;
Next.js has a server-side environment, and window object only exists in the browser. You can check which environment code is running with this:
const isServerSide = typeof window === 'undefined'
So your code will probably look like:
const executeCaptcha = () => {
if (recaptchaWidget !== null && typeof window !== 'undefined') {
window.grecaptcha.reset(recaptchaWidget);
window.grecaptcha.execute(recaptchaWidget);
}
};
I created this form component in react with validation using yup and react hook form. I also created a tab like structure inside the form to avoid the vertical scroll. There's two option on how can a user change the tab by pressing the next button or dotted navigation below. The issue is I can't think a solution on how can I validate those input field after the user decided to click the dotted navigation or the next button without typing on the input field. All the validation is being done after the user click the submit button. The output that I want is if the user decided to click the next or dotted navigation without providing a data the form will trigger the validation without pressing the onsubmit.
Thanks!
const schema = yup.object().shape({
projectName: yup.string().required('Please enter project name here'),
projectClient: yup
.string()
.required('Please choose your client for this project'),
});
const createProject = () => {
const [projectInfo, setProjectInfo] = useState({
projectName: '',
projectClientName: '',
});
const { register, errors, handleSubmit } = useForm({
resolver: yupResolver(schema),
mode: 'all',
});
}
const handleOnSubmit = () => {
dispatch(registerProject(projectInfo));
};
return (
<form
className='form-container w-full flex flex-col relative'
onSubmit={handleSubmit(handleOnSubmit)}
>
<div
className={`w-full ${
tabActive === 'Create' ? 'block' : 'hidden'
}`}
>
<div className='relative rounded-tl-lg rounded-tr-lg'>
<h4 className='text-grey text-lg font-bold text-center tracking-wider'>
Create Project
</h4>
<XIcon
className='w-5 h-5 absolute top-0 right-0 opacity-40 cursor-pointer'
aria-hidden='true'
onClick={() => showProjectCreate(false)}
/>
</div>
<div className='pt-10'>
<label className='text-sm font-medium' htmlFor='password'>
Project name
</label>
<div className='mt-2 relative'>
<InputElement
className={`w-full px-4 py-5 text-sm leading-tight rounded-md appearance-none ${
errors.projectName
? 'border-danger focus:border-danger'
: 'border focus:border-primary'
} border focus:border-primary focus:outline-none focus:shadow-outline`}
id='projectName'
name='projectName'
type='text'
placeholder='Enter your project name here'
ref={register}
value={projectInfo.projectName}
onChange={handleOnChange}
/>
<p className='text-xxs text-danger mt-2 absolute'>
{errors.projectName?.message}
</p>
</div>
</div>
<div className='pt-10'>
<label className='text-sm font-medium' htmlFor='projectClientName'>
Project client name
</label>
<div className='mt-2 relative'>
<InputElement
className={`w-full px-4 py-5 text-sm leading-tight rounded-md appearance-none ${
errors.projectName
? 'border-danger focus:border-danger'
: 'border focus:border-primary'
} border focus:border-primary focus:outline-none focus:shadow-outline`}
id='projectClientName'
name='projectClientName'
type='text'
placeholder='Enter your project name here'
ref={register}
value={projectInfo.projectClientName}
onChange={handleOnChange}
/>
<p className='text-xxs text-danger mt-2 absolute'>
{errors.projectName?.message}
</p>
</div>
</div>
</div>
</form>
)
}
I think you can do the validation Onblur event ?
I have three components for form:
CreateInvoice.js (Parent component Form)
Item.js (Child Component)
Input.js (simple input with some styling)
When I try to access the register method to the Item.js file using this code const { register } = useFormContext();, it shows me this error message
TypeError: register is not a function
Input
src/components/Input.js:5
2 |
3 | const Input = ({ inputName, readOnly, register }) => {
4 | return (
> 5 | <input {...(readOnly && { disabled: true, readOnly, value: 0 })} type="text" id={inputName} className="w-full bg-primaryOne p-3 rounded-md shadow-md border border-borderOne focus:outline-none focus:border-secondaryOne transition text-white font-bold text-xs" {...register(inputName, { required: true })} />
6 | )
7 | }
8 |
But When I directly call useForm in Item.js, it doesn't show me an error message and when I add a new item to the form, it appends an empty object to the invoices array like this.
Anyone, please help me to fix this.
Input component code(Input.js):
import React from 'react'
const Input = ({ inputName, readOnly, register }) => {
return (
<input {...(readOnly && { disabled: true, readOnly, value: 0 })} type="text" id={inputName} className="w-full bg-primaryOne p-3 rounded-md shadow-md border border-borderOne focus:outline-none focus:border-secondaryOne transition text-white font-bold text-xs" {...register(inputName, { required: true })} />
)
}
export default Input
Whole form code(CreateInvoice.js):
import React, { useState } from 'react'
import { useForm, Controller, useFieldArray, FormProvider } from "react-hook-form";
import DatePicker from 'react-datepicker'
import "react-datepicker/dist/react-datepicker.css";
import Label from './Label';
import Input from './Input';
import Item from './Item';
// import { useDispatch } from 'react-redux';
// import { createInvoice } from '../actions/invoices';
const CreateInvoice = ({ openForm, setOpenForm }) => {
const { register, control, handleSubmit, errors } = useForm();
const [newItems, setNewItems] = useState([]);
const { fields, append, remove } = useFieldArray({
control,
name: "invoices"
});
// const dispatch = useDispatch();
const onSubmit = data => {
console.log(data);
// dispatch(createInvoice(data));
};
return (
<div className={`transition ${!openForm ? 'transform translate-x-full hidden' : '-translate-x-full'}`}>
<div className="fixed top-0 left-0 flex items-center justify-center w-full h-screen z-10" onClick={() => setOpenForm(!openForm)}></div>
<div className="fixed top-0 left-0 z-20 ml-24">
<FormProvider >
<form onSubmit={handleSubmit(onSubmit)} className="w-screen max-w-2xl h-screen bg-primaryTwo p-14">
<h1 className="text-white text-2xl font-bold mb-10">Create Invoice</h1>
<div className="overflow-scroll w-full h-full flex flex-col pr-7 content-area pb-10">
<small className="text-secondaryTwo font-bold text-xs">Bill Form</small>
<div>
<Label labelName="Street Address" />
<Input inputName="streetAddress" register={register} />
</div>
<div className="flex justify-between flex-wrap">
<div>
<Label labelName="City" />
<Input inputName="city" register={register} />
</div>
<div>
<Label labelName="Post Code" />
<Input inputName="postCode" register={register} />
</div>
<div>
<Label labelName="Country" />
<Input inputName="country" register={register} />
</div>
</div>
<small className="text-secondaryTwo font-bold text-xs mt-8">Bill To</small>
<div>
<Label labelName="Client Name" />
<Input inputName="clientName" register={register} />
</div>
<div>
<Label labelName="Client Email" />
<Input inputName="clientEmail" register={register} />
</div>
<div>
<Label labelName="Street Address" />
<Input inputName="clientStreetAddress" register={register} />
</div>
<div className="flex flex-wrap justify-between">
<div>
<Label labelName="City" />
<Input inputName="clientCity" register={register} />
</div>
<div>
<Label labelName="Post Code" />
<Input inputName="clientPostCode" register={register} />
</div>
<div>
<Label labelName="Country" />
<Input inputName="clientCountry" register={register} />
</div>
</div>
<div className="flex justify-between">
<div className="w-1/2 mr-2">
<Label labelName="Invoice Date" />
<Controller
control={control}
name="paymentDue"
render={({ field }) => (
<DatePicker
className="w-full bg-primaryOne p-3 rounded-md shadow-md border border-borderOne focus:outline-none focus:border-secondaryOne transition text-white font-bold text-xs"
onChange={(date) => field.onChange(date)}
selected={field.value}
/>
)}
/>
</div>
<div className="w-1/2 ml-2">
<Label labelName="Payment Terms" />
<select className="w-full bg-primaryOne p-3 rounded-md shadow-md border border-borderOne focus:outline-none focus:border-secondaryOne transition text-white font-bold text-xs" name="Payments Term" id="Payments Term" {...register("Payments Term", { required: true })}>
<option value="1">Next 1 Day</option>
<option value="7">Next 7 Days</option>
<option value="14">Next 14 Days</option>
<option value="30">Next 30 Days</option>
</select>
</div>
</div>
<div>
<Label labelName="Descriptions" />
<Input inputName="descriptions" register={register} />
</div>
<p className="text-gray-500 text-lg mt-6 mb-2 font-bold">Item List</p>
<div>
// in this code I need help
{fields.map((invoice, index) => <Item key={invoice.id} index={index} remove={remove} />)}
</div>
<button className="w-full bg-borderOne hover:bg-primaryOne transition text-white border-none rounded-full mt-4 p-4 text-xs font-bold flex justify-center" onClick=
{e => {
e.preventDefault();
append({});
}}
>
<span className="font-semibold mr-1">+</span>Add New Item
</button>
</div>
<div className="flex justify-between py-4">jsx
<button className="rounded-full text-neutral text-xs bg-primaryOne outline-none px-8 py-4 font-bold" onClick={() => setOpenForm(!openForm)}>Discard</button>
<div className="pr-7">
<button className="rounded-full text-neutral text-xs bg-primaryOne outline-none px-8 py-4 font-bold">Save as Draft</button>
<input className="rounded-full text-neutral text-xs bg-secondaryTwo outline-none ml-2 px-8 py-4 font-bold" type="submit" value="Save & Send" />
</div>
</div>
</form>
</FormProvider>
</div >
</div >
)
}
export default CreateInvoice
whole child component(Item.js) code
import React, { useState } from 'react'
import Input from './Input'
import Label from './Label'
import { useFormContext } from "react-hook-form";
const Item = ({ index, remove }) => {
const { register } = useFormContext();
return (
<div className="flex justify-center items-end">
<div className="w-3/5">
<Label labelName="Item Name" />
<Input inputName="inputName" register={register} />
</div>
<div className="w-2/12 mx-3">
<Label labelName="Qty." />
<Input inputName="quantity" register={register} />
</div>
<div className="w-1/3">
<Label labelName="Price" />
<Input inputName="price" register={register} />
</div>
<div className="mx-3">
<Label labelName="Total" />
<Input inputName="total" register={register} readOnly />
</div>
<button className="mb-4" aria-label="delete button" onClick={
e => {
e.preventDefault();
remove(index);
}}
>
<svg className="transition fill-current text-gray-400 hover:fill-current hover:text-red-400" width="13" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M11.583 3.556v10.666c0 .982-.795 1.778-1.777 1.778H2.694a1.777 1.777 0 01-1.777-1.778V3.556h10.666zM8.473 0l.888.889h3.111v1.778H.028V.889h3.11L4.029 0h4.444z" fillRule="nonzero" /></svg></button>
</div>
)
}
export default Item
You're missing to spread the formMethods to the <FormProvider /> in your <CreateInvoice /> component.
const CreateInvoice = ({ openForm, setOpenForm }) => {
const formMethods = useForm();
const { register, control, handleSubmit, formState: { errors } } = formMethods;
return (
<FormProvider {...formMethods} >
...
</FormProvider>
);
}
For the second issue:
you're not registering the field array item <input /> components correctly and miss to set the index, so RHF can't setup the link to this fields. Check the demo, i just passed the fieldId as a prop to your <Item /> component.
since v7 RHF's errors object is a property of the formState property returned by useForm. I updated the code example above.
you should set the defaultValues for your field array item when you're calling append. From the docs:
When you append, prepend, insert and update the field array, the obj can't be empty object rather need to supply all your input's defaultValues.