Creating Conditional Form Fields with React Hook Form and TypeScript

When you start creating forms in React, you’ll start to see many types of forms for many use cases. One particular use case that is more complex than most cases, is when your form fields depend on the data the user has entered. This is what is traditionally called conditional fields or dynamic forms.
We can create these forms as flexible as we need them to be, as it is never a good idea to show extremely long forms to users. We use a dynamic form to show as few form elements as possible to avoid overwhelming the user. This helps keep the user focused and connected with as few as possible form fields. It also allow us to reveal follow-up questions that depend on previous answers.
On this post we are going to take a look at react hook form conditional fields or react hook form dependent fields by creating a dynamic form that adapts to the responses the user gives us, and only ask relevant questions to our users.

Project Setup and Dependencies: react hook form and TypeScript

Before we start, you should be able to follow along if you have a react project with react hook form installed. In case you don’t – please go to this post, create a react hook form, then come back. For those that have their own react hook form form ready, you can follow along.

If you need to install react hook form do so by running this command in the project root folder:
npm install react-hook-form --save

Before moving any further with the code, we need to think how this conditional logic will work for our form. For example: if <field_A> is “YES” then <field_B> is displayed, then is <field_B> is “NO” then <field_C> is hidden and <field_D> is displayed. This is the conditional logic we have to follow to make our conditional form fields work – for example.

As stated above: the idea is to simplify the form and only show form fields relevant to a previous response.
After we have an idea of how this logic should work, then is time to code it up. We are going to use a form logic for a survey form for a software engineer. The questions will be:
  1. Do you have experience with JavaScript?
  2. Years of experience with JavaScript?
  3. Do you know React?
  4. Do you know React Hook Form?
  5. What other language do you normally use?
  6. What is your name?
  7. What is your phone?
For our form logic, our form elements will show or hide in the following way:
  • Initially only question [1] will be visible.
    • Then if question [1] is “YES”:
      • Show question [2], and If question [2] is greater than 2 years.  
      • Show question [3], and if question [3] is “YES” show question [4]
    • Otherwise If question [1] is “NO”:
      • Show question [5]
      • After question [1] and [2] or [3] or [4] or [5] is answered then we show question [6]
      • After question [6] we then show question [7]

As you can see this simple form can get quite complex quickly. We have to abstract this logic as best as we can if we want to easily maintain complex forms logic or to reuse this type of form. On top of that, there is another layer: validation rules that work with the above conditional logic, so form errors are consistent with this logic. We will not be covering validating these field on this post, to keep this post focused on conditional logic. I am going to add validation on a separate post.

Implementing Conditional Form Fields

As mentioned above we have to do our best to abstract the conditional logic for our form, so we can easily maintain our react hook form in case anything changes in the future – like adding a new question in the middle or changing when a question appears.
To accomplish this, we are going to treat our form as data and “generate” our form depending on a json structure. Since we are using TypeScript, let’s start by creating the relevant type that will describe our form data, or structure of the form itself.

Let’s create a new folder inside our src folder, and name it “types”. Inside our project src folder:

mkdir types && cd types && touch index.ts && cd ..
export type FormInput = {
    type: 'text' | 'radio' | 'select' | 'checkbox' | 'textarea' | 'submit';
    fieldType?: 'text' | 'number'
    name: string
    value: string | number
    placeholder?: string
    label?: string
    options?: SelectOpt[],
    depConditionsTrueIf?: 'all' |  'any'
    conditionalLogic?: ConditionalLogic[]
}

export type SelectOpt = {
    text: string
    val: string | number
}

export type ConditionalLogic = {
    depFieldName: string
    depFieldValue: string | number
    depConditionsTrueIf?: 'any' | 'all',
    depFieldValueCondition: '>' | '<' | '>=' | '<=' | '=' | '!=' | 'NotEmpty'
    andGroup?: ConditionalLogic[]
}
Here we are defining the types of input form controls we have: a Text, a Radio button, Select, checkbox or textarea fields. Next the name of the field, a default value, a placeholder property, options (for a select or radio fields) and lastly the two main properties that will make the conditional fields possible:

depConditionsTrueIf: this will help us consider a condition true only if all conditions are true or any condition is true. This is like a programatic AND or OR condition.

