Misunderstanding react-hook-forms.
I have a form for editing some stuff. Form contains fieldArray.
I set initial formData in useForm hook using default values
const methods = useForm({ defaultValues: defaultValues });
where defaultValues is
const defaultValues = {
test: [
{
name: "useFieldArray1"
},
{
name: "useFieldArray2"
}
]
};
And fieldArray. Here I'm using Controller (it's simplified case - in fact Custom input Controller more complex)
<ul>
{fields.map((item, index) => {
return (
<li key={item.id}>
<Controller
name={`test[${index}].name`}
control={control}
render={({value, onChange}) =>
<input onChange={onChange} defaultValue={value} />}
/>
<button type="button" onClick={() => remove(index)}>
Delete
</button>
</li>
);
})}
</ul>
When form is rendered everything is fine. Default values are displayed in input fields. But when I delete all fields and click append - new fields are not empty ... Default values are displayed
again. And it happens only with Controller. Why it happens ? And how I can avoid it?
Please, here is CodeSandBox link. Delete inputs and press append to reproduce what I am saying.
https://codesandbox.io/s/react-hook-form-usefieldarray-nested-arrays-forked-7mzyw?file=/src/fieldArray.js
Thanks
<Controller
name={name}
rules={rules}
defaultValue={defaultValue ? defaultValue : ''}
render={({ field, fieldState }) => {
return (
<TextField
inputRef={field.ref}
{...props}
{...field}
label={label}
value={field.value ? field.value : ''}
onChange={(event) => {
field.onChange(event.target.value);
props.onChange && props.onChange(event);
}}
style={props.style || { width: '100%' }}
helperText={fieldState?.error && fieldState?.error?.message}
error={Boolean(fieldState?.error)}
size={props.size || 'small'}
variant={props.variant || 'outlined'}
fullWidth={props.fullWidth || true}
/>
);
}}
/>
Related
I have a basic react hook form with field array. With append the form fields replicates as expected. For each array, I want the user to choose some field to enter without entering the other. I am using radio button and useState to achieve this. However, when i change the selection in an array, the selections in the other arrays changes as well. Please how do i correct this ? Or is there a better way to achieve this functionality. Thanks in advance for your help. The code is found below. I also have codeSandbox: https://codesandbox.io/s/usefieldarray-react-hook-form-2yp3vb?file=/src/App.js:0-3753
export default function App() {
const { handleSubmit, control } = useForm({
defaultValues: {
Detail: [
{
userName: {},
officeAddress: {},
homeAddress: {}
}
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: "Detail"
});
const [checked, setChecked] = useState();
// onChange function for the address forms
const changeAddressForm = (e) => {
setChecked(e.target.value);
};
const onSubmit = async (data) => {};
return (
<div className="App">
<h1>Selecting a different form in each field array</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<ul>
{fields.map((field, index) => {
return (
<li
key={field.id}
className="w3-border w3-border-green w3-padding"
>
<div>
<div className="w3-padding-large">
<label>Username</label>
<Controller
name={`Detail.${index}.userName`}
control={control}
render={({ field }) => (
<Input
onChange={(value) => field.onChange(value)}
style={{ width: 200 }}
/>
)}
/>
</div>
<div>
<Radio.Group onChange={changeAddressForm} value={checked}>
<Radio value={1}>Office address</Radio>
<Radio value={2}>Home address</Radio>
</Radio.Group>
</div>
<div className="w3-padding-large">
{checked === 1 && (
<div>
<label>Office address</label>
<Controller
name={`Detail.${index}.officeAddress`}
control={control}
render={({ field }) => (
<Input
onChange={(value) => field.onChange(value)}
style={{ width: 200 }}
/>
)}
/>
</div>
)}
</div>
<div className="w3-padding-large">
{checked === 2 && (
<div>
<label>Home address</label>
<Controller
name={`Detail.${index}.homeAddress`}
control={control}
render={({ field }) => (
<Input
onChange={(value) => field.onChange(value)}
style={{ width: 200 }}
/>
)}
/>
</div>
)}
</div>
</div>
</li>
);
})}
</ul>
<section>
<button
type="button"
onClick={() =>
append({
userName: {},
homeAddress: {},
officeAddress: {}
})
}
>
Append
</button>
</section>
</form>
</div>
);
}
I have a formik fieldarray where every row would have 3 dropdowns. And I would like to have a validation schema which works this way:
If email is selected in the first dropdown, then the value given in the 3rd dropdown(it has allowAdditions) to be validated if a valid email id is given or not.
If a <, <=, >, >= operator is selected in the second dropdown,then the value given in the 3rd dropdown(it has allowAdditions) to be validated if it has a number only.
Before achieveing this, I tried to give a simple validation to validate if the length od the 1st dropdown is greater than 6. Though the validation worked, no warning message is shown as it comes with normal FormInput in formik. Not sure if this is because it is a dropdown.
Validation:
const validationSchema = object().shape({
rows: array().of(
object().shape({
value: string().min(4, "too short").required("Required")
})
)
});
Formik:
<Formik
initialValues={{ rows: initialValues }}
onSubmit={(values) => {
// transform the rows to add the condition key for each row object
const output = values.rows.map((row, index) => {
if (index === 0) {
return { ...row, condition: "if" };
} else {
return { ...row, condition: "and" };
}
});
console.log(output);
}}
validationSchema={validationSchema}
>
{({ handleSubmit, values, setFieldValue }) => (
<Form onSubmit={handleSubmit} className={"rulesetForm"}>
<pre>{JSON.stringify(values, null, 2)}</pre>
<FieldArray
name="rows"
render={({ push, remove }) => {
return (
values.rows.length > 0 &&
values.rows.map((row, index) => {
const mainFieldOptions = getMainFieldOptions(
values.rows,
index
);
return (
<Grid key={`mainfield-operation-value-${index}`}>
<Grid.Row className={"rulesetGrid fluid"}>
{index === 0 ? (
<p className="condition"> If</p>
) : (
<p className="condition"> And</p>
)}
<Dropdown
name={`rows.${index}.mainField`}
className={"dropdown fieldDropdown"}
widths={2}
placeholder="Field"
fluid
selection
options={mainFieldOptions}
value={row.mainField || ""}
onChange={(e, { value }) => {
setFieldValue(`rows.${index}.mainField`, value);
}}
/>
<Dropdown
name={`rows.${index}.operation`}
className={"dropdown operationDropdown"}
widths={2}
placeholder="Operation"
fluid
selection
options={operation}
value={row.operation}
onChange={(e, { value }) =>
setFieldValue(`rows.${index}.operation`, value)
}
/>
<Dropdown
name={`rows.${index}.value`}
className={"dropdown valueDropdown"}
widths={1}
placeholder="Value"
fluid
search
allowAdditions
selection
multiple
options={dropDownOptions}
value={row.value}
onAddItem={handleAddition}
onChange={(e, { value }) =>
setFieldValue(`rows.${index}.value`, value)
}
/>
{values.rows.length - 1 === index && (
<Icon
className={" plus icon plusIcon"}
onClick={() => push(initialValues[0])}
/>
)}
{values.rows.length !== 1 && (
<Icon
className={"minus crossIcon"}
onClick={() => remove(index)}
/>
)}
</Grid.Row>
</Grid>
);
})
);
}}
/>
<div>
<div style={{ marginTop: "1rem" }}>
<Button
floated="right"
type="submit"
variant="contained"
primary={true}
>
Submit
</Button>
</div>
</div>
</Form>
)}
</Formik>
Here is the sandbox link: https://codesandbox.io/s/semantic-ui-example-forked-lmqi9?file=/example.js:1826-6181
Any help is really really appreciated and would mean a lot. Thanks in advance
You need to use the Yup When along with Test
Working Sandbox
CodeSandbox
I am using react-hook-form and using third party DatePicker. Since it's a custom component using it as a controlled component to register it. This works fine
<Controller
control={control}
name="reviewStartDate"
render={({ field: { onChange, onBlur, value } }) => (
<DatePicker
className={`form-control ${errors.reviewStartDate ? 'is-invalid' : ''}`}
customInput={<input />}
wrapperClassName="datePicker"
onChange={onChange}
onBlur={onBlur}
selected={value ? new Date(value) : ''}
dateFormat='dd-MMM-yyyy'
/>
)}
/>
Similarly/however, I am using thirdparty Multiselect. Here the value is not being registered. It does show the selected value but when I submit the form the value is not present in data.
<Controller
control={control}
name="rootCauseAnalysisCategory"
render={({ field: { value } }) => (
<Multiselect
options={rootCauseAnalysisCategorys}
isObject={false}
showCheckbox={true}
hidePlaceholder={true}
closeOnSelect={false}
selectedValues={value}
/>
)}
/>
Similarly
The <MultiSelect /> component has onSelect and onRemove props, so you can just pass onChange to them. This will work because they both have the signature that the first argument is an array containing the current selected values.
<Controller
control={control}
name="rootCauseAnalysisCategory"
defaultValue={[]}
render={({ field: { value, onChange } }) => (
<Multiselect
options={rootCauseAnalysisCategorys}
isObject={false}
showCheckbox={true}
hidePlaceholder={true}
closeOnSelect={false}
onSelect={onChange}
onRemove={onChange}
selectedValues={value}
/>
)}
/>
UPDATE
If you want to access the current value for rootCauseAnalysisCategory, you have to use watch. Please note, that it is also important to either provide a defaultValue at the <Controller /> field level or call useForm with defaultValues. In the example i passed the defaultValue at the field level.
function App() {
const { control, handleSubmit, watch } = useForm();
const onSubmit = (data) => {
console.log(data);
};
const rootCauseAnalysisCategorys = ["Category 1", "Category 2"];
const rootCauseAnalysisCategory = watch("rootCauseAnalysisCategory");
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="rootCauseAnalysisCategory"
defaultValue={[]}
render={({ field: { value, onChange } }) => (
<Multiselect
options={rootCauseAnalysisCategorys}
isObject={false}
showCheckbox={true}
hidePlaceholder={true}
closeOnSelect={false}
onSelect={onChange}
onRemove={onChange}
selectedValues={value}
/>
)}
/>
{rootCauseAnalysisCategory?.includes("Category 1") && <p>Category 1</p>}
<input type="submit" />
</form>
</div>
);
}
I spend almost 1 week to solve this problem.
I want to disable the button while the field gets an error.
Somehow always the form.getFieldsError() gets Array(0), even the error message this is required or should be 7 digit was showed up.
If you have any idea please help me.
this is my code.
import React from 'react';
import { Form, Input, Button } from 'antd';
import { MinusCircleOutlined, PlusOutlined } from '#ant-design/icons';
const DynamicFieldSet = () => {
const MAX = 3;
const [form] = Form.useForm();
const onValuesChange = (changedValues, allValues) => {
console.log(allValues.lines);
};
console.log(
// this is always true
form.getFieldsError().filter(({ errors }) => errors.length).length === 0
);
return (
<Form
initialValues={{ lines: [''] }}
onValuesChange={onValuesChange}
form={form}
name={'lines'}
>
<Form.List name={'lines'}>
{(fields, { add, remove }, { errors }) => {
return (
<div>
{fields.map((field, index) =>
index < MAX ? (
<Form.Item required={false} key={field.key}>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
message: 'this is required',
},
{
min: 7,
max: 7,
message: 'should be 7 digit',
},
]}
noStyle
>
<Input
placeholder=""
style={{ width: '50%' }}
type="number"
/>
</Form.Item>
{index === 0 ? (
<>
<PlusOutlined
onClick={() => {
add();
}}
/>
</>
) : index === MAX - 1 ? (
<MinusCircleOutlined onClick={() => remove(field.name)} />
) : (
<>
<PlusOutlined
onClick={() => {
add();
}}
/>
<MinusCircleOutlined
onClick={() => remove(field.name)}
/>
</>
)}
</Form.Item>
) : null
)}
</div>
);
}}
</Form.List>
<Form.Item>
<Button disabled={false} htmlType="submit">
Send
</Button>
</Form.Item>
</Form>
);
};
export default DynamicFieldSet;
I'm not really sure why but form.getFieldsError() is just not going to work. In order to achieve what you need, you should try using form.validateFields() instead.
The validateFields method, returns a promise that shows all the fields' data or errors on them:
form.validateFields()
.then(values => {
// Do something with values
})
.catch(errors => {
// Do something with errors;
});
Note that this will validate all the fields that you haven't even touched, causing all the provided error messages of all the fields to be shown. So, in case you want to validate just one field or so, all you need to do is to provide an array of field's names like ['myInput'].
From here, just create a new state to be set when errors appear and use it to show/hide-enable/disable your button.
I am rendering a customized react-querybuilder. Whenever I add a rule the input box is rendered with default empty value. The problem is that when I enter one character in the Input box it loses focus.
This does seem like a duplicate question. But, after trying out the solutions mentioned below -
Storing value in state.
autoFocus on input tag (this is messed it up even further!)
I am not able to figure it out.
I have added the code to stackblitz
Please find the relevant code:
const [queryOutput, setQueryOutput] = useState("");
...
<QueryBuilder
{...props}
controlElements={{
combinatorSelector: props => {
let customProps = {
...props,
value: props.rules.find(x => x.combinator) ? "or" : props.value
};
return (
<div className="combinator-wrapper">
<button className="form-control-sm btn btn-light mt-2">
{customProps.value.toUpperCase()}
</button>
</div>
);
},
addRuleAction: props => {
return (
<button
className={props.className}
title={props.title}
onClick={e => {
return props.handleOnClick(e);
}}
>
+ Add New Rule
</button>
);
},
addGroupAction: props => {
return (
<button
className={props.className}
title={props.title}
onClick={e => {
return props.handleOnClick(e);
}}
>
{props.label}
</button>
);
},
valueEditor: ({
className,
field,
operator,
inputType,
value,
handleOnChange,
level
}) => {
if (field === "enabled") {
return (
<input
className={className}
type="checkbox"
checked={value !== "" ? value : false}
onChange={e => handleOnChange(e.target.checked)}
/>
);
}
return (
<input
className={className}
value={value}
onChange={e => handleOnChange(e.target.value)}
/>
);
}
}}
onQueryChange={query => {
let customQuery = { ...query, combinator: "or" };
return setQueryOutput(
formatQuery(customQuery ? customQuery : query, "sql")
);
}}
/>
Needed to assign a reference of the valueEditor component rather than defining it inline(so that it does not create a new instance on every render).
Updated the relevant code:
const valueEditor = ({
className,
field,
operator,
inputType,
value,
handleOnChange,
level
}) => (
<input
className={className}
value={value}
onChange={e => handleOnChange(e.target.value)}
/>
);
.....
<QueryBuilder
{...props}
controlElements={{
...
valueEditor
...
}}
/>