41

As I am looking at the examples in the reference for controlled form components in react.js official website, I am wondering how is one supposed to implement a form in which you would be able to remove and add input elements dynamically in such a way that they are controlled components? Is this even possible?

In the examples we can see:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

Due to the nature of my work, I often find myself having to implement such forms. Moreover, I don't add or remove input elements directly - I am managing custom components, but for the sake of simplicity here I am asking for basic form elements.

Ali
  • 1,633
  • 7
  • 35
  • 58
Martin Shishkov
  • 2,709
  • 5
  • 27
  • 44

6 Answers6

74

How adding/removing input elements dynamically possible?

Yes, it is possible, you can add/remove input elements dynamically, But for that you need to take care of few things:

1- Proper binding of events.

2- Array to store the values of each input element separately.

3- When user fill value in any input element then updating only that specific value in state.

Logic:

Maintain an array inside state, that will store the values. Use #array.map to create ui (input element) for each array values. while creating the fields, use a remove button with each field, and pass the index of field in that function, it will help you to identify which field you wants to delete, do the same thing for onChange also.

Check this example:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { values: [] };
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  createUI(){
     return this.state.values.map((el, i) => 
         <div key={i}>
         <input type="text" value={el||''} onChange={this.handleChange.bind(this, i)} />
         <input type='button' value='remove' onClick={this.removeClick.bind(this, i)}/>
         </div>          
     )
  }

  handleChange(i, event) {
     let values = [...this.state.values];
     values[i] = event.target.value;
     this.setState({ values });
  }
  
  addClick(){
    this.setState(prevState => ({ values: [...prevState.values, '']}))
  }
  
  removeClick(i){
     let values = [...this.state.values];
     values.splice(i,1);
     this.setState({ values });
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.values.join(', '));
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
          {this.createUI()}        
          <input type='button' value='add more' onClick={this.addClick.bind(this)}/>
          <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id='container'/>

Check the working jsfiddle: https://jsfiddle.net/mayankshukla5031/ezdxg224/

Mayank Shukla
  • 100,735
  • 18
  • 158
  • 142
12

How adding/removing input elements dynamically with functional components?

The same component from the selected answer reviewed and rewrited as a functional component.

import React from 'react';
import { useState } from 'react';

function DynamicInput() {

    const [values, setValues] = useState({ val: []});

      function createInputs() {
        return values.val.map((el, i) =>
          <div key={i}>
            <input type="text" value={el||''} onChange={handleChange.bind(i)} />
            <input type='button' value='remove' name={i} onClick={removeClick.bind(i)} />
          </div>
        );
      }

    function handleChange(event) {
      let vals = [...values.val];
      vals[this] = event.target.value;
      setValues({ val: vals });
    }

    const addClick = () => {
      setValues({ val: [...values.val, '']})
    }

    const removeClick = (event) => {
      let vals = [...values.val];
      let index = Number(event.target.name);
      vals.splice(index, 1);
      setValues({ val: vals });
    }

    const handleSubmit = event => {
      alert('A name was submitted: ' + values.val.join(', '));
      event.preventDefault();
    }

    return (
      <form onSubmit={handleSubmit}>
          {createInputs()}
          <input type='button' value='add more' onClick={addClick} />
          <input type="submit" value="Submit" />
      </form>
    );

}

export default DynamicInput;

Brijesh Kalkani
  • 789
  • 10
  • 27