conditionalLogic: this is the core of our conditional fields. This is an array of conditions that need to be satisfied in order for this field to be displayed. In case of depConditionsTrueIf is set to all then all conditions need to be true to show this form field, otherwise setting it to any will show this field if any condition is true.

This is a very flexible structure to help us make our form field adapt to any type of conditional logic we need.
Now all we have left is implementing the code that will “generate” our form following our form json “schema” and also write the form schema itself that will enable us to construct this form, and follow our conditional logic.
The question now is: do we start coding the chicken or the egg? let’s start with the data (chicken) as we need to have this defined before we can test our form generator code (our egg).

Writing our Input Fields Form Schema

We have an fun project going on and we want to ensure maximum reusability. The beauty of this structure is that we can generate as many forms as we’d like and only need to maintain our form json structure or form definition.

Let’s first create a folder for our form inside our src folder, and create a file for it:

mkdir forms && cd forms && touch forms.ts && cd ..
Now let’s import our FormInput type and start creating our form definition:
import type { FormInput } from "../types";

export const surveyForm: FormInput[] = [
    {
        type: 'radio',
        options: [{ text: 'Yes', val: 'yes'}, { text: 'No', val: 'no'} ],
        name: 'has-JS-Experience',
        value: '',
        label: 'Do you have experience with JavaScript?',
    },
    {
        type: 'text',
        fieldType: 'number',
        name: 'js-years-experience',
        value: '',
        label: 'Years of experience with JavaScript?',
        conditionalLogic: [
            {
                depFieldName: 'has-JS-Experience',
                depFieldValue: 'yes',
                depFieldValueCondition: '='
            },
        ]
    },
    {
        type: 'radio',
        options: [{ text: 'Yes', val: 'yes'}, { text: 'No', val: 'no'} ],
        name: 'know-react',
        value: '',
        label: 'Do you know React?',
        conditionalLogic: [
            {
                depFieldName: 'js-years-experience',
                depFieldValue: 2,
                depFieldValueCondition: '>'
            },
        ]
    },
    {
        type: 'radio',
        options: [{ text: 'Yes', val: 'yes'}, { text: 'No', val: 'no'} ],
        name: 'know-react-hook-form',
        value: '',
        label: 'Do you know React Hook Form?',
        conditionalLogic: [
            {
                depFieldName: 'know-react',
                depFieldValue: 'yes',
                depFieldValueCondition: '='
            },
        ]
    },
    {
        type: 'text',
        name: 'what-other-language',
        value: '',
        placeholder: 'Node.js, Go, Python',
        label: 'What other language do you know?',
        conditionalLogic: [
            {
                depFieldName: 'has-JS-Experience',
                depFieldValue: 'no',
                depFieldValueCondition: '='
            },
        ]
    },
    {
        type: 'text',
        name: 'what-is-your-name',
        value: '',
        label: 'What is your name?',
        depConditionsTrueIf: 'any',
        conditionalLogic: [
            {
                depFieldName: 'has-JS-Experience',
                depFieldValue: 'yes',
                depFieldValueCondition: '=',
                andGroup: [
                    {
                        depFieldName: 'js-years-experience',
                        depFieldValue: '',
                        depFieldValueCondition: 'NotEmpty'
                    },
                    {
                        depFieldName: 'know-react',
                        depFieldValue: '',
                        depFieldValueCondition: 'NotEmpty'
                    },
                    {
                        depFieldName: 'know-react-hook-form',
                        depFieldValue: '',
                        depFieldValueCondition: 'NotEmpty'
                    },

                ]
            },
            {
                depFieldName: 'has-JS-Experience',
                depFieldValue: 'no',
                depFieldValueCondition: '=',
                andGroup: [
                    {
                        depFieldName: 'what-other-language',
                        depFieldValue: '',
                        depFieldValueCondition: 'NotEmpty'                        
                    }
                ]                
            }
        ]
    },
    {
        type: 'text',
        name: 'what-is-your-phone',
        value: '',
        label: 'What is Your Phone?',
        conditionalLogic: [
            {
                depFieldName: 'what-is-your-name',
                depFieldValue: '',
                depFieldValueCondition: 'NotEmpty'
            },
        ]
    },
    {
        type: 'submit',
        name: 'submit-dev-survey',
        value: '',
        label: 'Submit Survey',
        depConditionsTrueIf: 'any',
        conditionalLogic: [
            {
                depFieldName: 'know-react',
                depFieldValue: 'no',
                depFieldValueCondition: '='
            },
            {
                depFieldName: 'what-is-your-phone',
                depFieldValue: '',
                depFieldValueCondition: 'NotEmpty'                
            }
        ]
    },
]

