React Hook Form validation using nested custom checkbox component - javascript

I am using React Hook Form. I've made a custom checkbox which looks like this:
const Checkbox = ({ text, className, setCheckbox, checkbox }) => {
const { register } = useFormContext();
const statute = register("statute");
return (
<Wrapper className={className}>
<StyledLabel>
<div>{text}</div>
<StyledInput
type="checkbox"
name="statute"
onChange={(e) => {
statute.onChange(e);
setCheckbox && setCheckbox(!checkbox);
}}
/>
<Checkmark />
</StyledLabel>
</Wrapper>
);
};
Checkbox (Checkbox.js) is nested in a parent component (Register.js):
const Register = ({ setRegisterView }) => {
const validationSchema = Yup.object().shape({
statute: Yup.bool().oneOf([true], "You need to accept the page statute."),
});
const methods = useForm({
mode: "onSubmit",
resolver: yupResolver(validationSchema),
});
const {
register: validate,
formState: { errors },
handleSubmit,
} = methods;
return (
<Wrapper>
<FormProvider {...methods}>
<Form onSubmit={handleSubmit(registerProcess)}>
<StyledCheckbox
text="Accept the page statute."
setCheckbox={null}
checkbox={null}
/>
{errors.statute && <Error>{errors.statute.message}</Error>}
<LoginButton type="submit">SIGN UP</LoginButton>
</Form>
</FormProvider>
</Wrapper>
);
};
export default Register;
The problem is that when I check the checkbox I get an information in errors.statute.message: statute must be a `boolean` type, but the final value was: `"on"`..
When I change this:
onChange={(e) => {
statute.onChange(e);
setCheckbox && setCheckbox(!checkbox);
}}
to this:
{...register("statute")}
then it works great and errors.statute.message shows correct message just when checked=false in checkbox input. But I need to have the extended version of onChange.

The problem is, that you not link the returned ref of the register call to your <StyledInput />. So just spread the return value of register - in the code example below i also omitted the name prop as it is included in statute. Here you can find all the props register will return.
<StyledInput
type="checkbox"
{...statute}
onChange={e => {
statute.onChange(e);
setCheckbox && setCheckbox(!checkbox);
}}
/>

Related

MUI + React Hook Form: Fill out TextField value but then can't modify the value

I'm using the combination of MUI + React Hook Form, so I've created a CustomTextField.tsx component to make it worked.
// CustomTextField.tsx
import { TextField } from "#mui/material";
export default function CustomTextField(props: any) {
return (
<Controller
name={props.name}
control={props.control}
render={({
field: { onChange, value },
fieldState: { error },
formState
}) => <TextField onChange={onChange} {...props} />}
/>
);
}
Then at the app/parent level, I want to these steps:
Fetch data and display to the TextField.
Modify the text in TextField
Submit the new value in TextField
This is my approach:
//App.tsx
export default function App() {
const { control, handleSubmit } = useForm();
const [fetchedData, setFetchedData] = useState<string>("");
...
return (
...
<CustomTextField
control={control}
name="description"
label="Description"
type="text"
variant="outlined"
value={fetchedData ? fetchedData: ""} //<-- fetched data is set to value of TextField to display
/>
...
);
}
With this approach, I managed to display the fetchedData on TextField UI, but can not modify that data on text field. Also when I submit, the data is not correct to what display on the TextField
I have created a codesandbox link for this: https://codesandbox.io/s/blissful-sanne-x858dx?file=/src/App.tsx:190-1155
How can I display the fetchedData, but also can modify the data in text field and then submit through React Hook Form later?
What you are trying to is have a text input where the user can type a value, but you can also set the value externally by clicking the "Fetch Data" button.
Your setup includes conflicting sources of truth. The value of the field is set to the data state and the value which is stored in the react-hook-form state is ignored.
What you want to do is to modify the internal state of the react-hook-form when the button is pressed.
You can delete the local state:
const [data, setData] = useState<string>("");
Instead, you can use the setValue helper function from the form:
const { control, handleSubmit, setValue } = useForm<FormValues>();
const fetchingData = () => {
setValue("description", "fetched description Text");
};
In your CustomTextField, make sure that you set the value of the input to the value from the form state.
render={({
field: { onChange, value },
fieldState: { error },
formState
}) => <CusTextField onChange={onChange} value={value} {...props} />}
Complete Code
Inspired by #Linda solution, I've came up with this approach, that ensures the functionality working with the style of MUI TextField as well.
The setValue will be passed down to custom TextField to set the value. And to keep the {...props} functionality, I delete the setValue props before spreading it on MUI TextField.
//CustomTextField.tsx
import { TextField } from "#mui/material"
import { Controller } from "react-hook-form"
export default function CustomTextField(props: any) {
const propsObj = { ...props }
delete propsObj.setValue
return (
<Controller
name={props.name}
control={props.control}
defaultValue={""}
render={({
field: { onChange, value },
fieldState: { error },
formState,
}) => (
<TextField
value={value}
onChange={({ target: { value } }) => {
onChange(value)
if (props?.setValue) props.setValue(props.name, value)
}}
{...propsObj}
/>
)}
/>
)
}
//App.tsx
export default function App(){
const { control, handleSubmit, setValue } = useForm()
return (
<form onSubmit={handleSubmit(...)}>
<CustomTextField
control={control}
name="description"
label="Description"
type="text"
variant="outlined"
setValue={setValue}
/>
<form/>
)
}