yuri
  • 3,182
  • 2
  • 16
  • 26
  • I am defining everything exactly as you mentioned in your answer, but this woudn't work if we have two inputs generated with the `addClick` method, the values will be the same in both inputs. do you know how to solve it? This is how `jsx` look like `
    `
    – Ali Feb 07 '20 at 19:18
  • Don't get it, ask a new question with proper details and post here the link, I'll try to solve it ;) – yuri Feb 08 '20 at 11:54
  • I asked new question with the code here, https://stackoverflow.com/questions/60132525/how-to-create-and-store-values-of-a-dynamic-form-reactjs .Thanks in advanced – Ali Feb 09 '20 at 00:31
  • i know this is two years old, but i am trying to do the same thing here: https://stackoverflow.com/questions/73779422/form-handle-array-as-input-react where i have implemted this selution but i am missing something to connect it .. hope you maybe can help me.. – Nadia Hansen Sep 20 '22 at 14:53
1

If you want to use an object because you have many input field you can do this :

class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = { values: [] };
      this.handleSubmit = this.handleSubmit.bind(this);
    }

class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = { values: [{question: '',response: ''}] };
      this.handleSubmit = this.handleSubmit.bind(this);
    }
    
     createUI(){
       return this.state.values.map((el, i) => 
           <div key={i}>
              <input type="text" value={el.question}  onChange={this.handleChange.bind(this, i)} />
              <textarea type="text"  value={el.response} onChange={this.handleChange.bind(this, i)} />
              <input type='button' value='remove' onClick={this.removeClick.bind(this, i)}/>
           </div>          
       )
    }
     handleChange (i,event) {
       let values = [...this.state.values];
       values[i][event.target.id] =   event.target.value;
       this.setState({ values }); 
    }
    
    addClick(){
      this.setState(prevState => ({ values: [...prevState.values, {question: '',response: '',}]}))
    }
    
    removeClick(i){
       let values = [...this.state.values];
       values.splice(i,1);
       this.setState({ values });
    }
    
    handleSubmit(event) {
      event.preventDefault();
      alert(this.state.values.map(objet=> "{"+ "  "  + "'question' :" + "  " + objet.question + "  " + "'response' :" + "  " + objet.response +"  " + "}") );
    }
     render() {
      return (
        <form onSubmit={this.handleSubmit}>
            {this.createUI()}        
            <input type='button' value='add more' onClick={this.addClick.bind(this)}/>
            <input type="submit" value="Submit" />
        </form>
      );
    }
  }
  
  ReactDOM.render(<App />, document.getElementById('container'));
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<!-- begin snippet: js hide: false console: true babel: false -->
              <input type="text"  value={el.response} onChange={this.handleChange.bind(this, i)}  id="response" a />
          
         <input type='button' value='remove' onClick={this.removeClick.bind(this, i)}/>
        
         
       </div>
      
       )
    }
  
    handleChange (i,event) {
       let values = [...this.state.values];
       values[i][event.target.id] =   event.target.value;
       this.setState({ values }); 
    }
    
    addClick(){
      this.setState(prevState => ({ values: [...prevState.values, {question: '',response: '',}]}))
    }
    
    removeClick(i){
       let values = [...this.state.values];
       values.splice(i,1);
       this.setState({ values });
    }
  
    handleSubmit(event) {
      event.preventDefault();
      alert(this.state.values.map(objet=> "{"+ "  "  + "'question' :" + "  " + objet.question + "  " + "'response' :" + "  " + objet.response +"  " + "}") );
    }
    render() {
      return (
        <form onSubmit={this.handleSubmit}>
            {this.createUI()}   
            <div  className="d-flex justify-content-end">
              <KamaminiButton onClick={this.addClick.bind(this)}  text='Ajouter une autre question' />
            </div>     
            <input type="submit" value="Submit" />
        </form>
      );
    }
  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id='container'/>
Moctar Yonli
  • 107
  • 5
1

I believe to have found a much simpler way to do this by using the useRef() hook. You simply keep a reference to the form which has the variable number of inputs, and use a button for adding and removing inputs. Then on submit, you use the childNodes() function to get all the values.

form.js:

import React, { useState, useRef } from 'react'

const Form = () => {
  const formRef = useRef(null)
  const [numInputs, setNumInputs] = useState(1)

  const submitHandler = () => {
    let inputs = formRef.current.childNodes
    for (let i = 0; i < numInputs; i++) {
      console.log(inputs[i].value)
    }
  }

  return (
    <form ref={formRef}>
      {Array.from(Array(numInputs)).map((_, i) => (
        <input type="text" />
      ))}
    </form>
    <button onClick={() => setNumInputs(numInputs - 1)}>Remove input</button>
    <button onClick={() => setNumInputs(numInputs + 1)}>Add input</button>
    <button onClick={submitHandler}>Submit</button>
  )
}
JSON_Derulo
  • 892
  • 2
  • 14
  • 39
0

You can easily use your state or the props that you are passing from other components to decide what your form should be.

Here a dumb example:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    let myForm;
    if(this.props.someprop == true){
       myForm = (
           <form onSubmit={this.handleSubmit}>
              <label>
                 Name:
                 <input type="text" value={this.state.value} onChange=      {this.handleChange} />
              </label>
              <input type="submit" value="Submit" />
           </form>
       );
    }
    else if(this.state.statevar == "expectedValue"){
        myForm = (
           // other HTML with embedded JS here
        );
    }


    return (
         {myForm}
    );
  }
}

I did it in the render method just to be clear but all the logic can be moved in auxiliary functions.

rakwaht
  • 3,666
  • 3
  • 28
  • 45
0
import React, { Component, } from 'react';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
var childJson = []
export default class AddInvoice extends Component {
constructor(props) {
    super(props);
    this.state = {
        Json: [],
        rows: [{}]
    }
}

handleChange = idx => e => {
    const { name, value } = e.target;
    const rows = [...this.state.rows];
    rows[idx] = { [name]: value };
    this.setState({ rows });

    console.log(`rows-->>${this.state.rows[idx].amount}`);
    childJson.push(this.dynamicJson(this.state.rows, idx))
    this.setState({ Json: childJson })

};
handleAddRow = () => {
    const item = {
        name: "",
        mobile: "",
        btn: ""
    };
    this.setState({
        rows: [...this.state.rows, item]
    });
};
handleRemoveRow = (idx) => {
    this.state.rows.splice(idx, 1);
    this.setState({ rows: this.state.rows });
};
dynamicJson(rows, index) {
    return {
        "service": rows[index].text,
        "tax": rows[index].tax,
        "amount": rows[index].amount
    }
};
render() {
    return (
        <div className="ui one column grid">
            <div className=" row">
                <div className="one wide computer one wide tablet one wide mobile column">
                </div>
                <div className="fourteen wide computer fourteen wide tablet fourteen wide mobile column">
                    <h1 id="title_header">INVOICE-ADD NEW</h1>
                </div>
                <div className="one wide computer one wide tablet one wide mobile column">
                </div>
            </div>
            <div className=" row">
                <div className="one wide computer one wide tablet one wide mobile column">
                </div>
                <div className="fourteen wide computer fourteen wide tablet fourteen wide mobile column">
                    <div id="#cus_segment" className="ui segment" style={{ backgroundColor: '#f5f5f5' }}>
                        <div className="ui form">
                            <div className="ui  stackable grid">
                                <div className="six wide column">
                                    <div className="field">
                                        <label>Invoice No</label>
                                        <input type="text" name="invoiceno" placeholder="Invoice No" value={this.state.invoiceno} onChange={e => this.setState({ invoiceno: e.target.value })} />
                                    </div>
                                </div>
                                <div className=" six wide column">
                                    <div className="field">
                                        <label>Customer</label>
                                        <select className="ui fluid selection search dropdown" name="customer" value={this.state.customer} onChange={e => this.setState({ customer: e.target.value })}>
                                            <option defaultValue="">select</option>
                                            <option value="sohanpatil@gmail.com">sohanpatil@gmail.com</option>
                                            <option value="mayurawati@gmail.co">mayurawati@gmail.com</option>
                                            <option value="nasirpatel@gmail.com">nasirpatel@gmail.com</option>
                                            <option value="nandkishorshinde@gmail.com">nandkishorshinde@gmail.com</option>
                                        </select>
                                    </div>
                                </div>
                                <div className="one row">
                                    <div className="six wide column">
                                        <div className="field">
                                            <label>Invoice Date</label>
                                            <div className="ui calendar" id="calender1">
                                                <div className="ui input right icon">
                                                    <i className="calendar icon"></i>
                                                    <input type="text" placeholder="Invoice Date" value={this.state.invoicedate} onBlur={e => this.setState({ invoicedate: e.target.value })} />
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                    <div className="six wide column">
                                        <div className="field">
                                            <label>Due Date</label>
                                            <div className="ui calendar" id="calender2">
                                                <div className="ui input right icon">
                                                    <i className="calendar icon"></i>
                                                    <input type="text" placeholder="Due Date" value={this.state.duedate} onBlur={e => this.setState({ duedate: e.target.value })} />
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <div className="two row">
                                    <div className="six wide column">
                                        <div className="field">
                                            <label>Header</label>
                                            <input type="text" name="header" placeholder="Header" value={this.state.header} onChange={e => this.setState({ header: e.target.value })} />
                                        </div>
                                    </div>
                                    <div className="six wide column">
                                        <div className="field">
                                            <label>Remark</label>
                                            <input type="text" name="remark" placeholder="Remark" value={this.state.remark} onChange={e => this.setState({ remark: e.target.value })} />
                                        </div>
                                    </div>
                                </div>
                                <div className="three row">
                                    <div className="ten wide column">
                                        <button className="ui primary button" type="submit">Save</button>
                                        <button className="ui  button" type="submit">Clear</button>
                                        <button className="ui  button" type="submit">Cancel</button>
                                    </div>
                                </div>

                                <div className="foure row">
                                    <div className="one wide column">
                                        <div className="field">
                                            <h4 style={{ textAlign: "center", borderRadius: 2 }}>Action</h4>
                                        </div>
                                    </div>
                                    <div className="two wide column" style={{ marginLeft: 55 }}>
                                        <div className="field">
                                            <h4 style={{ textAlign: "center", borderRadius: 2 }}>Text</h4>
                                        </div>
                                    </div>
                                    <div className="three column" style={{ marginLeft: 200 }}>
                                        <div className="field">
                                            <h4 style={{ textAlign: "center", borderRadius: 2 }}>Tax</h4>
                                        </div>
                                    </div>
                                    <div className="foure wide column" style={{ marginLeft: 190 }}>
                                        <div className="field">
                                            <h4 style={{ textAlign: "center", borderRadius: 2 }}>Amount</h4>
                                        </div>
                                    </div>
                                </div>
                                {this.state.rows.map((item, idx) => (
                                    <div className="five row" id="addr0" key={idx} >
                                        <div className="one wide column">
                                            <div className="field">
                                                <div className="ui icon" style={{ backgroundColor: "#f76060", color: "white", height: 35, width: 40, textAlign: "center", borderRadius: 2 }} onClick={() => this.handleRemoveRow(idx)}>
                                                    <i className="trash alternate icon" style={{ marginTop: 8 }}></i>
                                                </div>
                                            </div>
                                        </div>

                                        <div className="five wide column">
                                            <div className="field">
                                                <input type="text" name="text" placeholder="text" value={this.state.rows[idx].text} onChange={this.handleChange(idx)} />
                                            </div>
                                        </div>

                                        <div className="three wide column">
                                            <div className="field">
                                                <select className="ui fluid selection search dropdown " name="tax" id="tax_dropdown" value={this.state.rows[idx].tax} onChange={this.handleChange.bind(this)}>
                                                    <option defaultValue="">Select</option>
                                                    <option value="STAX">STAX</option>
                                                    <option value="VAT">VAT</option>
                                                </select>
                                            </div>
                                        </div>

                                        <div className="three wide column">
                                            <div className="field">
                                                <input type="text" name="amount" placeholder="amount" value={this.state.rows[idx].amount} onChange={this.handleChange(idx)} />
                                            </div>
                                        </div>
                                    </div>

                                ))}
                                <div className="six row">
                                    <div className="two wide column">
                                        <div className="field">
                                            <div className="ui icon" style={{ backgroundColor: "#c4d3d3", height: 35, width: 55, textAlign: "center", borderRadius: 2 }} onClick={this.handleAddRow}>
                                                <i className="plus icon" style={{ marginTop: 8 }}></i>
                                                <label>Add</label>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div className="one wide computer one wide tablet one wide mobile column">
                </div>
            </div>

        </div>
    );
}

}