Perfect! what a long file you may be saying, but we have created our entire form and defined our conditional logic. As you may have noticed we have a type that maps to the type of form field we want, next we have a name that goes into the name property of the form field and we have a label that is the descriptive text and a value that is the initial value for each field. These are the simplest properties to understand.

Next we have a conditionalLogic that is an array of objects that follows our ConditionalLogic type we created in our types/index.ts TypeScript file. It works as follows:

  • depFieldName: the name of the current field depends on
  • depFieldValue: the field value that we are looking for this condition to be valid.
  • depFieldValueCondition: the condition we are using to compare the other field’s value
  • andGroup: an optional property to chain additional conditions that should be true in order to consider this conditional logic valid (so our form field can be shown)
We are defining the following form structure:

Question 1: (Have JS experience?) Show all the time (notice there is no conditionalLogic property)
Question 2: (JS Years of Experience?) Show only if [Question 1] (with field name: has-JS-experience) is equal to “yes”
Question 3: (Know React?) Show only if [Question 2] (JS-years-experience) is greater than 2
Question 4: (Know react hook form?) Show only if [Question 3] (know-react) is equal to “yes”
Question 5: (What other language?) Show only if [Question 1] is equal to “no”
Question 6: (What is your name?) Show only if any of the following are true:
   – [Question 1] (has-JS-Experience) is equal to “yes” AND:
         – [Question 2] (JS-years-experience) is not equal to empty string
         – [Question 3] (know-react) is not equal to empty string
         – [Question 4] (know-react-hook-form) is not equal to empty string
    – [Question 1] (has-JS-Experience) is equal to “no” AND:
         – [Question 5] is not empty.
Question 7: (What is your Phone?) Show only if [Question 6] (what-is-your-name) is not equal to empty string.

That question 6 is quite complex. Imagine having to maintain this logic in code when a new change comes? this conditional logic could easily become time consuming to update. But fortunately you are preparing future proof code. And since we are handling this logic as data, we can even create fields and save them to our database! but that is for another post!
Now let’s write the code that will parse our form schema, build our form and run our conditional logic.

Building a Form Generator with Conditional Fields and React Hook Form

If you are still here, congratulations – you are seriously a person that values flexibility and making your code easier to maintain.
Now let’s write our form schema parser, and make our form use react hook form behind the scenes for us.
import type { ConditionalLogic } from "../types";

export const passesConditionalLogic = (rootFieldName: string, conditionalLogic: ConditionalLogic[] | undefined, logicTrueIf: 'all' | 'any' | undefined, watchList: Record<string, any>): boolean => {
    if (!conditionalLogic) {
        return true;
    }

    let andGroupMet = true;
    let res = true;
    let runningCond = true;
    const conditionType = logicTrueIf === 'any' ? 'any' : 'all';
    for (let i = 0; i < conditionalLogic.length; i += 1) {
        const condLogic = conditionalLogic[i];

        const fieldName = condLogic.depFieldName;
        const condOp = condLogic.depFieldValueCondition;
        const fieldValue = watchList[fieldName];
        const expectedValue = condLogic.depFieldValue;
        const andGroup = condLogic?.andGroup;

        // reset on each new condition, as we only need any condition to be true!
        if (conditionType === 'any') {
            andGroupMet = true;
        }

        res = evalConditionalLogic(fieldValue, condOp, expectedValue);

        if (andGroup) {
            // only set runningCond initially when there is a andGroup
            if (i === 0) {
                runningCond = false;
            }
            for (let andGroupLogic of andGroup) {
                const andFieldName = andGroupLogic.depFieldName;
                const andCondOp = andGroupLogic.depFieldValueCondition;
                const andFieldValue = watchList[andFieldName];
                const andExpectedValue = andGroupLogic.depFieldValue;
                andGroupMet = andGroupMet && evalConditionalLogic(andFieldValue, andCondOp, andExpectedValue);
                if (!andGroupMet) {
                    break;
                }
            }
        }

        if (conditionType === 'all') {
            runningCond = res && runningCond;
        } else if (conditionType === 'any') {
            if (andGroup) {
                runningCond = runningCond || (andGroupMet && res);
            } else {
                runningCond = res;
            }
            // if true, we found a condition that shows this field, no need to keep checking
            if (runningCond) {
                break;
            }
        }
    }
    return runningCond;
}

