React - How to handle nested forms and its value

Viewed 18336

I have to setup one form which have nested values in single form Basically I need to send data in below format to API

Payload: {"name": "The org name", "detail": "The org detail", "attributes": {"address": {"lines":[], "city":"", "state": "", "country": "India", "zip": ""}, "contacts":[{"name": "abc", "phone": "PH"}, {"name": "x", "phone": "PH"}] }}

I used react-bootstrap for handling forms.

below are the currrent code of my form

constructor(props) {
    super(props);
    this.state = this.getInitialState();
  }
  getInitialState() {
    const initialState = {
      organizationForm: {
        name: '',
        detail: '',
        type: 'org',
        attributes: {
          contacts: [{
            name: '',
            phone: ''
          }],
          address: {
            lines: [],
            city: '',
            state: '',
            country: '',
            zip: ''
          }
        }
      },
      errors: {}
    };

    return initialState;
  }

handleChange(e) {
    const organizationForm = this.state.organizationForm;
    var key = e.target.name;
    var value = e.target.value;
    organizationForm[key] = value;
    this.setState({
      organizationForm
    });
  }

Below are the code of form

  <Col className="create-content-wrap" sm={12}>
        <form className="">
          <FormGroup className="custom-form-group required row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              Name
            </ControlLabel>
            <FormControl
              className="custom-form-control col-sm-9"
              type="text"
              name="name"
              value={organizationForm.name}
              onChange={this.handleChange.bind(this)}
            />
          </FormGroup>
          <FormGroup className="custom-form-group required row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              Detail
            </ControlLabel>
            <FormControl
              className="custom-form-control col-sm-9"
              type="text"
              name="detail"
              componentClass="textarea"
              value={organizationForm.detail}
              onChange={this.handleChange.bind(this)}
            />
          </FormGroup>
          <FormGroup className="custom-form-group row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              Address
            </ControlLabel>
            <FormControl
              className="custom-form-control col-sm-9"
              type="text"
              name="lines"
              componentClass="textarea"
              value={organizationForm.lines}
              onChange={this.handleAddressChange.bind(this)}
            />
          </FormGroup>
          <FormGroup className="custom-form-group row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              City
            </ControlLabel>
            <FormControl
                className="custom-form-control col-sm-9"
                type="text"
                name="city"
                value={organizationForm.attributes.address.city}
                onChange={this.handleAddressChange.bind(this)}
            />
          </FormGroup>
          <FormGroup className="custom-form-group row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              State
            </ControlLabel>
            <FormControl
              className="custom-form-control col-sm-9"
              type="text"
              name="state"
              value={organizationForm.attributes.address.state}
              onChange={this.handleAddressChange.bind(this)}
            />
          </FormGroup>
          <FormGroup className="custom-form-group row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              Country
            </ControlLabel>
            <FormControl
              className="custom-form-control col-sm-9"
              type="text"
              name="country"
              value={organizationForm.attributes.address.country}
              onChange={this.handleAddressChange.bind(this)}
            />
          </FormGroup>
          <FormGroup className="custom-form-group row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              Zipcode
            </ControlLabel>
            <FormControl
              className="custom-form-control col-sm-9"
              type="number"
              name="zip"
              value={organizationForm.attributes.address.zip}
              onChange={this.handleAddressChange.bind(this)}
            />
          </FormGroup>
          <FormGroup className="custom-form-group row">
            <ControlLabel className="custom-form-control-label col-sm-3">
              Contacts
            </ControlLabel>
            <FormControl
              className="custom-form-control col-sm-9"
              type="number"
              name="contacts"
              value={organizationForm.attributes.contacts}
              onChange={this.handleChange.bind(this)}
            />
          </FormGroup>
        </form>
      </Col>

I am noobie in react js world. How can i bind nested fields of address and contacts ?

3 Answers

You can add couple of methods to handle for address and attributes like below Way - 1

<FormControl
      className="custom-form-control col-sm-9"
      type="text"
      name="city"
      value={organizationForm.attributes.address.city}
      onChange={this.handleAddressChange(this)}
 />

handleAddressChange = (e) => {
    const organizationForm = this.state.organizationForm;
    let address = organizationForm.attributes.address;
    var key = e.target.name;
    var value = e.target.value;
    address[key] = value;
    organizationForm.attributes.address = address;
    this.setState({
      organizationForm
    });
  }

This way your form is loosely coupled also . So if any change happens in the child object will not affect the other one. Similarly you can add for all the nested objects like address, attribute etc.

Way - 2

You can do the same by below but you need to handle which object your modifing with in the same method .

<FormControl
          className="custom-form-control col-sm-9"
          type="text"
          name="city"
          value={organizationForm.attributes.address.city}
          onChange={event => { this.handleChange(event, this.state.organizationForm.attributes.address  ,'address'); }}
     />

     <FormControl
          className="custom-form-control col-sm-9"
          type="text"
          name="city"
          value={organizationForm.attributes.address.city}
          onChange={event => { this.handleChange(event, this.state.organizationForm.attributes.contacts  ,'contacts'); }}
     />


     <FormControl
          className="custom-form-control col-sm-9"
          type="text"
          name="city"
          value=onChange={event => { this.handleChange(event, this.state.organizationForm ,'organizationForm'); }}
     />


    handleChange = (e , object , type) => {
        const organizationForm = this.state.organizationForm;
        var key = e.target.name;
        var value = e.target.value;
        object[key] = value;
        if(type === 'address'){
            organizationForm.attributes.address = object;
        } else if (type === 'contacts'){
            organizationForm.attributes.contacts = object;
        } 
        this.setState({
          organizationForm : organizationForm
        });
      }

I suggest that you keep your component state flat, just one key for each field in the form. When you submit the form, then you can format it into the shape necessary for the API.

This might not work for all cases, but it worked for me and might be helpful to anyone searching this now. I had a form that rendered different input elements, but it also rendered a button that opened up a modal component with a form unrelated to the larger form surrounding it. To solve the problem of this nested form: first, I removed the form elements in the modal (the <form>) and left just the <input> inside the component. Then, in that element, I added:

onChange = {e => setValue(e.target.value)}

After this, I made a state variable that looked like this: const [value, setValue] = useState("");. And finally, I added a button under the input with something like onClick={handleSubmit} and defined a function that submitted the form with value as the result of the form submission.

So essentially, I replaced the internal state of the nested form with React state, removed the form elements and kept the input, and then created a button that would handle my submission. That way, I bypassed having nested forms by just having an input element instead of a full form.

Related