Handle Reset for specific field formik

I am new to react and I have just started using Formik
I like how simple it makes making forms and handling forms in react.
I have created multiple custom fields using formik, I am putting the react-select field I created as an example here.
import { ErrorMessage, Field } from "formik";
import React from "react";
import Select from 'react-select'
const SelectInput = (props) => {
const { label, name, id,options, required, ...rest } = props;
const defaultOptions = [
{label : `Select ${label}`,value : ''}
]
const selectedOptions = options ? [...defaultOptions,...options] : defaultOptions
return (
<div className="mt-3">
<label htmlFor={id ? id : name}>
{label} {required && <span className="text-rose-500">*</span>}
</label>
<Field
// className="w-full"
name={name}
id={id ? id : name}
>
{(props) => {
return (
<Select
options={selectedOptions}
onChange={(val) => {
props.form.setFieldValue(name, val ? val.value : null);
}}
onClick = {(e)=>{e.stopPropagation}}
{...rest}
// I want someting like onReset here
></Select>
);
}}
</Field>
<ErrorMessage
name={name}
component="div"
className="text-xs mt-1 text-rose-500"
/>
</div>
);
};
export default SelectInput;
This is the usual code I use for submitting form as you can see I am using resetForm() method that is provided by formik, I want to attach the reseting logic in on submit method itself.
const onSubmit = async (values, onSubmitProps) => {
try {
//send request to api
onSubmitProps.resetForm()
} catch (error) {
console.log(error.response.data);
}
};
If you want to reset the selected value after the form is submitted, you need to provide a controlled value for the Select component.
The Formik Field component provides the value in the props object, so you can use it.
For example:
SelectInput.js
import { ErrorMessage, Field } from 'formik';
import React from 'react';
import Select from 'react-select';
const SelectInput = ({ label, name, id, options, required, ...rest }) => {
const defaultOptions = [{ label: `Select ${label}`, value: '' }];
const selectedOptions = options ? [...defaultOptions, ...options] : defaultOptions;
return (
<div className='mt-3'>
<label htmlFor={id ? id : name}>
{label} {required && <span className='text-rose-500'>*</span>}
</label>
<Field
// className="w-full"
name={name}
id={id ? id : name}
>
{({
field: { value },
form: { setFieldValue },
}) => {
return (
<Select
{...rest}
options={selectedOptions}
onChange={(val) => setFieldValue(name, val ? val : null)}
onClick={(e) => e.stopPropagation()}
value={value}
/>
);
}}
</Field>
<ErrorMessage name={name} component='div' className='text-xs mt-1 text-rose-500' />
</div>
);
};
export default SelectInput;
and Form.js
import { Formik, Form } from 'formik';
import SelectInput from './SelectInput';
function App() {
return (
<Formik
initialValues={{
firstName: '',
}}
onSubmit={async (values, { resetForm }) => {
console.log({ values });
resetForm();
}}
>
<Form>
<SelectInput
name='firstName'
label='First Name'
options={[{ label: 'Sam', value: 'Sam' }]}
/>
<button type='submit'>Submit</button>
</Form>
</Formik>
);
}
export default App;
Therefore, if you click the Submit button, value in the Select component will be reset.
You can also make a useRef hook to the Fromik component and then reset the form within the reset function without adding it as a parameter to the function.
https://www.w3schools.com/react/react_useref.asp
It's one of the really nice hooks you'll learn as you progress through React :)
So if I understood you correctly you want to reset a specif field value onSubmit rather than resetting the whole form, that's exactly what you can achieve using actions.resetForm().
Note: If nextState is specified, Formik will set nextState.values as the new "initial state" and use the related values of nextState to update the form's initialValues as well as initialTouched, initialStatus, initialErrors. This is useful for altering the initial state (i.e. "base") of the form after changes have been made.
You can check this in more detail here.
And here is an example of resetting a specific field using resetForm() whereby you can see as you input name, email and upon submit only email field will get empty using resetForm.
import "./styles.css";
import React from "react";
import { Formik } from "formik";
const initialState = {
name: "",
email: ""
};
const App = () => (
<div>
<h1>My Form</h1>
<Formik
initialValues={initialState}
onSubmit={(values, actions) => {
console.log(values, "values");
actions.resetForm({
values: {
email: initialState.email
}
});
}}
>
{(props) => (
<form onSubmit={props.handleSubmit}>
<input
type="text"
onChange={props.handleChange}
onBlur={props.handleBlur}
value={props.values.name}
name="name"
/>
<br />
<input
type="text"
onChange={props.handleChange}
onBlur={props.handleBlur}
value={props.values.email}
name="email"
/>
<br />
<br />
{props.errors.name && <div id="feedback">{props.errors.name}</div>}
<button type="submit">Submit</button>
</form>
)}
</Formik>
</div>
);
export default App;

