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
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;
}