const evalConditionalLogic = (value: string, condOp: string, expectedValue: string | number): boolean => {
    let res;
    switch (condOp) {
        case 'NotEmpty':
            res = value?.length > 0;
            break;
        case '!=':
            res = value !== expectedValue;
            break;
        case '<':
            res = value < expectedValue;
            break;
        case '<=':
            res = value <= expectedValue;
            break;
        case '=':
            res = value === expectedValue;
            break;
        case '>':
            res = value > expectedValue;
            break;
        default:
            res = false;
    }
    return res;
}
There is a lot on this file, so we’ll start explaining the global logic of this function:

Lines 12 to 58 iterate over all the conditions inside our conditionalLogic array. We extract the configuration parameters of this condition and check if we have a conditionType set to “any” – if so we reset our accumulated boolean evaluator called andGroupMet (after processing 1 or more conditions) so we can continue to chain the previous value with new values.

For example if we have: true && false = false -> because both need to be true. So we set it to true on every iteration as we don’t care what a previous value was in an “any” condition – which should be valid when the first condition is met. In the case of “all” we don’t do that, and keep the previous boolean value – so we can make the condition false if a previous evaluation was false even if the new condition is true (true && false === false)
Then we have an inner for loop iterating on the andGroup array, which contains conditions that should be true for this condition to be true. In the case of “any” this boolean is reset to true on every iteration, or if the condition is “all” then all the conditions must be true for the field to be shown.

– Lines 45 to 58 we have a condition checking if we have an “all” or “any” condition and merges the previous boolean result depending on each case: if “all” then we merge the current result with the previous result. If “any” and we have an “andGroup” on that field: we merge the running boolean OR the evaluation of the andGroupMet with the result variable of the parent field evaluation. For conditional fields with no andGroup we only store the result in our running condition (runningCond variable on line 51)

– Lines 62 to 87 we simply process each comparison according to the conditional operation we have as a string value in our json form definition. The evaluation is done and then returned to the caller of the function. Simple. Having this on a separate function helps us extend it with additional comparison operators.

If you still don’t understand the conditional logic parser, you can run it with your own values (form structure), and compare the result adding as many console.log() as you need to see it in action.
If you see any potential optimizations or any bugs, let me know!

Implementing a Form Generator Component with Dynamic Fields

We already covered the most complex part of the project – the conditional logic parser. We did it! 🙂

The final part is reading from our json form definition: what fields we need and creating each according to its type.

For that we are going to create our dependencies or component that will generate the the form input control. In other words, each input field will be an independent React component.

This dynamic input control component will be wrapped around another form generator component that will have a callback (a function that gets called by a child submit submit button component when clicked)

First let’s create a new file called (while still inside our ./forms folder): DynamicInputControl.tsx 

touch DynamicInputControl.tsx
Next let’s define our imports:
import {
  Control,
  FieldErrors,
  UseFormRegister,
  UseFormUnregister,
  useWatch,
} from "react-hook-form";
import { ConditionalLogic, SelectOpt } from "../types";
import { useEffect } from "react";
import { passesConditionalLogic } from "./utils";

Great, now let’s define the types for our component props. 

type DynamicInputControlProps = {
  name:string;
  label:string|undefined;
  type:string;
  placeholder:string|undefined;
  fieldType:string|undefined;
  options:SelectOpt[] |undefined;
  conditionalLogic:ConditionalLogic[] |undefined;
  register:UseFormRegister<any>;
  unregister:UseFormUnregister<any>;
  logicTrueIf?:"all"|"any";
  errors:FieldErrors;
  control:Control;
  index:number;
};
We have a prop for almost every property in our form definition on our json form structure. We have a few extra ones like “register”, “unregister”, “control”, “errors”, “index”, these props are to allow our components to interact with the react hook form API.
Now let’s create the actual functional component which will help us generate our form fields:
import {
  Control,
  FieldErrors,
  UseFormRegister,
  UseFormUnregister,
  useWatch,
} from "react-hook-form";
import { ConditionalLogic, SelectOpt } from "../types";
import { useEffect } from "react";
import { passesConditionalLogic } from "./utils";

