Related
I am creating a dynamically generated form which reads a file from a template. I have an array of "questions" which are mapped onto a react-hook-form. The defaultValues I am using is an array of objects. The issue I am having is using react-select inside of a react-hook-form controller because it does not set the form state if it is an array. In my testing it works without issue if default values is not an array. Please see my MRE code example
const questions = [
{
value: '',
}];
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
export default function Form()
{
const methods = useForm({
defaultValues: questions,
});
const onSubmit = (data: any) => console.log(data);
return (
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Controller
name="0.value"
control={methods.control}
render={({ field }) => (
<Select
{...field}
value={options.find((c) => c.value === field.value)}
options={options}
onChange={(e) => field.onChange(e?.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
What would be the best way to get react-hook-form controllers components like react-select to work with an array of defaultValues
The issue you are having is due to your default values being an array instead of an object. You should just wrap your questions in an object with a key of your choice. Please see example.
const questions = [
{
value: '',
}];
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
export default function Form()
{
const methods = useForm({
defaultValues: {questions},
});
const onSubmit = (data: any) => console.log(data);
return (
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Controller
name="questions.0.value"
control={methods.control}
render={({ field }) => (
<Select
{...field}
value={options.find((c) => c.value === field.value)}
options={options}
onChange={(e) => field.onChange(e?.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
Below is the code where I am trying to select value from dropdown.
handleChange is not working, when I select the value from dropdown it's not getting updated with selected value from dropdown. It's getting vanished(blank).
Dropdown values are getting populated when I select it's not catching the new selected value.
Can someone help me on this like what I am missing here?
export const FormikSelectField = ({ label, ...props }) => {
const [field, meta] = useField(props);
const [isFocused, setOnFocus] = useState(false);
const handleChange = (value) => {
// this is going to call setFieldValue and manually update values.topcis
console.log('Value in handlechange..', value ,'and', props.name);
props.onChange(props.name, value.value);
};
const handleFocus = () => {
setOnFocus(true);
};
const handleBlur = (e) => {
// this is going to call setFieldTouched and manually update touched.topcis
setOnFocus(false);
props.onBlur(props.name, true);
field.onBlur(e);
};
return (
<>
<label htmlFor={props.labelName}>{props.labelName} </label>
<Select
id={props.labelName}
options={props.options}
isMulti={false}
onChange={handleChange}
onBlur={(e) => handleBlur(e)}
placeholder='Select an option'
onFocus={handleFocus}
value={props.value}
/>
{meta.touched && meta.error && !isFocused ? (
<div className="error">{meta.error}</div>
) : null}
</>
);
};
formikInitialValues = () => {
return {
Name: [{
title: '',
value: '',
}]
};
};
YupValidationSchema = () => {
return Yup.object({
Name: Yup.array()
.of(
Yup.object().shape({
title: Yup.string().required(),
value: Yup.string().required(),
})
)
.required("Please select an option")
.nullable()
});
};
<FormikSelectField
value={this.state.selectNameOption}
onChange={this.handleNameChange}
onBlur={this.handleNameChangeBlur}
error={formik.errors.Name}
options={this.state.NameOptions}
touched={formik.touched.Name}
name="Name"
labelName="Name"
/>
You should avoid mixing the state when using Formik. Formik will take care of the state for you.
import { Formik, Form, useField, ErrorMessage } from "formik";
import * as Yup from "yup";
import Select from "react-select";
const iceCreamOptions = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
];
const FormSelect = ({ name, options }) => {
const [field, meta, helpers] = useField(name);
return (
<>
<Select
name={name}
value={field.value}
onChange={(value) => helpers.setValue(value)}
options={options}
onBlur={() => helpers.setTouched(true)}
/>
<ErrorMessage name={name} />
</>
);
};
const initialValues = {
icecream: null
};
const validationSchema = Yup.object().shape({
icecream: Yup.object()
.shape({
value: Yup.string(),
label: Yup.string()
})
.required("Please select a value")
.nullable()
});
export default function App() {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)}
validationSchema={validationSchema}
>
{(props) => {
return (
<Form>
<pre>{JSON.stringify(props, undefined, 2)}</pre>
<FormSelect name="icecream" options={iceCreamOptions} />
</Form>
);
}}
</Formik>
);
}
Example Working Sandbox
Write this code
onChange={(e:React.ChangeEvent<HTMLInputElement>)=> setFieldValue("title",e.target.value)}
or
onChange={(e)=> setFieldValue("title",e.target.value)}
I used
react-quill for one of form element.
react-hook-form form validation
yup/yup resolver for validation schema
Normal inputs and textareas working as expected, but validation for react-quill is not working.
These are my code snippets.
Custom react-quill wrapper element
import React, { useRef, useState } from "react";
import PropTypes from "prop-types";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import "react-quill/dist/quill.bubble.css";
import "react-quill/dist/quill.core.css";
function Editor(props) {
const [theme, setTheme] = useState("snow");
const { id, value, inputRef, placeholder, onChange } = props;
return (
<ReactQuill
id={id}
ref={inputRef}
theme={theme}
onChange={onChange}
value={value}
modules={{
toolbar: {
...Editor.modules.toolbar,
handlers: {
// image: handleImageUpload,
},
},
...Editor.modules,
}}
formats={Editor.formats}
bounds={".app"}
placeholder={placeholder ?? ""}
/>
);
}
/*
* Quill modules to attach to editor
* See https://quilljs.com/docs/modules/ for complete options
*/
Editor.modules = {
toolbar: [
// [{ header: '1' }, { header: '2' }, { font: [] }],
// [{ size: [] }],
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
["bold", "italic", "underline", "strike", "blockquote"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
["link", "image", "video"],
["clean"],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
],
clipboard: {
// toggle to add extra line breaks when pasting HTML:
matchVisual: false,
},
};
/*
* Quill editor formats
* See https://quilljs.com/docs/formats/
*/
Editor.formats = [
"header",
"font",
"size",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"list",
"bullet",
"indent",
"link",
"image",
"video",
];
/*
* PropType validation
*/
Editor.propTypes = {
placeholder: PropTypes.string,
};
export default Editor;
This is form component
const validationSchema = Yup.object().shape({
companyName: Yup.string().required("Company Name is required"),
... ...
jobDescription: Yup.string().required("Job description is required"), // react-quill
... ...
howtoApply: Yup.string().required("This field is required"),
applyUrl: Yup.string().required("This field is required"),
applyEmail: Yup.string().required("This field is required"),
});
const formOptions = { resolver: yupResolver(validationSchema) };
const { register, handleSubmit, reset, control, formState } = useForm(
formOptions
);
useEffect(() => {
console.log("formState", formState); // log for form value changes
});
... ... ...
<Controller
control={control}
name="jobDescription"
// rules={{
// required: "Description must have some content.",
// validate: (value) => {
// console.log("Controller", value);
// return (
// value.split(" ").length > 10 ||
// "Enter at least 10 words in the body."
// );
// },
// }}
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, isTouched, isDirty, error },
formState,
}) => (
<Editor
onChange={(description, delta, source, editor) => {
console.log(
"onChange:description",
description,
);
console.log("inputRef", ref);
onChange(description);
}}
value={value || ""}
inputRef={ref}
theme="snow"
id="jobDescription"
/>
)}
/>
I checked these issues,
https://github.com/zenoamaro/react-quill/issues/635
https://github.com/react-hook-form/react-hook-form/discussions/3372
but still not working.
Current behavior
I confirmed the changed markdown logged in console (onChange function in Editor), but can't see formState log of useEffect hook upon editor change.
(regardless of add rules prop to Controller or not)
Any help would be much appreciated
Thank you
export default function App() {
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors }
} = useForm();
useEffect(() => {
register("emailContent", { required: true, minLength: 15 });
}, [register]);
const onEditorStateChange = (editorState) => {
setValue("emailContent", editorState);
};
const onSubmit = (data) => {
console.log(data);
};
const editorContent = watch("emailContent");
return (
<div className="App">
<ReactQuill
theme="snow"
value={editorContent}
onChange={onEditorStateChange}
/>
<p className="Error">{errors.emailContent && "Enter valid content"}</p>
<input type="submit" onClick={handleSubmit(onSubmit)} />
</div>
);
}
I have created a code sandbox with a working example for react-hook-form. Here is the link:
https://codesandbox.io/s/quill-with-react-hook-form-validation-cdjru
<Controller
name="desc"
control={control}
rules={{
required: "Please enter task description",
}}
theme="snow"
modules={modules}
render={({ field }) => (
<ReactQuill
{...field}
placeholder={"Write Description"}
onChange={(text) => {
field.onChange(text);
}}
/>
)}
/>;
You Can Add Validator As ur like
I have a react code
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import SelectBox from '../SelectBox';
import Icon from '../Icon';
import Input from '../Input';
import CountryItem from './components/CountryItem';
const PhoneNumber = props => {
const { children, className } = props;
const [selectedOption, setSelectedOption] = useState('');
const [inputParam, setInputParam] = useState('');
const inputParamChangeHandler = event => {
const inputChar = event.target.value;
setInputParam(inputChar);
console.log({ inputChar });
};
const selectedOptionChangeHandler = event => {
const valueSelected = event.target.value;
setSelectedOption(valueSelected);
console.log({ valueSelected });
};
console.log({ children });
const childrenWithPropsFromParent = React.Children.map(children, child => child);
const optionsWithImage = [
{
value: '+880',
label: <CountryItem />,
},
{
value: '+55',
label: (
<span>
<span className="phone-number__country-flag">
<Icon name="home" />
</span>
<span>
<span className="phone-number__country-name">Brazil</span>
<span className="phone-number__country-code">(+55)</span>
</span>
</span>
),
},
{
value: '+40',
label: (
<span>
<span className="phone-number__country-flag">
<Icon name="home" />
</span>
<span>
<span className="phone-number__country-name">Romania</span>
<span className="phone-number__country-code">(+40)</span>
</span>
</span>
),
},
{
value: '+44',
label: (
<span>
<span className="phone-number__country-flag">
<Icon name="home" />
</span>
<span>
<span className="phone-number__country-name">United Kingdom</span>
<span className="phone-number__country-code">(+44)</span>
</span>
</span>
),
},
{
value: '+1',
label: (
<span>
<span className="phone-number__country-flag">
<Icon name="home" />
</span>
<span>
<span className="phone-number__country-name">Bahamas</span>
<span className="phone-number__country-code">(+1)</span>
</span>
</span>
),
},
];
return (
<div className={classNames('phone-number', className)}>
<div>Phone Number</div>
<div className="phone-number__wrapper">
<SelectBox
size="small"
value={selectedOption}
touched={false}
onChange={selectedOptionChangeHandler}
isSearchable={false}
isDisabled={false}
isClearable={false}
options={optionsWithImage}
width="small"
/>
<Input value={inputParam} onChange={inputParamChangeHandler} placeholder="XXXX XXX XXX" />
</div>
{/* {childrenWithPropsFromParent} */}
</div>
);
};
PhoneNumber.defaultProps = {
children: null,
className: null,
};
PhoneNumber.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};
export default PhoneNumber;
SelectBox Component:
import React, { Component } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import Select from 'react-select';
import CreatableSelect from 'react-select/creatable';
import './select-box.styles.scss';
import colorOptions from '../../helpers/colorOptions';
import Tooltip from '../Tooltip';
import Icon from '../Icon';
const sizeBasedSelector = (element, size) => {
switch (size) {
case 'tiny':
return `select-box__${element}--tiny`;
case 'small':
return `select-box__${element}--small`;
case 'large':
return `select-box__${element}--large`;
default:
return null;
}
};
class SelectBox extends Component {
constructor(props) {
super(props);
this.state = {
selectedOption: props.value,
};
}
customTheme = theme => {
const { primary, primary75, primary50, primary25 } = this.props.colors;
return {
...theme,
colors: {
...theme.colors,
primary,
primary75,
primary50,
primary25,
},
};
};
renderPlaceHolder = () => {
const { preIcon, placeholderText, size } = this.props;
return (
<div className="select-box__placeholder">
{preIcon && (
<span className={classnames('select-box__pre-icon', sizeBasedSelector('pre-icon', size))}>
{preIcon}
</span>
)}
<span
className={classnames(
'select-box__placeholder-text',
sizeBasedSelector('placeholder-text', size)
)}
>
{placeholderText}
</span>
</div>
);
};
handleChange = selectedOption => {
this.setState({ selectedOption });
this.props.onChange(selectedOption);
};
handleInputChange = inputValue => {
this.props.onInputChange(inputValue);
};
getFlags = () => {
const { isClearable, isDisabled, isMulti, isSearchable } = this.props;
return {
isClearable,
isDisabled,
isMulti,
isSearchable,
};
};
render() {
const { selectedOption } = this.state;
const {
autoFocus,
className,
classNamePrefix,
colors,
errorMsg,
label,
name,
options,
size,
touched,
isMulti,
isDisabled,
isCreatable,
width,
required,
} = this.props;
const hasError = touched && errorMsg;
const selectProps = {
...this.getFlags(),
autoFocus,
className: classnames(
`${className} ${className}--${size}`,
sizeBasedSelector('width', width),
{
'select-box-container--notMulti': !isMulti,
'select-box-container--error': hasError,
}
),
classNamePrefix,
colors,
theme: this.customTheme,
value: selectedOption,
name,
options,
onInputChange: this.handleInputChange,
onChange: this.handleChange,
placeholder: this.renderPlaceHolder(),
required,
};
return (
<>
{label && (
<span
className={classnames(`select-box__label select-box__label--${size}`, {
'select-box__label--required': required,
'select-box__label--disabled': isDisabled,
})}
>
{label}
</span>
)}
<div className="select-box-wrapper">
{isCreatable ? <CreatableSelect {...selectProps} /> : <Select {...selectProps} />}
{errorMsg && (
<Tooltip
classNameForWrapper="select-box__tooltip-wrapper"
classNameForTooltip="select-box__tooltip"
content={errorMsg}
position="bottom-right"
gap={0}
>
<Icon name="invalid" />
</Tooltip>
)}
</div>
</>
);
}
}
SelectBox.defaultProps = {
autoFocus: false,
className: 'select-box-container',
classNamePrefix: 'select-box',
colors: {
primary: colorOptions.coolGray20,
primary75: colorOptions.coolGray45,
primary50: colorOptions.coolGray70,
primary25: colorOptions.coolGray95,
},
errorMsg: '',
isClearable: true,
isCreatable: false,
isDisabled: false,
isMulti: false,
isSearchable: true,
label: '',
name: 'select-box',
onChange: () => {},
onInputChange: () => {},
options: [],
placeholderText: 'Select...',
preIcon: null,
required: false,
size: 'small',
touched: false,
value: null,
width: 'small',
};
SelectBox.propTypes = {
autoFocus: PropTypes.bool,
className: PropTypes.string,
classNamePrefix: PropTypes.string,
colors: PropTypes.shape({
primary: PropTypes.string,
primary75: PropTypes.string,
primary50: PropTypes.string,
primary25: PropTypes.string,
}),
errorMsg: PropTypes.string,
isClearable: PropTypes.bool,
isCreatable: PropTypes.bool,
isDisabled: PropTypes.bool,
isMulti: PropTypes.bool,
isSearchable: PropTypes.bool,
label: PropTypes.string,
name: PropTypes.string,
onChange: PropTypes.func,
onInputChange: PropTypes.func,
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string,
})
),
preIcon: PropTypes.node,
placeholderText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
value: PropTypes.oneOf([
PropTypes.shape({
label: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})
),
]),
required: PropTypes.bool,
size: PropTypes.oneOf(['tiny', 'small', 'large']),
touched: PropTypes.bool,
width: PropTypes.oneOf(['tiny', 'small', 'large', 'full']),
};
export default SelectBox;
now I want to take the value of the selectedOption from the SelectBox component but this current code base giving me error while I select a option from the SelectBox. The error is
Uncaught TypeError: Cannot read property 'value' of undefined
at Object.selectedOptionChangeHandler [as onChange] (PhoneNumber.jsx:22)
at Object.onChange (SelectBox.jsx:71)
at StateManager.callProp (stateManager-04f734a2.browser.esm.js:98)
at StateManager._this.onChange (stateManager-04f734a2.browser.esm.js:38)
at Select._this.onChange (Select-9fdb8cd0.browser.esm.js:1079)
at Select._this.setValue (Select-9fdb8cd0.browser.esm.js:1106)
at Select._this.selectOption (Select-9fdb8cd0.browser.esm.js:1155)
at onSelect (Select-9fdb8cd0.browser.esm.js:1732)
at HTMLUnknownElement.callCallback (react-dom.development.js:336)
at Object.invokeGuardedCallbackDev (react-dom.development.js:385)
How to get the value from onChange from the SelectBox using the current implementation?
Your SelectBox component's handleChange callback outputs the selected value versus an event, so you should consume the value.
SelectBox
handleChange = selectedOption => {
this.setState({ selectedOption });
this.props.onChange(selectedOption); // <-- sends selected value!
};
PhoneNumber
const selectedOptionChangeHandler = valueSelected => { // <-- consume valueSelected
setSelectedOption(valueSelected);
console.log({ valueSelected });
};
I have a react component and I want to pass down onChange as a prop to a child component, I'm using atomic design, here's my code:
HeadComponent.js
class Headcomponent extends React.Component{
constructor (props) {
super(props);
this.state = {
email: '',
password: '',
formErrors: {email: '', password: ''},
emailValid: false,
passwordValid: false,
formValid: false,
items: [],
}
}
handleUserInput = (e) => {
const name = e.target.name;
const value = e.target.value;
this.setState({[name]: value},
() => { this.validateField(name, value) });
}
render(){
const fields= [
{
label: 'hhdghghd',
placeholder: 'fhfhhfhh 1',
ExampleMessage: this.state.formErrors.email ,
ErrorMessage: 'error message for input 1',
inputValue: this.state.email,
onChange: this.handleUserInput //this is where I'm passing my function
},
{
label: 'fffff',
placeholder: 'tttttt 2',
ExampleMessage: 'example message for second label',
ErrorMessage: 'error message for input 2',
onChange: this.handleUserInput
},
]
return (
<div>
<Form fields={fields} buttonText="Submit"/>
</div>
);
}
}
export default Headcomponent;
Form.js
const Form = props => (
<form className="Form">
{
props.fields.map((field, i) => (<LabeledInput label={field.label}
placeholder={field.placeholder}
ExampleMessage={field.ExampleMessage}
ErrorMessage={field.ErrorMessage}
onChange= {(e)=> field.onChange}
inputValue= {field.inputValue}
key={i}
passwordError={props.passwordError}
/>))
}
<Button text={props.buttonText} />
</form>
);
Form.propTypes = {
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
buttonText: PropTypes.string.isRequired,
};
export default Form;
LabeledInput
const LabeledInput = props => (
<div className={`form-group `} >
<Label text={props.label} />
<Input inputValue={props.inputValue} placeholder={props.placeholder} type="text" onChange={(e)=> props.onChange} />
<ErrorMessage text={props.ErrorMessage} />
<ExampleMessage ExampleMessage={ props.ExampleMessage} />
</div>
);
LabeledInput.propTypes = {
label: PropTypes.string.isRequired,
placeholder: PropTypes.string,
onChange: PropTypes.func.isRequired,
//value: PropTypes.string.isRequired,
exampleText: PropTypes.string,
};
export default LabeledInput;
Input.js
const Input = props => (
<input type={props.type} class="form-control form-control-success is-valid" placeholder={props.placeholder} value={props.inputValue} className="form-control form-control-success"
onChange={ (e)=> props.onChange } />
);
Input.propTypes = {
inputValue: PropTypes.string,
type: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func.isRequired,
};
export default Input;
How to pass handleUserInput from HeadComponent.js down to Input.js, so far using props I can't get it to trigger while changing text.
You forgot to actually call field.onChange method. Instead of :
onChange= {(e)=> field.onChange}
You can change it to:
onChange= {field.onChange}
I also noticed that your handleUserInput set state at :
{ [name]: e.target.value }
However, you are not setting name to any of the inputs and that's not gonna work.
Please have a look at my working sample:
const LabeledInput = props => (
<div className="form-group">
<label>{props.label}</label>
<input
className="form-control"
type="text"
placeholder={props.placeholder}
onChange={props.onChange} // That's how you have to call onChange
name={props.name} // You are not setting this prop
/>
<div>{props.ErrorMessage}</div>
<div>{props.ExampleMessage}</div>
</div>
)
const Form = ({ buttonText, fields }) => (
<form className="Form">
{fields.map((field, i) => <LabeledInput key={i} {...field} />)}
<button className="btn btn-primary">{buttonText}</button>
</form>
)
class Headcomponent extends React.Component {
constructor() {
super()
this.state = {
email: '',
password: '',
formErrors: {email: '', password: ''},
emailValid: false,
passwordValid: false,
formValid: false,
items: [],
}
this.handleUserInput = this.handleUserInput.bind(this)
}
handleUserInput(e) {
const name = e.target.name
const value = e.target.value
// Now that you have `name` and `value`, state updates are going to work
this.setState({
[name]: value
})
}
render() {
const fields = [
{
label: 'Email',
placeholder: 'email placeholder',
ExampleMessage: this.state.formErrors.email ,
ErrorMessage: 'error message for input 1',
inputValue: this.state.email,
onChange: this.handleUserInput,
name: 'email',
},
{
label: 'Password',
placeholder: 'password placeholder',
ExampleMessage: 'example message for second label',
ErrorMessage: 'error message for input 2',
onChange: this.handleUserInput,
name: 'password',
},
]
return (
<div>
<Form fields={fields} buttonText="Submit"/>
<div style={{ marginTop: 20 }}>
<div>state.email: {this.state.email}</div>
<div>state.password: {this.state.password}</div>
</div>
</div>
)
}
}
ReactDOM.render(
<Headcomponent />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<div id="root"></div>