Dynamic Form Builder

Build a form where users can dynamically add or remove input fields (like text boxes or dropdowns).

Solution

Please enter your details

Personal

Partner

Items to Include

Explanation

The HTML Structure

A form element with an onSubmit attribute set to handleSubmit.

Multiple label and input elements linked by the htmlFor and id attributes.

The functions

Several functions are used to handle all three types of state. There are standard state, partnerToggle and multiple item inputs.

Hold state in formState

There are many moving parts, rather than keeping each in a separate variable, they are all kept together with room to include the extra values that can be dynamically added.

Standard input functions

The handleInputChange function receives a string with the key of the field to update. This reduces the amount of functions needed to update state.

A functional state update allows the previous state to be added using the spread operator. The spread operator takes the state object and only updates [field]: event.target.value in the object.

toggleShowPartner function

This takes the previous state object and toggles the showPartner value.

AddItemHandler function

A new item is created where the id is created using the current date and for safety, the formState.items.length is added which will change between every newly added item.

The new item is added to the previous items array using a functional state update and the spread operator

deleteItemHandler function

The item id is passed from the element back to the function. The current state is retrieved, and the items is filtered using the filter method to return items that do not have that id.

handleSubmit function

When submitting a form, the usual action is to send the data and refresh the page. To stop this event.preventDefault is called. The data is logged to the console in this example.

formState showPartner

The showPartner state is used with a ternary operator to conditionally render the text, 'Remove' or 'Add', to the button.

The input for partner is conditionally rendered using short-circuit evaluation.

formState.items

The current items are rendered to the page using the map array method. Each one has an anonymousonChange function that takes all the previous items and adds the new content into the items array. setFormState then updates formState.

Code

import { ChangeEvent, useState } from "react";
import styles from "./Solution.module.css";

type ItemProps = {
  id: number;
  value: string;
};

type FormState = {
  showPartner: boolean;
  firstName: string;
  lastName: string;
  partnerFirstName: string;
  partnerLastName: string;
  items: ItemProps[];
};

const Solution = () => {
  const [formState, setFormState] = useState<FormState>({
    showPartner: false,
    firstName: "",
    lastName: "",
    partnerFirstName: "",
    partnerLastName: "",
    items: [],
  });

  const handleInputChange =
    (field: keyof FormState) => (event: ChangeEvent<HTMLInputElement>) => {
      setFormState((prev) => ({ ...prev, [field]: event.target.value }));
    };

  const toggleShowPartner = () => {
    setFormState((prev) => ({ ...prev, showPartner: !prev.showPartner }));
  };

  const addItemHandler = () => {
    const newItem = { id: Date.now() + formState.items.length, value: "" };
    setFormState((prev) => ({ ...prev, items: [...prev.items, newItem] }));
  };

  const deleteItemHandler = (itemId: number) => {
    setFormState((prev) => ({
      ...prev,
      items: prev.items.filter((item) => item.id !== itemId),
    }));
  };

  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    console.log(formState);
  };

  return (
    <>
      <h2>Please enter your details</h2>
      <form onSubmit={handleSubmit} className={styles.form}>
        <div className={styles.container}>
          <h3 className={styles.h3}>Personal</h3>
          <label htmlFor="firstName" className={styles.label}>
            First Name
            <input
              required
              className={styles.input}
              type="text"
              value={formState.firstName}
              onChange={handleInputChange("firstName")}
            />
          </label>
          <label htmlFor="lastName" className={styles.label}>
            Last Name
            <input
              required
              className={styles.input}
              type="text"
              value={formState.lastName}
              onChange={handleInputChange("lastName")}
            />
          </label>
          <h3 className={styles.h3}>Partner</h3>
          <button
            type="button"
            onClick={toggleShowPartner}
            className={styles.showButton}
          >
            {formState.showPartner ? "Remove" : "Add"} Partner
          </button>
          {formState.showPartner && (
            <>
              <label htmlFor="partnerFirstName" className={styles.label}>
                First Name
                <input
                  required
                  className={styles.input}
                  type="text"
                  value={formState.partnerFirstName}
                  onChange={handleInputChange("partnerFirstName")}
                />
              </label>
              <label htmlFor="partnerLastName" className={styles.label}>
                Last Name
                <input
                  required
                  className={styles.input}
                  type="text"
                  value={formState.partnerLastName}
                  onChange={handleInputChange("partnerLastName")}
                />
              </label>
            </>
          )}
          <h3 className={styles.h3}>Items to Include</h3>
          {formState.items.map((item, index) => (
            <div key={item.id} className={styles.item}>
              <input
                required
                type="text"
                className={styles.input}
                value={item.value}
                onChange={(e) => {
                  const newItems = [...formState.items];
                  newItems[index] = {
                    ...newItems[index],
                    value: e.target.value,
                  };
                  setFormState((prev) => ({ ...prev, items: newItems }));
                }}
              />
              <button
                type="button"
                className={styles.deleteButton}
                onClick={() => deleteItemHandler(item.id)}
              >
                Delete
              </button>
            </div>
          ))}
          <button
            type="button"
            onClick={addItemHandler}
            className={styles.addButton}
            aria-label="Add Item"
          >
            Add Item
          </button>
        </div>
        <button className={styles.submitButton} type="submit">
          Submit
        </button>
      </form>
    </>
  );
};

export default Solution;

Styling

.form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 10px 0;
}

.label {
  display: block;
}

.input {
  margin: 5px 0;
  display: block;
  padding: 5px;
}

.showButton {
  width: fit-content;
  padding: 5px;
  margin: 10px 0;
}

.addButton {
  width: fit-content;
  padding: 5px;
  margin: 10px 0;
}

.item {
  display: flex;
  gap: 5px;
}

.deleteButton {
  width: fit-content;
  padding: 5px;
  margin: 10px 0;
}

.container {
  border: 1px solid black;
  padding: 15px;
}

.h3 {
  margin: 30px 0 10px;
}

.submitButton {
  padding: 5px;
  margin-top: 10px;
}

Links