type DynamicInputControlProps = {
  name: string;
  label: string | undefined;
  type: string;
  placeholder: string | undefined;
  fieldType: string | undefined;
  options: SelectOpt[] | undefined;
  conditionalLogic: ConditionalLogic[] | undefined;
  register: UseFormRegister<any>;
  unregister: UseFormUnregister<any>;
  logicTrueIf?: "all" | "any";
  errors: FieldErrors;
  control: Control;
  index: number;
};

const DynamicInputControl = ({
  name,
  label,
  type,
  fieldType,
  placeholder,
  options,
  conditionalLogic,
  register,
  unregister,
  logicTrueIf,
  errors,
  control,
}: DynamicInputControlProps) => {
  const error = (errors[name]?.message as string) || null;
  const watchList = useWatch({
    control,
  });

  const passes = passesConditionalLogic(
    name,
    conditionalLogic,
    logicTrueIf,
    watchList
  );

  useEffect(() => {
    if (!passes) {
      unregister(name);
    }
  }, [passes, unregister, name]);

  if (!passes) {
    return null;
  }

  switch (type) {
    case "text":
      return (
        <div key={name} className={`field ${error ? "has-error" : ""}`}>
          <label htmlFor={name}>{label}</label>
          <input
            placeholder={placeholder || ""}
            id={name}
            {...register(name)}
            type={fieldType ? fieldType : "text"}
          />
          <span>{error}</span>
        </div>
      );
    case "radio":
      return (
        <div key={name} className={`field ${error ? "has-error" : ""}`}>
          <label>{label}</label>
          {options?.map(({ text, val }) => (
            <span key={`${text}-${val}`}>
              {text}
              <input {...register(name)} type="radio" value={val} />
              <span>{error}</span>
            </span>
          ))}
        </div>
      );
    case "select":
      return (
        <div key={name} className={`field ${error ? "has-error" : ""}`}>
          <label htmlFor={name}>{label}</label>
          <select name={name}>
            {options?.map(({ text, val }) => (
              <option key={val}>{text}</option>
            ))}
          </select>
          <span>{error}</span>
        </div>
      );
    case "submit":
      return (
        <div key={name} className={`field`}>
          <input type="submit" value={label} />
        </div>
      );
  }

  return null;
};

export default DynamicInputControl;
It’s a lot of code but the most important part is on line 43, where we are listening to the changes of all other input controls in our form. When we say “listen” we mean that every time our user types something in any input control, all others will be “notified” with the changes I made to the form.

We pass the control property inside as saying: “we want to listen to the notifications that belong to the main form we are creating” – this means that all our form input controls are wired together with this control property. This is part of react hook form’s API and we are making sure we are following closely according to the react hook form docs. 

Some might say: “but why listen to all other form fields?, and why not instead listen to the fields each form field depends on?” – good question – the answer is: in the case our user fills several of our questions and decides to go back to our first question – for example – (Do you have experience with JS?) and changes the answer from “no” to “yes” or viceversa, would cause some conditions not to re-render (or recheck or conditional logic when it is needed)

The next interesting line is line 47: we imported our function passesConditionalLogic which calls our field conditional logic evaluator that takes the name of the field, the conditional logic array (defined in our form json definition), what type of evaluation it is (all or any) and finally the values of all our fields that comes from the call to useWatch – the react hook that monitors the values of our form made with react hook form.

