How to create custom validation from react hook form? - javascript

I want to create a custom validation starting from the validation below. But I'm not successful so far. I had visited this site and followed the codes in his "Custom validation rules" but I can't replicate it.
The isBefore method is working fine, but the validation does not. And also how can we put a custom message with this custom validation?
const isBefore = (date1, date2) => moment(date1).isBefore(moment(date2));
const rules = {
publishedDate: {
required: 'The published date is required.',
before: isBefore(scheduledDate, expiredDate)
},
}
<Controller
control={control}
name="publishedDate"
rules={rules.publishedDate}
render={({ onChange }) => (
<DatePicker
className="mb-px-8"
onChange={(value) => {
setPublishedDate(value);
onChange(value);
}}
minDate={new Date()}
value={publishedDate}
/>
)}
/>

Here is my attempt:
you need to use the hook useEffect and a controller.
at the top of the page you need these two imports:
import React, { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
then you need the validation function this lives outside of the component.
const isBefore = (date) => {
if (!date) {
return false;
}
const today = new Date();
today.setHours(0, 0, 0, 0);
return date > today;
};
The above function checks that the date you picked is in the future and not the past.
Under your component you set everything to useForm
const {
register,
handleSubmit,
control,
setValue,
watch,
errors,
setError,
clearError
} = useForm();
You then setup variables to watch for the datepicker to update, along with useEffect to watch for the change:
const startDate = watch("startDate");
useEffect(() => {
register({ name: "startDate", type: "custom" }, { validate: { isBefore } });
});
You then define a handler inside of the component that handles the data change along with the validation.
const handleDateChange = (dateType) => (date) => {
if (!isBefore(date)) {
setError(dateType, "isBefore");
} else {
setError(dateType, "isBefore");
}
setValue(dateType, date);
alert(date);
};
the custom error message can exist anywhere in the form and you don't need to tie a ref to it. the useForm() and watch('startDate') control the data for you.
Here is the custom error message that can live anywhere within the form component.
Please see updated codesandbox where I have the custom error message displayed near the submit button
{errors.startDate && (
<div variant="danger">
{errors.startDate.type === "isBefore" && (
<p>Please choose present or future date!</p>
)}
</div>
Here is a working codesandbox that I cleaned up a bit from yesterday, and added in some comments.
https://codesandbox.io/s/play-momentjs-forked-1hu4s?file=/src/index.js:1494-1802
If you click the input and then choose a date in the past, and then click submit, the custom error message will show. However, if you select a date in the future and hit submit the message doesn't show.
Here is a resource I used:
https://eincode.com/blogs/learn-how-to-validate-custom-input-components-with-react-hook-form
Also more information on watch that you get from useForm function:
https://react-hook-form.com/api/useform/watch/

Try using rules of react-hook-form to add validations
<Controller
name="currentName"
control={control}
render={({ field }) => (
<TextField
value={field.value}
onChange={field.onChange}
inputRef={field.ref}
variant="outlined"
size="small"
fullWidth
autoComplete="off"
helperText={helperText}
/>
)}
rules={{
validate: {
required: (value) => {
if (value === "SomeValue") return 'Some Message';
if (!value) return '*Required';
}
},
maxLength: 5
}}
defaultValue=""
/>

Related

Issue testing functional component with ternary return using React 18 and RTL

I have tried a few ways to do this correctly, but lack the testing experience to catch what I'm missing. I have a LoginForm.tsx component that inside holds a few event handlers and a couple bits of local state using React.useState(). The component returns a ternary statement conditionally rendering two components, and within one of them, that component renders different content based on another boolean condition.
authSuccess: when false, main component returns a <Card /> component; when true, the component returns <Navigate to={...} replace /> to redirect user to account.
isLoading: when false, children of <Card /> is form content, when true, children is a <Spinner /> component.
The problem is, I can't seem to find how to change those useState values in my tests and mock the behavior of this component. I would like to test that errors are rendering correctly as well. I am not using Enzyme since it seems it is dead for anything after React 17, so I have been trying to find a way to do this using just React Testing Library out of the box with Create React App Typescript.
The component code looks like this:
import * as React from 'react'
// ...
export default function LoginForm() {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [errors, setErrors] = React.useState<{ [key: string]: string } | any>({});
const [authSuccess, setAuthSuccess] = React.useState<boolean>(false);
const initialState: LoginFormInitialState = {
email: '',
password: '',
};
// Form Hook
const { values, onChange, onSubmit } = useForm({ callback: handleLogin, initialState });
// ==> HANDLERS
function successCallback(data) {
// ...
return setAuthSuccess(true); // <---- Changes state to return redirect
}
function errorHandler(e: any) {
// ...
setErrors(errors); // <---- // sets errors object to render errors
return setIsLoading(false); // <---- Changes contents of <Card />
}
function handleLogin() {
setIsLoading(true); // <---- Changes content of card
setErrors({}); // <---- Clears errors
// Passes handlers as callbacks into api
return userAccountAPI.login({ data: values, successCallback, errorHandler });
}
return authSuccess ? (
// If authentication was successful, redirect user to account page
<Navigate to={ACCOUNT_OVERVIEW} replace />
) : (
// No auth success yet, keep user on login page
<Card
title={<h1 data-testid="form-header">Login To Account</h1>}
data-testid={'login-card'}
bodyStyle={{
display: isLoading ? 'flex' : 'block',
justifyContent: 'center',
padding: isLoading ? '100px 0 ' : '',
}}>
{isLoading ? (
<Spin size='large' data-testid={'login-spinner'}></Spin>
) : (
<Form name='login_form' data-testid={'login-form'} initialValues={{ remember: true }}>
<Form.Item name='email' rules={[{ required: true, message: 'Please input your email!' }]}>
<Input
name='email'
onChange={onChange}
prefix={<UserOutlined className='site-form-item-icon' />}
placeholder='Email'
/>
</Form.Item>
<Form.Item name='password' rules={[{ required: true, message: 'Please input your Password!' }]}>
<Input
name='password'
onChange={onChange}
prefix={<LockOutlined className='site-form-item-icon' />}
type='password'
placeholder='Password'
/>
</Form.Item>
<Form.Item>
<Button
data-testid='login-button'
onClick={onSubmit}
type='primary'
name='login'
htmlType='submit'
className='login-form-button'
block>
Log in
</Button>
</Form.Item>
</Form>
)}
{/* Render out any form errors from the login attempt */}
{Object.entries(errors).length > 0 && (
<Alert
type='error'
message={
<ul style={{ margin: '0' }}>
{Object.keys(errors).map((er, i) => {
return <li key={i}>{errors[er]}</li>;
})}
</ul>
}
/>
)}
</Card>
);
}
I would like to be able to make an assertion about the card, but not if authSuccess=true, in which case I'd want to assert that we do not have the card and that the redirect has been rendered. I would want to test that the Spinner is a child of the card if isLoading = true, but also that I have the form as a child if it is false.
I have tried some of the approaches I've seen in other issues, but many of them have a button in the UI that directly changes the value and the solution is typically "grab that button and click it" and there you go. But the only button here is the login, and that doesn't directly change the local state values I need to mock.
I have also tried something like this which seems to have worked for some people but not for me here..
import * as React from 'react'
describe('<LoginForm />', () => {
const setup = () => {
const mockStore = configureStore();
render(
<Provider store={mockStore()}>
<LoginForm />
</Provider>
);
};
it('should have a spinner as child of card', () => {
setup();
jest.spyOn(React, 'useState')
.mockImplementationOnce(() => ['isLoading', () => true])
.mockImplementationOnce(() => ['errors', () => {}])
.mockImplementationOnce(() => ['authSuccess', () => false]);
const card = screen.getAllByTestId('login-card');
const spinner = screen.getAllByTestId('login-spinner');
expect(card).toContainElement(spinner);
});
});
It seems like Enzyme provided solutions for accessing and changing state, but as mentioned, I am not using Enzyme since I am using React 18.
How can I test this the way I intend to, or am I making a fundamental mistake with how I am approaching testing this? I am somewhat new to writing tests beyond that basics.
Thanks!
From the test I see that you are using react testing library. In this case you should "interact" with your component inside the test and check if the component reacts properly.
The test for spinner should be like that:
render the component
find the email input field and "type" there an email - use one of getBy* methods and then type with e.g. fireEvent.change(input, {target: {value: 'test#example.com'}})
find the password input field and "type" there a password - same as above
find the submit button and "click" it - use one of getBy* methods to find it and then use fireEvent to click it
this (I assume) should trigger your onSubmit which will call the handleLogin callback which will update the state and that will render the spinner.
check if spinner is in the document.
Most probably you would need some mocking for your userAccountAPI so it calls a mock function and not some real API. In here you can also mock that API to return whatever response you want and check if component displays correct content.

Formik: target is undefined when passing Material UI Date Picker as prop to React Component

I'm trying to create a reusable component. where I am passing the form fields as prop. when I click on datepicker field. I'm getting this error:
TypeError: target is undefined
How can I fix this?
Here's how my input component looks like:
import { useField } from 'formik';
import { DatePicker } from '#material-ui/pickers';
export const DatePickerField = ({ label, ...props }) => {
const [field, meta] = useField(props);
return (
<DatePicker label={label} fullWidth {...field} {...props} />
);
};
Here's how the reusable component looks like:
import { Form } from 'formik'
export const ReusableComp = ({ fields }) => (
<Form noValidate>
{fields}
</Form>
)
Here's where I am using this component:
export const App = () => (
<ReusableComp fields={
<div className='mb-3'>
<DateTimePickerField
label='Start DateTime'
name='start_date_time'
/>
</div>
} />
)
Result of console.log(fields)
In my case I was using the Material UI DesktopDatePicker (with Typescript), and defining the onChange prop like this solved it:
onChange={(value): void => {
formik.setFieldValue("dueDate", value);
}}
You have to replace "dueDate" with your formik value name.
After lots of research and thanks #Rosen Tsankov for pointing my attention to the onChange function.
I have seen questions about the same error on SO which are not answered. so this may help them and anyone in future facing this error.
As #Rosen Tsankov have said the material ui DatePicker component returns the date value as the first argument of the onChange function.
the field returned from useField have the following: name, value, onChange, onBlur.
the onChange function returned from useField expects the first argument to be an event which in this case is the date value. that's why we get the error:
TypeError: target is undefined
Because formik is trying to access target property and date has no property target. something like this date.target and this is undefined
so here's how I have fixed it. instead of spreading the field. I have added name and value from field to DatePicker. then I have used the setFieldValue from formik to manually update the input value like so.
import { DatePicker } from '#material-ui/pickers';
import { useField, useFormikContext } from 'formik';
export const DatePickerField = ({ label, ...props }) => {
const [field, meta] = useField(props);
const { setFieldValue } = useFormikContext();
return (
<DatePicker
fullWidth
{...props}
label={label}
name={field.name}
value={field.value}
helperText={meta.error}
error={meta.touched && Boolean(meta.error)}
onChange={(value) => setFieldValue(field.name, value)}
/>
);
};
DatePicker expects onChange((date) => ...) but you are passing formik handler wich expects onChange((event) => ....)

multi step form in react taking wrong validation

I am writing my problem as a fresh part here.
I made a multi step form where I have on dynamic filed in 1st form, that field is to create password manually or just auto generated.
So my multi step form is working fine going to and forth is fine, but I have to pass the fields to main component so it can check for validation, and I am passing that password too
Here comes the issue
When i pass the password field also then it takes the validation even when I have click on auto generated password
I am passing fields like this fields: ["uname", "email", "password"], //to support multiple fields form
so even not check on let me create password it takes the validation.
When i click let me create password and input some values then click on next and when I comes back the input field sets to hidden again to its initial state I know why it is happening, because when I come back it takes the initial state allover again.
i am fed-up with this thing for now, I have tried many things but didn't work below is my code
import React, { useState, useEffect } from "react";
import Form1 from "./components/Form1";
import Form2 from "./components/Form2";
import Form3 from "./components/Form3";
import { useForm } from "react-hook-form";
function MainComponent() {
const { register, triggerValidation, errors, getValues } = useForm();
const [defaultValues, setDefaultValues] = useState({});
const forms = [
{
fields: ["uname", "email", "password"], //to support multiple fields form
component: (register, errors, defaultValues) => (
<Form1
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
fields: ["lname"],
component: (register, errors, defaultValues) => (
<Form2
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
fields: [""],
component: (register, errors, defaultValues) => (
<Form3
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
}
];
const [currentForm, setCurrentForm] = useState(0);
const moveToPrevious = () => {
setDefaultValues(prev => ({ ...prev, ...getValues() }));
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm - 1);
});
};
const moveToNext = () => {
setDefaultValues(prev => ({ ...prev, ...getValues() }));
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm + 1);
});
};
const prevButton = currentForm !== 0;
const nextButton = currentForm !== forms.length - 1;
const handleSubmit = e => {
console.log("whole form data - ", JSON.stringify(defaultValues));
};
return (
<div>
<div class="progress">
<div>{currentForm}</div>
</div>
{forms[currentForm].component(
register,
errors,
defaultValues[currentForm]
)}
{prevButton && (
<button
className="btn btn-primary"
type="button"
onClick={moveToPrevious}
>
back
</button>
)}
{nextButton && (
<button className="btn btn-primary" type="button" onClick={moveToNext}>
next
</button>
)}
{currentForm === 2 && (
<button
onClick={handleSubmit}
className="btn btn-primary"
type="submit"
>
Submit
</button>
)}
</div>
);
}
export default MainComponent;
please check my code sand box here you can find full working code Code sandbox
React Hook Form embrace native form validation, which means when your component is removed from the DOM and input state will be removed. We designed this to be aligned with the standard, however we start to realize more and more users used to controlled form get confused with this concept, so we are introducing a new config to retain the unmounted input state. This is still in RC and not released.
useForm({ shouldUnregister: true })
Solution for now:
break into multiple routes and store data in the global store
https://www.youtube.com/watch?v=CeAkxVwsyMU
bring your steps into multiple forms and store data in a local state
https://codesandbox.io/s/tabs-760h9
use keepAlive and keep them alive:
https://github.com/CJY0208/react-activation

