Formik FieldArray nested object validations with Yup - javascript

https://codesandbox.io/s/wonderful-brattain-928gd
Above, I have added some sample code of the problem I am trying to figure out. I am not sure how to map the errors to the correct items in the FieldArray.
In the example, there are yes/no radio buttons which allow a user to indicate whether they have foods they want to add. If they select 'yes', the food options appear and they must select at least 1 of the foods and enter its expiration date to fully validate.
I am trying to add an "expiration" validation error when the users fails to enter an expiration date in the text field. For example, if I select "Beef" and do not enter an expiration date, the errors populate in the Formik errors. However, I don't know how to map that error to the correct expiration text box.
Any help is appreciated!
Note:
Validations are only triggered when the validated button is clicked

There is codesandbox and code shown belown:
<Form>
<pre
style={{
textAlign: "left"
}}
>
<h3>Data</h3>
{JSON.stringify(values, null, 2)}
<h3>Errors</h3>
{JSON.stringify(errors, null, 2)}
</pre>
<Field
name="food.hasFood"
value
type="radio"
onChange={e => {
setFieldValue("food.hasFood", true);
}}
/>{" "}
Yes
<Field
name="food.hasFood"
value={false}
type="radio"
onChange={e => {
setFieldValue("food.hasFood", false);
}}
/>{" "}
No
{values.food.hasFood && (
<FieldArray name="food.selected">
{arrayHelpers => {
return foodTypes.map(item => (
<div key={item}>
<Field
name={item}
value={item}
type="checkbox"
as={Checkbox}
checked={values.food.selected
.map(f => f.name)
.includes(item)}
onChange={e => {
if (e.target.checked) {
arrayHelpers.push({
name: e.target.value,
expiration: ""
});
} else {
const index = values.food.selected
.map(f => f.name)
.indexOf(e.target.value);
arrayHelpers.remove(index);
}
}}
/>
{item}
{errors.food && touched.food && (
<p>
{Array.isArray(errors.food.selected)
? ""
: errors.food.selected}
</p>
)}
{values.food.selected.map((selectedFood, index) => {
if (item === selectedFood.name) {
return (
<div>
<Field
key={index}
as={TextField}
name={`food.selected[${index}].expiration`}
/>
{console.log(errors)}
{errors.food && touched.food && (
<p>
{Array.isArray(errors.food.selected)
? errors.food.selected[index].expiration
: errors.food.selected}
</p>
)}
</div>
);
}
return null;
})}
</div>
));
}}
</FieldArray>
)}
<button
type="button"
onClick={() => {
validateForm();
}}
>
Validate
</button>
</Form>

Related

How to populate a dropdown menu in React

I am trying to populate my dropdown with the value of testType but it is not showing up. Is there something I am missing? When I print the data being sent, it is correct but it's not displaying in the actual dropdown menu. I don't want this to be populated everytime - only when the user clicks edit criteria (not for add question).
I'm currently testing it for criteriaNum = 1 so the dropdown should display testType = 'Visual Check'.
This is my EditQuestionnaire.js:
export const PureEditQuestionnaire = ({
criteriaNum,
criteriaFields,
image,
isSaving,
testType,
onFieldChange,
onClickSave,
getInputValue,
getDropdownValue,
}) => (
<>
<Header
tabIndex="-1"
isAddCriteria={!!criteriaNum}
onClickSave={onClickSave}
isSaving={isSaving}
isOnTop={true}
/>
<CategoryContainer>
<p>
Fields with <span style={{ color: "red" }}>*</span> are required
</p>
{criteriaFields.map((field) => (
<FieldContainer key={field.name}>
<label htmlFor={field.name.replaceAll(" ", "-")} className="fields">
{field.name}
</label>
{field.type === "input" ? (
<TextField
id={field.name.replaceAll(" ", "-")}
value={getInputValue(field.name)}
fullWidth={true}
required={true}
placeholder={"Enter the " + field.name}
multiline={field.name === "Description" && true}
rows={field.name === "Description" ? 4 : 0}
onChange={(event) =>
onFieldChange(field.name, event.target.value)
}
/>
) : field.type === "dropdown" ? (
<>
{/* <label htmlFor={field.name}>Select the {" " + field.name}</label> */}
<div id={field.name.replaceAll(" ", "-") + "-desc"}>
Select the {" " + field.name}
</div>
<select
id={field.name.replaceAll(" ", "-")}
aria-describedby={field.name.replaceAll(" ", "-") + "-desc"}
value={field}
onChange={(event) =>
onFieldChange(field.name, event.target.value)
}
>
<option selected disabled>
{/* Select a {" " + field.name} */}
</option>
{field.fields.map((field) => (
<option key={field} value={field}>
{field}
</option>
))}
</select>
</>
) : (
<ImageDropbox
id={field.name.replaceAll(" ", "-")}
image={image}
imageFieldName={field.name}
onImageChange={onFieldChange}
/>
)}
</FieldContainer>
))}
</CategoryContainer>
<CategoryContainer>
<h1>Answers</h1>
<AnswerTable />
</CategoryContainer>
<Header
onClickSave={onClickSave}
isSaving={isSaving}
isOnTop={false}
isAddCriteria={!!criteriaNum}
/>
</>
)
This is the getDropdownValue I'm exporting from my PureEditQuestionnaire:
const getDropdownValue = (fieldName) => {
switch (fieldName) {
case "Test Type":
return criteriaValues.testType
case "Capability":
return criteriaValues.capability
default:
return ""
}
}
Double-check you usage of the select and option element. I'm also not sure what's the object type of field.
Hopefully this helps:
How to use select option in react js?