ref.current is null in gatsby react app when trying to execute recaptcha

I am trying to use this https://react-hook-form.com/get-started npm package with this package https://www.npmjs.com/package/react-google-recaptcha in gatsby and react. I want to use the invisible recaptcha it looks like I have to execute the recaptcha which I am trying to do by creating a react ref but it says the rec.current is null, Not quote sure what to do. The onSubmit function is where I am getting the null result, I was assuming I would be able to fire the captcha here and then get back the captcha value to later send off to google in my lambda function for verification.
Thanks ahead of time
Here is my code thus far
import React, { useState } from "react"
import Layout from "../components/layout"
import Img from "gatsby-image"
import { graphql, Link } from "gatsby"
import { CartItems } from "../components/cart"
import { useForm } from "react-hook-form"
import ReCAPTCHA from "react-google-recaptcha"
const StoreDetails = ({ data }) => {
const { register, handleSubmit, watch, errors } = useForm()
const recaptchaRef = React.createRef()
const onSubmit = data => {
console.log(recaptchaRef)
recaptchaRef.current.execute() //this shows up null
}
function onChange(value) {
console.log("Captcha value:", value)
}
function error(value) {
alert(value)
}
return (
<>
{data.allSanityProducts.edges.map(({ node: product }, i) => {
return (
<React.Fragment key={i}>
<Item>
<Img
fluid={product.featureImage && product.featureImage.asset.fluid}
/>
<div>
...
<form onSubmit={handleSubmit(onSubmit)}>
{/* register your input into the hook by invoking the "register" function */}
<input name="example" defaultValue="test" ref={register} />
{/* include validation with required or other standard HTML validation rules */}
<input
name="exampleRequired"
ref={register({ required: true })}
/>
{/* errors will return when field validation fails */}
{errors.exampleRequired && (
<span>This field is required</span>
)}
<ReCAPTCHA
className="captchaStyle"
sitekey="obsf"
onChange={onChange}
onErrored={error}
badge={"bottomright"}
size={"invisible"}
ref={recaptchaRef}
/>
<input type="submit" />
</form>
</div>
</Item>
</React.Fragment>
)
})}
{close && <CartItems />}
</>
)
}
const WithLayout = Component => {
return props => (
<>
<Layout>
<Component {...props} />
</Layout>
...
</>
)
}
export default WithLayout(StoreDetails)
export const query = graphql`
query StoreDeatailsQuery($slug: String!) {
...
}
`
You are never populating the reference with any value. Initially is set to null in:
const recaptchaRef = React.createRef()
You have to wait for the Google response to fill the recaptchaRef with a value. In other words, you need to use a promise-based approach to fill it using an executeAsync() and using an async function:
const onSubmit = async (data) => {
const yourValue = await recaptchaRef.current.executeAsync();
console.log(yourValue)
}
You can check for further details about the props exposed in react-google-recaptcha documentation.