Material UI + React Form Hook + multiple checkboxes + default selected

I am trying to build a form that accommodates multiple 'grouped' checkboxes using react-form-hook Material UI.
The checkboxes are created async from an HTTP Request.
I want to provide an array of the objects IDs as the default values:
defaultValues: { boat_ids: trip?.boats.map(boat => boat.id.toString()) || [] }
Also, when I select or deselect a checkbox, I want to add/remove the ID of the object to the values of react-hook-form.
ie. (boat_ids: [25, 29, 4])
How can I achieve that?
Here is a sample that I am trying to reproduce the issue.
Bonus point, validation of minimum selected checkboxes using Yup
boat_ids: Yup.array() .min(2, "")
I've been struggling with this as well, here is what worked for me.
Updated solution for react-hook-form v6, it can also be done without useState(sandbox link below):
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const defaultNames = ["bill", "Manos"];
const { control, handleSubmit } = useForm({
defaultValues: { names: defaultNames }
});
const [checkedValues, setCheckedValues] = useState(defaultNames);
function handleSelect(checkedName) {
const newNames = checkedValues?.includes(checkedName)
? checkedValues?.filter(name => name !== checkedName)
: [...(checkedValues ?? []), checkedName];
setCheckedValues(newNames);
return newNames;
}
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{["bill", "luo", "Manos", "user120242"].map(name => (
<FormControlLabel
control={
<Controller
name="names"
render={({ onChange: onCheckChange }) => {
return (
<Checkbox
checked={checkedValues.includes(name)}
onChange={() => onCheckChange(handleSelect(name))}
/>
);
}}
control={control}
/>
}
key={name}
label={name}
/>
))}
<button>Submit</button>
</form>
);
}
Codesandbox link: https://codesandbox.io/s/material-demo-54nvi?file=/demo.js
Another solution with default selected items done without useState:
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
Breaking API changes made in 6.X:
validation option has been changed to use a resolver function wrapper and a different configuration property name
Note: Docs were just fixed for validationResolver->resolver, and code examples for validation in repo haven't been updated yet (still uses validationSchema for tests). It feels as if they aren't sure what they want to do with the code there, and it is in a state of limbo. I would avoid their Controller entirely until it settles down, or use Controller as a thin wrapper for your own form Controller HOC, which appears to be the direction they want to go in.
see official sandbox demo and the unexpected behavior of "false" value as a string of the Checkbox for reference
import { yupResolver } from "#hookform/resolvers";
const { register, handleSubmit, control, getValues, setValue } = useForm({
resolver: yupResolver(schema),
defaultValues: Object.fromEntries(
boats.map((boat, i) => [
`boat_ids[${i}]`,
preselectedBoats.some(p => p.id === boats[i].id)
])
)
});
Controller no longer handles Checkbox natively (type="checkbox"), or to better put it, handles values incorrectly. It does not detect boolean values for checkboxes, and tries to cast it to a string value. You have a few choices:
Don't use Controller. Use uncontrolled inputs
Use the new render prop to use a custom render function for your Checkbox and add a setValue hook
Use Controller like a form controller HOC and control all the inputs manually
Examples avoiding the use of Controller:
https://codesandbox.io/s/optimistic-paper-h39lq
https://codesandbox.io/s/silent-mountain-wdiov
Same as first original example but using yupResolver wrapper
Description for 5.X:
Here is a simplified example that doesn't require Controller. Uncontrolled is the recommendation in the docs. It is still recommended that you give each input its own name and transform/filter on the data to remove unchecked values, such as with yup and validatorSchema in the latter example, but for the purpose of your example, using the same name causes the values to be added to an array that fits your requirements.
https://codesandbox.io/s/practical-dijkstra-f1yox
Anyways, the problem is that your defaultValues doesn't match the structure of your checkboxes. It should be {[name]: boolean}, where names as generated is the literal string boat_ids[${boat.id}], until it passes through the uncontrolled form inputs which bunch up the values into one array. eg: form_input1[0] form_input1[1] emits form_input1 == [value1, value2]
https://codesandbox.io/s/determined-paper-qb0lf
Builds defaultValues: { "boat_ids[0]": false, "boat_ids[1]": true ... }
Controller expects boolean values for toggling checkbox values and as the default values it will feed to the checkboxes.
const { register, handleSubmit, control, getValues, setValue } = useForm({
validationSchema: schema,
defaultValues: Object.fromEntries(
preselectedBoats.map(boat => [`boat_ids[${boat.id}]`, true])
)
});
Schema used for the validationSchema, that verifies there are at least 2 chosen as well as transforms the data to the desired schema before sending it to onSubmit. It filters out false values, so you get an array of string ids:
const schema = Yup.object().shape({
boat_ids: Yup.array()
.transform(function(o, obj) {
return Object.keys(obj).filter(k => obj[k]);
})
.min(2, "")
});
Here is a working version:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const { control, handleSubmit } = useForm({
defaultValues: {
bill: "bill",
luo: ""
}
});
return (
<form onSubmit={handleSubmit(e => console.log(e))}>
{["bill", "luo"].map(name => (
<Controller
key={name}
name={name}
as={
<FormControlLabel
control={<Checkbox value={name} />}
label={name}
/>
}
valueName="checked"
type="checkbox"
onChange={([e]) => {
return e.target.checked ? e.target.value : "";
}}
control={control}
/>
))}
<button>Submit</button>
</form>
);
}
codesandbox link: https://codesandbox.io/s/material-demo-65rjy?file=/demo.js:0-932
However, I do not recommend doing so, because Checkbox in material UI probably should return checked (boolean) instead of (value).
Here's my solution, which is not using all the default components from Material UI cause at my interface each radio will have an icon and text, besides the default bullet point not be showed:
const COMPANY = "company";
const INDIVIDUAL = "individual";
const [scope, setScope] = useState(context.scope || COMPANY);
const handleChange = (event) => {
event.preventDefault();
setScope(event.target.value);
};
<Controller
as={
<FormControl component="fieldset">
<RadioGroup
aria-label="scope"
name="scope"
value={scope}
onChange={handleChange}
>
<FormLabel>
{/* Icon from MUI */}
<Business />
<Radio value={COMPANY} />
<Typography variant="body1">Company</Typography>
</FormLabel>
<FormLabel>
{/* Icon from MUI */}
<Personal />
<Radio value={INDIVIDUAL} />
<Typography variant="body1">Individual</Typography>
</FormLabel>
</RadioGroup>
</FormControl>
}
name="scope"
control={methods.control}
/>;
Observation: At this example I use React Hook Form without destruct:
const methods = useForm({...})
This is my solution with react hook form 7, the other solutions don't work with reset or setValue.
<Controller
name={"test"}
control={control}
render={({ field }) => (
<FormControl>
<FormLabel id={"test"}>{"label"}</FormLabel>
<FormGroup>
{items.map((item, index) => {
const value = Object.values(item);
return (
<FormControlLabel
key={index}
control={
<Checkbox
checked={field.value.includes(value[0])}
onChange={() =>
field.onChange(handleSelect(value[0],field.value))
}
size="small"
/>
}
label={value[1]}
/>
);
})}
</FormGroup>
</FormControl>
)}
/>
link to codesandbox: Mui multiple checkbox