Focus next input field after maxLength is full. React.js

Hi I am having a problem with my for and inputs. I have 6 inputs with maxLength = 1 and i want to focus next input when when value is entred.
<Form
form={form}
className={styles.form}
onFinish={onFinish}
initialValues={initialValues}
>
<div className={styles.labeContainer}>
<LabelField name="code">
{({ value, onChange }) => {
return (
<div>
{inputsArr.map((_, index) => (
<Input
key={index}
maxLength={1}
defaultValue={value[index]}
className={styles.codeInput}
onChange={(val) => {
onChange(
value.substring(0, index) +
val.target.value +
value.substring(index + 1)
);
}}
/>
))}
</div>
);
}}
</LabelField>
</div>
</Form>
I was trying to use something with refs but i had no idea how to get this working. Also did some reseasrch on this, but nothing helped. I am using rc-field-form but docs are very vage

React Formik and Checkbox

I need to map dynamicaly on a array. This array can change with a search field and i need to know what checkbox are checked to send my data with a submit but i dont find the right way to do to check the checkbox with Formik. This is my code :
const initialValues = {
checked: [],
};
const onSubmit = (values) => {
console.log(values);
};
<Formik onSubmit={onSubmit} initialValues={initialValues}>
{({ values, handleSubmit }) => (
<form
onSubmit={handleSubmit}
noValidate={true}
>
{Listing.map((Search, index) => {
if (
Search.installationAddress.includes(
searchMeter
) ||
Search.deviceId.includes(searchMeter)
) {
return (
<div
key={index}
>
<Checkbox
color="text.standard"
label={`${Search.installationAddress}`+" Compteur N° " +`${Search.deviceId}`}
type="checkbox"
name="checked"
value={{
contractAccountId:
Search.contractAccountId,
deviceId: Search.deviceId,
}}
size="large"
/>
</div>
);
}
})}
<Checkbox
color="text.standard"
label="I confirm"
onChange={() => setValidationButton(!validationButton)}
value=""
checked={validationButton}
/>
<Button
type="submit"
disabled={!validationButton}
>
ACTIVATE
</Button>
</form>
)}
</Formik>

How to get the collapse to open depending if there is an error - ReactJs

