In the last post I showed how can we plot a form whose data is all dynamic, coming from a backend API. Continuing from there, say we have again a totally dynamic set of fields coming from a backend API. It has some fields that are conditionally linked to another field. We can call them parent and child fields. When the value of the parent changes, one or more children’s fields show or hide themselves based on a conditional criteria assigned to them. All of this is totally dynamic and cannot be known in advance.
Moreover, the information about what type of field will it be also comes in the API response. A field could be of any type, such as select, text, number, email, phone, radio, checkbox, file, etc.
For this kind of dynamic form, it is important that the data from the backend is well formatted. I could think of two ways the data will be well-suited for dynamic conditional fields:
1. Children’s Fields Are Nested in the Parent
const dynamicFields = [
{
id: 1,
value: "",
fieldName: "Are you a software engineer?"
children: [
{
id: 2,
value: "",
fieldName: "How many years of experience do you have in software engineering?"
showWhenParentIs: "Yes"
},
{
id: 3,
value: "",
fieldName: "What is your profession?"
showWhenParentIs: "No"
}
]
}
]
2. Both Parent and Children Are in a Flat Structure
const dynamicFields = [
{
id: 1,
value: "",
fieldName: "Are you a software engineer?"
},
{
id: 2,
parentId: 1,
value: "",
fieldName: "How many years of experience do you have in software engineering?"
showWhenParentIs: "Yes"
},
{
id: 3,
parentId: 1,
value: "",
fieldName: "What is your profession?"
showWhenParentIs: "No"
}
]
In either case, we need to process the data and add only those child fields for which the parent is fulfilling the criteria.
But I’d prefer the first way as the record of the child remains with the parent field. This helps in keeping the single source of truth for all the children’s data and even persisting their changed values when the parent value changes.
I’m using React Hook Form for dynamic rendering of the form. The demo and the example code are given below:
Demo
Example Code
import React, { useState } from "react";
import { useForm, useFieldArray } from "react-hook-form";
export default function ConditionalForm() {
const {
control,
register,
handleSubmit,
watch,
formState: { errors }
} = useForm();
const [loading, setLoading] = useState(false);
const { fields, append, remove } = useFieldArray({
control,
name: "conditionalForm"
});
const value = watch("conditionalForm");
// dummy data
const conditionalFormFromBackend = [
{
id: 1,
value: "",
fieldName: "Are you a software engineer?",
fieldType: "select",
choices: [{ id: 1, value: "Yes" }, { id: 2, value: "No" }],
required: true,
children: [
{
id: 2,
value: "",
fieldName:
"How many years of experience do you have in software engineering?",
required: true,
choices: [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
{ id: 6, value: 6 },
{ id: 7, value: 7 },
{ id: 8, value: 8 },
{ id: 9, value: 9 },
{ id: 10, value: 10 },
{ id: 11, value: "10+" }
],
fieldType: "select",
showWhenParentIs: "Yes",
},
{
id: 3,
value: "",
fieldType: "text",
required: true,
fieldName: "What is your profession?",
showWhenParentIs: "No",
}
]
}
];
const onSelectChange = (index) => {
value[index]?.children?.forEach((c, childIndex) => {
const childIndexInForm =
value.findIndex(field => field.id === c.id);
// if the child is supposed to be shown
// and doesn't already exist in form/fields
if (c.showWhenParentIs === value[index].value
&& childIndexInForm === -1) {
append(c);
}
else if (c.showWhenParentIs !== value[index].value
&& childIndexInForm > -1) { // when child should not be shown but it exists
// replace the child object in the parent children array
// before removing from the form, so that value persists
// for the time when the child appears again
value[index].children[childIndex] =
value[childIndexInForm];
remove(childIndexInForm);
}
});
};
// simulate backend API call
const loadConditionalForm = () => {
setLoading(true);
setTimeout(() => {
setLoading(false);
conditionalFormFromBackend.forEach((q) => {
append(q, { shouldFocus: false });
});
}, 2000);
}
const onSubmit = (data) => {
console.log(data);
// submit dynamic conditionalForm!
};
return (
<div
style={{
borderStyle: "ridge",
padding: "10px",
minHeight: "300px"
}}>
{
(!loading && !fields.length)
&&
<button
type="button"
onClick={loadConditionalForm}>
Load Conditional Form
</button>
}
{loading ? (
"loading conditional form..."
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
{fields?.map((field, index) => {
const fieldData = value[index]
return (
<div key={fieldData.id}
style={{ marginBottom: "10px" }}>
{fieldData.fieldName} <br></br>
{
fieldData.fieldType === "select" ?
<div>
<select
{...register(`conditionalForm.${index}.value`, {
required: fieldData.required,
onChange: () => onSelectChange(index)
})}>
<option value="">Select</option>
{
fieldData?.choices?.map(c =>
<option
key={c.id}
value={c.value}>
{c.value}
</option>)
}
</select>
</div>
: null
}
{
fieldData.fieldType === "text" ?
<div>
<input
{...register(`conditionalForm.${index}.value`, {
required: fieldData.required
})}
/>
</div>
: null
}
{errors?.conditionalForm?.[index] && (
<span style={{ color: "red", fontSize: "small" }}>
Required
</span>
)}
</div>
)
}
)}
</div>
<br></br>
{!loading && fields.length > 0 && <input type="submit" />}
</form>
)}
</div>
)
}
In this example, I’m using fieldType
"select"
and "text"
to render select dropdown and input fields respectively. It could be any other field type, and that can be handled with an extra check.
Notice in the demo that if you select “Yes” for the parent, select some value of the child, select “No” for the parent, add some value in the text field, and then select back to “Yes” for the parent, the child value will be the one you selected earlier and not blank.
For fields that are deeply nested and depend conditionally on their parents at each level will require a recursive solution to travese the whole tree and show/hide (add/remove) the fields as we go.
See also
- SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
- Yup Date Format Validation With Moment JS
- Yup Number Validation: Allow Empty String
- Exactly Same Query Behaving Differently in Mongo Client and Mongoose
- JavaScript Unit Testing JSON Schema Validation
- Reduce JS Size With Constant Strings
- JavaScript SDK