DEV Community

loading...
Cover image for Creating a React Dynamically Controlled Form

Creating a React Dynamically Controlled Form

Kailana Kahawaii
Fullstack Javascript | React | Ruby | Rails
Updated on ・4 min read

A dynamically controlled (DCF) form is a form that allows users to add—and take away—input fields at the click of a button. It is an alternative to single input fields and is intended to encapsulate procedural or grouped content.

In this tutorial, we’ll create a simple recipe form using a DCF to separate ingredients and steps.

I’ll be covering the form creation in this tutorial using React.

Specifically, we'll:

  • Construct a form using state
  • Write handleChange functions for recipe attributes
  • Write add and delete functionality for ingredients and steps
  • Write render input functions for ingredients and stepss
  • Write a handleSubmit for the entire form
  • Render the form

Recipe Construction in State

In a new React app, create a component for the form.

touch AddRecipeForm.js

The constructor will hold our form data in state. We want our recipe to have a title, summary, ingredients (name and amount) and steps.

 constructor(){
        super()
        this.state={
            title:"",
        summary: "",
        ingredients: [
            {name: "", amount: ""}
        ],
        steps: []
        }
    }

As you can see, we’re holding title and summary information as well. This information won’t need to be held in an array, so we can use one handleChange function for both of them.

Handle Change for Title and Summary

Write a single handleChange function to allow a user to write the title and summary for a recipe.


    handleChange = (event) => {
        this.setState({
            [event.target.name]: event.target.value
        })
    }

Handle Change for Step and Ingredients

We’ll then need to handle ingredient name, amount and step changes separately.

For ingredient state changes, we’ll need to map through the ingredient names and amounts in different functions.

 handleIngredientNameChange = (e, ingredientIndex) => {
        let newIngredientName = e.target.value;
        this.setState((prev) => {
          return {
            ...prev,
            ingredients: prev.ingredients.map((ingredient, index) => {
              if (index == ingredientIndex) {
                return { ...ingredient, name: newIngredientName};
              } 
              return ingredient;
            }),
          };
        });
      };

      handleIngredientAmountChange = (e, ingredientIndex) => {
        let newIngredientAmount = e.target.value;
        this.setState((prev) => {
          return {
            ...prev,
            ingredients: prev.ingredients.map((ingredient, index) => {
              if (index == ingredientIndex) {
                return { ...ingredient, amount: newIngredientAmount};
              } 
              return ingredient;
            }),
          };
        });
      };

For Step changes, we only need to map through the step.

  handleStepChange = (e, stepIndex) => {
        let newStep = e.target.value;
        this.setState((prev) => {
          return {
            ...prev,
            steps: prev.steps.map((step, index) => {
              if (index == stepIndex) {
                return { ...step, step_summary: newStep};
              } 
              return step;
            }),
          };
        });
      };

Add and Remove Ingredients

We want to give our user the option to add and remove ingredients. We use filter to take care of removing the ingredient.

addIngredientInputs = () => {
        this.setState((prev) => {
            return {
              ...prev,
              ingredients: [...prev.ingredients, { name: "", amount:"" }],
            };
          });
    }
    removeIngredientInput = (e, ingredientIndex) => {
      e.preventDefault()

      this.setState({
        ingredients: this.state.ingredients.filter((ingredient, removedIngredient) => removedIngredient !== ingredientIndex )
      })
    }

Render Ingredients

Finally, we’ll need to render the ingredient inputs. I've used a bit of Bootstrap styling here.

renderIngredientInputs = () => {
             return this.state.ingredients.map((ingredient, index) => {
          return (

                <div key={`name ${index}`} 
                className="form-group">

                <input className="mb-3"
                    value={this.state.ingredients[index].name}
                    onChange={(e) => this.handleIngredientNameChange(e, index)}
                    placeholder="Name"
                    name="name"

                />

                <input
                    value={this.state.ingredients[index].amount}
                    onChange={(e) => this.handleIngredientAmountChange(e, index)}
                    placeholder="Amount"
                    name="amount"

                />
                <br></br>

                <Button variant="outline-secondary" onClick={(e)=>this.removeIngredientInput(e,index)}>{this.state.ingredients[index].name ? `Delete ${this.state.ingredients[index].name}` : `Delete Ingredient`}</Button>

            </div>
          );
        });
      };