React Hook Form Controller Issues

I have been using react hook form library with native elements but would like to switch to custom components using the Controller API.
I am having an issue with my custom input component updating React state but not updating the ref inside the form state. Thus, a required field is always marked as invalid and I cannot submit my form.
Here is a demo of my issue: https://codesandbox.io/s/react-hook-form-controller-bofv5
It should log out form data upon submission - but submission never happens because form is not valid.
I think I have narrowed down your issue. First I removed the rules={{ required: true }} from the controller and tried the form. It told me firstName: undefined. Then I commented out the onChange attribute. After that, the form is working fine. It seems that onChange should be used if you want to provide a custom value extractor. The value needs to be returned from the function. An example of a simple input would be this: onChange={([{target}]) => target.value} reference. Additionally, it is important to note that handleSubmit extracts some internal state with the values, like that you don't need to keep track of those yourself.
This updated component seems to be working:
function App() {
const { control, handleSubmit, errors } = useForm();
// const [data, setData] = useState({ firstName: "" });
const onSubmit = data => console.log(data);
// const onChangeHandler = e => {
// const { name, value } = e.target;
// const _data = { ...data };
// _data[name] = value;
// setData(_data);
// };
return (
<>
{/* <p>{JSON.stringify(data)}</p> */}
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
as={Input}
name="firstName"
id="firstName"
label="First Name"
control={control}
// value={data.firstName}
rules={{ required: true }}
errors={errors.firstName}
// onChange={([e]) => onChangeHandler(e)}
/>
<input type="submit" />
</form>
</>
);
}
Just a side note, I've never worked with this library so only trust me as far as you can toss me.

Categories

Resources