#Antd# How can I bind self-made component with Form.item

I want to use my component like this
<Form.item>
<MyComponent />
</Form.item>
I can't pass the defaultValue to MyComponent and the Form can't get the value from MyComponent when I hit the submit button.
How can I fix this?
For using a default value you can use initialValues from antd's Form together with the in Form.Item defined name.
E.g.:
<Form
{...layout}
name="basic"
initialValues={{
myComponent: "IntialTestValue"
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="MyComponent"
name="myComponent"
To use custom or third party component you need bind your component to a value and onChange.
E.g.:
const MyComponent = ({ value = {}, onChange }) => {
const [inputValue, setInputValue] = useState(value);
const onInputChange = e => {
setInputValue(e.target.value);
onChange(e.target.value);
};
return (
<>
<Input defaultValue={value} value={inputValue} onChange={onInputChange} />
</>
);
};
Here is a working CodeSandbox.
Another good example can be found int antd Form docs: CodeSandbox

Sending parameter with function to child component and back to parent component

I've got a custom component called InputWithButton that looks like this:
const InputWithButton = ({ type = "text", id, label, isOptional, name, placeholder = "", value = "", showPasswordReset, error, isDisabled, buttonLabel, handleChange, handleBlur, handleClick }) => (
<StyledInput>
{label && <label htmlFor="id">{label}{isOptional && <span className="optional">optioneel</span>}</label>}
<div>
<input className={error ? 'error' : ''} type={type} id={id} name={name} value={value} placeholder={placeholder} disabled={isDisabled} onChange={handleChange} onBlur={handleBlur} autoComplete="off" autoCorrect="off" />
<Button type="button" label={buttonLabel} isDisabled={isDisabled} handleClick={() => handleClick(value)} />
</div>
{error && <Error>{Parser(error)}</Error>}
</StyledInput>
);
export default InputWithButton;
Button is another component and looks like this:
const Button = ({ type = "button", label, isLoading, isDisabled, style, handleClick }) => (
<StyledButton type={type} disabled={isDisabled} style={style} onClick={handleClick}>{label}</StyledButton>
);
export default Button;
I'm using the InputWithButton component in a parent component like this:
render() {
const { name } = this.state;
return (
<React.Fragment>
<InputWithButton label="Name" name="Name" buttonLabel="Search" value={name} handleChange={this.handleChange} handleClick={this.searchForName} />
</React.Fragment>
);
}
If the button is clicked, the searchForName function is called:
searchForName = value => {
console.log(value); //Input field value
}
This is working but I want to add another parameter to it but this time, a parameter that comes from the parent component
// handleClick={() => this.searchForName('person')}
<InputWithButton label="Name" name="Name" buttonLabel="Search" value={name} handleChange={this.handleChange} handleClick={() => this.searchForName('person')} />
The output in searchForName is now 'person' instead of the value.
I thought I could fix this with the following code:
searchForName = type => value => {
console.log(type); //Should be person
console.log(value); //Should be the value of the input field
}
However this approach doesn't execute the function anymore.
How can I fix this?
EDIT: Codepen
I would try handleClick={this.searchForName.bind(this, 'person')}, please let me know if it'll work for you.
EDIT:
I changed fragment from your codepen, it's working:
searchName(key, value) {
console.log(key);
console.log(value);
}
render() {
const { name } = this.state;
return (
<InputWithButton name="name" value={name} buttonLabel="Search" handleChange={this.handleChange} handleClick={this.searchName.bind(this, 'person')} />
)
}
as I suspected, just pass it an object and make sure you're accepting the argument in your handleClick function
handleClick={value => this.searchName({value, person: 'person'})}
or more verbose - without the syntactic sugar
handleClick={value => this.searchName({value: value, person: 'person'})}
then you can get at it with value.person
full codepen here
Hope this helps

Categories