Here, we assign each rendered ingredient with an index. We place the onChange events in the render and also add a button to remove the ingredients if needed.

Add and Remove Steps

Adding and removing steps are a bit simpler, but follow the same logic.



    addStepInputs = () => {
        this.setState((prev) => {
          return {
            ...prev,
            steps: [...prev.steps, ""],
          };
        });
      };

removeStepInput = (e, stepIndex) => {
        e.preventDefault()

        this.setState({
          steps: this.state.steps.filter((step, removedStep) => removedStep !== stepIndex )
        })
      }

As with the ingredients, give the user the option to add or remove steps. Then, render the step inputs.

Render Step Inputs

Again, I've used a bit of Bootstrap for styling. The important thing to take into account here is that each step is numbered. When a step is added, we add one to the count. Step${index+1} When we delete a step, we change the count based on where that step was deleted. We need to use +1 since the indexes start at 0.

renderStepInputs = () => {
               }
        return this.state.steps.map((step, index) => {
          return (
            <div key={index} className="form-group">
          <fieldset>
              <textarea
                placeholder={`Step${index+1}`}

                name="rec_steps"
                id="textArea"
                className="form-control"
                onChange={(e) => this.handleStepChange(e, index)}
                value={step.step_summary}
              />
              <button className="btn btn-secondary" type="button" onClick={(e)=>this.removeStepInput(e,index)}>{`Delete Step ${index+1}`}</button>
              </fieldset>
            </div>
          );
        });
      };
      handleStepChange = (e, stepIndex) => {
        let newStep = e.target.value;
        this.setState((prev) => {
          return {
            ...prev,
            steps: prev.steps.map((step, index) => {
              if (index == stepIndex) {
                return { ...step, step_summary: newStep};
              } 
              return step;
            }),
          };
        });
      };


Write handleSubmit

Finally, write a handleSubmit function to send data to the backend and bring the user back to the ingredients page.

handleSumbit = (e) => {
        e.preventDefault()
            this.props.onAddRecipe(this.state)
            this.props.history.push('/')

    }

Putting it all together in the render function

In the render function, write a form.

<h1>Add a new recipe!</h1>
        <form onSubmit={this.handleSumbit} >
<fieldset>
            <div class="form-group">
              <label for="inputDefault">Title</label>
              <input 
                type="inputDefault" 
                name="title"
                class="form-control" 
                id="inputDefault"
                placeholder="Enter title"
                onChange={this.handleChange}
                ></input>
            </div>
<div className="form-group">
                <label forHtml="textArea">Summary </label>
                <textarea 
                  className="form-control"
                  id="textArea"
                  rows="3"
                  name="summary"
                  onChange={this.handleChange} 
                  placeholder="80 characters max"></textarea>
            </div>


There are many things going on here, but many of them of stylistic. The onChange events handle our title and summary changes.

Below, we’ve added ingredient and step input fields.

 <div class="form-group">
              <label>Ingredients</label>
            {this.renderIngredientInputs()}
            <button type="button" className="btn btn-primary" onClick={()=> this.addIngredientInputs()}>+ Add Ingredient</button>
            </div>
            <div class="form-group">
              <label forHtml="textArea">Steps</label>
              {this.renderStepInputs()}
              <button type="button" className="btn btn-primary" onClick={()=> this.addStepInputs()}>+ Add Step</button>
            </div>

Finally, we write a button tied to the submit function.

<input type="submit" className="btn btn-secondary"></input>
          </fieldset>
        </form>
        </div>
        <div className="col-4"></div>
  </div>

Summary

In this tutorial, we’ve written an add recipe form that is dynamically controlled. We can add ingredients and their amounts along with steps. We can also delete this information if we need to.

Discussion (0)