Hi all I have following code.
I have two inputs and that two inputs are mandatory.
First input name is Name and when I am submit with any values it says Please input Name! . This part was working great.
Second input, which name is Short Info was located in collapse. And that collapse is <Form.Item>, it's mean that user can press + and add multiple Short Info inputs.
My problem is with that Short Info. When user press submit only first input shows error. For Short Info user should open that collapse to see error, which is not good.
Now how can I automatically open that collapse panel if there is error ?
Here is my code.
const [dataFromBackend, setDataFromBackend] = useState([]);
const addNewField = () => {
setDataFromBackend([...dataFromBackend]);
};
const submitForm = (values) => {
console.log('Received values of form: ', values);
};
const Header = ({ remove, index }) => {
return (
<Col align="center" span={1} justify="end">
<MinusCircleFilled
onClick={(e) => {
e.stopPropagation();
remove(index);
}}
/>
</Col>
);
};
return (
<>
<Form
name="validate_other"
onFinish={submitForm}
initialValues={{ values: [''] }}
>
<Form.Item
name="name"
label="Name"
rules={[
{
required: true,
message: 'Please input Name!',
},
]}
>
<Input />
</Form.Item>
<Form.List name="values">
{(fields, { add, remove }) => {
return (
<Row gutter={24}>
<Col span={24} md={24}>
<Card
title="Price"
extra={
<PlusCircleFilled
style={{
cursor: 'pointer',
fontSize: '20px',
color: '#00AEE6',
}}
onClick={() => {
add();
addNewField();
}}
/>
}
>
<div key={fields.key}>
{fields.map((field, i) => (
<div key={i}>
<Collapse accordion={true}>
<Panel
key={i}
header={<Header remove={remove} index={i} />}
>
<Col span={24} md={24}>
<Form.Item
name={[field.name, 'shortinfo']}
fieldKey={[field.fieldKey, 'shortinfo']}
label={'Short Info'}
rules={[
{
required: true,
message: 'Please input short info!',
},
]}
>
<Input />
</Form.Item>
</Col>
</Panel>
</Collapse>
</div>
))}
</div>
</Card>
</Col>
</Row>
);
}}
</Form.List>
<Button htmlType="submit">Save</Button>
</Form>
</>
)
Please help me to resolve this problem, thanks.
My solution would be as follows:
First of all, render all <Panels> inside the same <Colapse>, also remove accordion={true} from Colapse (so multiple Fields can be open at once) and add forceRender={true} to Panels, otherwise items inside a Panel are lazy loaded (if lazy load would be active, <Form.Item> wouldn't be rendered and don't show up as error)
<Collapse>
{fields.map((field, i) => {
return (
<Panel
...
forceRender={true}
...
>
...
</Panel>
);
})}
</Colapse>
Add a state to keep track of currently active keys and add it to the colapse, also remove the <div key={i}> it is not needed, since <Panel key={i}> is already a container, furthermore it will prevent onChange of Colapse to trigger and mess up your styling
const [activeKeysColapse, setactiveKeysColapse] = useState([]);
...
<Collapse
activeKey={activeKeysColapse}
onChange={(newActiveKeys) => {
// otherwise panels couldn't be open/closed by click
setactiveKeysColapse(newActiveKeys);
}}
>
{fields.map((field, i) => {
return (
<Panel
key={i}
forceRender={true}
>
...
</Panel>
);
})}
</Colapse>
Add onFinishFailed listener to form
const submitFormFailed = (errors) => {
// get all errorFields from the list and map the
// `activeKeys` to the index (because `<Panel key={i}>`)
const newShortInfoErrors = errors.errorFields
.filter((el) => el.name[0] === 'values')
.map((el) => el.name[1].toString());
// setting the state will close all fields which
// are valid and open all with an error message
setactiveKeysColapse(newShortInfoErrors);
};
...
<Form
...
onFinishFail={submitFormFailed}
>
Based on the code on your StackBlitz, this will do exactly what you want
I hope this will solve your problem, if there is anything unclear, feel free to comment

get field value from Formik & Material UI form

I am trying to disable a checkbox group based on the value of a radio group. I followed the method used in the last part of the Formik tutorial. Using react context removes a lot of clutter from the form itself but I'm not sure how to expose some values now.
In the form below, in the CheckboxGroup component, I'm attempting to print the word disabled as an attribute of checkbox1 if radio4's value is "yes". I'm not sure what value should be used here as fields doesn't work. How do I pass a value to the form given the React Context method used?
The form:
export default function HealthAssessmentForm() {
return (
<Formik
initialValues={{
radio4: '',
symptoms: '',
}}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
console.log(JSON.stringify(values, null, 2));
}}
validator={() => ({})}
>
<Form>
<RadioInputGroup
label="Disable the checkbox?"
name="radio4"
options={['Yes','No']}
/>
<CheckboxGroup
{(fields.radio4.value === "yes") ? "disabled" : null}
name="checkbox1"
options={[
{name:"hello",label:"hello"},
{name:"there",label:"there"},
]}
/>
<button type="submit">Submit</button>
</Form>
</Formik>
)
}
I'm not sure the custom components are relevant here but...
const RadioInputGroup = (props) => {
const [field, meta] = useField({...props, type:'radio'});
return (
<FormControl component="fieldset">
<FormLabel component="legend">{props.label}</FormLabel>
<RadioGroup aria-label={props.name} name={props.name} value={props.value}>
<FieldArray name="options">
{({ insert, remove, push }) => (
props.options.length > 0 && props.options.map((option,index) => (
<FormControlLabel key={index} {...props} value={option.toLowerCase()} control={<Radio />} label={option} />
))
)}
</FieldArray>
</RadioGroup>
</FormControl>
)
};
const CheckboxGroup = (props) => {
const [field, meta] = useField({...props, type: 'checkbox', });
return (
<FormControl component="fieldset">
<FormLabel component="legend">{props.label}</FormLabel>
<FormGroup>
<FieldArray name="options">
{({ insert, remove, push}) => (
props.options.length > 0 && props.options.map((option,index) => (
<FormControlLabel
{...field} {...props}
key={index}
control={<Checkbox />}
label={option.label}
/>
))
)}
</FieldArray>
</FormGroup>
<FormHelperText>Be careful</FormHelperText>
</FormControl>
)
}
I wrapped the whole <Form> in a function that passes props as an argument. I then get access to props.values.radio1. However, that has exposed that radio1 does not have a value even when it is clicked, which should be a separate issue.
{(props) => (
<Form>
<RadioInputGroup
label="Disable the checkbox?"
name="radio4"
options={['Yes','No']}
/>
<CheckboxGroup
disabled={props.values.radio1 === "No"}
name="checkbox1"
options={[
{name:"hello",label:"hello"},
{name:"there",label:"there"},
]}
/> </Form>
)}

Categories

Resources