React JS Render Form With Dynamic Conditional Fields

Show or hide based on the parent field value

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

When you purchase through links on techighness.com, I may earn an affiliate commission.