This function will give us if the conditional logic passed or not. In the case that our form field’s conditional logic fails (all the relevant conditions are not met) then we simply render null on line 61 (null is “nothing” in React) and we also unregister the form field on line 56 (meaning removing the data and field registration inside react hook form.

So what does this do for us when we unregister? we remove the field and all data with it, so when we show the field again (in case our user enters the right data to show this field again) we start with new state instead of any previous state that would not follow our required conditional logic.

Lines 64 to 109 define each type of form field, and return each depending on the type of form field we want to show our user. This is simply handled by a switch statement checking the type, and then adding the name property, a placeholder (in case we have one). Each is wrapped inside an outer div and calls the register function to tell reach hook form that we are associating this form field to our form.

Other fields like the radio buttons have an array of options show a text label for each radio button and a question text above.
With this we have created our react dynamic input fields, and this is in general what is a dynamic form that changes depending on the user responses.

Creating a Form Generator Wrapper Component

To finish our project we have to make our form generator component that will import our DynamicInputControl component.

Let’s show the code and then explain what it does:

import { useMemo } from "react";
import type { FormInput } from "../types";
import { useForm, Resolver, SubmitHandler } from "react-hook-form";
import DynamicInputControl from "./DynamicInputControl";

type FormGeneratorProps = {
  schema: FormInput[];
  resolver?: Resolver;
};

const FormGenerator = ({ schema, resolver }: FormGeneratorProps) => {
  type DynamicForm = {
    [K in (typeof schema)[number]["name"]]: (typeof schema)[number]["name"];
  };

  const defaultValues = useMemo(() => {
    return schema.reduce((acc: Record<string, string | number>, i) => {
      acc[i.name] =
        typeof i.value === "boolean" ? (i.value ? "true" : "false") : i.value;
      return acc;
    }, {});
  }, [schema]);

  const {
    register,
    unregister,
    handleSubmit,
    control,
    formState: { errors },
  } = useForm({
    resolver,
    defaultValues,
  });

  const onFormSubmit: SubmitHandler<DynamicForm> = (
    data: DynamicForm,
    e?: React.BaseSyntheticEvent
  ) => {
    e?.preventDefault();
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onFormSubmit)}>
      {schema.map((field, i) => (
        <DynamicInputControl
          key={field.name + "-" + i}
          name={field.name}
          label={field.label}
          type={field.type}
          placeholder={field.placeholder}
          fieldType={field.fieldType}
          options={field?.options}
          register={register}
          unregister={unregister}
          logicTrueIf={field?.depConditionsTrueIf}
          conditionalLogic={field?.conditionalLogic}
          control={control}
          errors={errors}
          index={i}
        />
      ))}
    </form>
  );
};

export default FormGenerator;
Compared to our DynamicInputControl component this is a much shorter file that focuses mainly in receiving the schema (or form definition json) and processing each field by calling our DynamicInputControl component.
At the top we import our TypeScript type and the relevant react hook form hooks to initialize our form (useForm, Resolver, SubmitHandler)
Inside our form generator component we define the types of form fields of type DynamicForm. This uses the schema type to define a dynamic TypeScript type that takes all our “name” fields adds them as key-value pairs: string: string | number – using the field names as keys.

Next we capture all our default values inside a useMemo which creates a cached processed copy merging all the fields from an array into an object and isolating boolean values to be “true” or “false” strings instead of actual true or false. This useMemo will only be called again when our dependency changes (line 22: schema)

On lines 24 to 33 we call the useForm hook from react hook form. This is what we use to initialize our react hook form and pass all the default values we captured above. This is where we extract our control variable to connect all our form input controls to this form. Same for the register and unregister functions that help us register or unregister form fields dynamically as our user types on our form.

On line 35 we defined our form submit handler that will capture all submit button clicks and the data the user typed on our form. We are using TypeScript generics here to tell TypeScript the type of data we are using in our form <DynamicForm>. We are also preventing our page from reloading when this button is clicked (e?.preventDefault()) and simply displaying our data in the browser’s console.

Lastly on line 43 we return by rendering our form with all the DynamicInputControl components (an array of components) that represents our form fields, and passing all the properties from our form definition json.

So you might be saying: “where’s the validation” – and in order to keep this blog post from being too long, I will continue on another future post on how to add validation rules to our form definition and implement these dynamically the same way our form was created.

Conclusion

In this post we took a deep dive into the creating a dynamic input form with conditional rendering. We used the react hook form’s useWatch function to monitor what our user typed and implemented a form component using the amazing react hook form library.

We first defined our form schema or definition of the fields of our form. We then created a dynamic input control that creates each type of field, subscribed each field using the useWatch hook, and monitored if the conditional logic is valid or not. If it is not valid, we remove the field (unregister) and finally wrapped our dynamic form field inside a FormGenerator component that receives the on submit event (when our user clicks the submit button).

With this we created a dynamic react form we can use time and time again without having to maintain conditional logic by hand directly in code. As we saw on this post, complex forms can easily be challenging to create but if we go beyond and aim to reuse, we can save ourselves many hours of work as requirements change. Sure, there is a tradeoff – having to code this type of form takes more time, but with the help of this post you won’t have to spend too much time.

If this post was useful or you have any suggestions or found a bug, post it in the comments below. See you soon!

About The Author

Leave a Comment

Your email address will not be published. Required fields are marked *