Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I set a default value for a select menu in my React form component?

I'm using React 16.13.0. I'm trying to set a default value for an select menu in my form component. I have this ...

import React, {Component} from 'react';

/* Import Components */
import Input from '../components/Input';
import Country from '../components/Country';
import Province from '../components/Province';
import Button from '../components/Button'

class FormContainer extends Component {
  statics: {
    DEFAULT_COUNTRY: 484;
  }

  constructor(props) {
    super(props);

    this.state = {
      countries: [],
      provinces: [],
      newCoop: {
        name: '',
        type: {
          name: ''
        },
        address: {
          formatted: '',
          locality: {
            name: '',
            postal_code: '',
            state: ''
          },
          country: FormContainer.DEFAULT_COUNTRY,
        },
        enabled: true,
        email: '',
        phone: '',
        web_site: ''
      },

    }
    this.handleFormSubmit = this.handleFormSubmit.bind(this);
    this.handleClearForm = this.handleClearForm.bind(this);
    this.handleInput = this.handleInput.bind(this);
  }

  /* This life cycle hook gets executed when the component mounts */

  handleFormSubmit(e) {
    e.preventDefault();
    const NC = this.state.newCoop;
    delete NC.address.country;

    fetch('/coops/',{
        method: "POST",
        body: JSON.stringify(this.state.newCoop),
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
      }).then(response => {
        response.json().then(data =>{
          console.log("Successful" + data);
        })
    })
  }
  handleClearForm() {
    // Logic for resetting the form
  }
  handleInput(e) {
    let self=this
    let value = e.target.value;
    console.log("value:" + value);
    let name = e.target.name;
    //update State
    this.setValue(self.state.newCoop,name,value)
  }

  setValue = (obj,is, value) => {
       if (typeof is == 'string')
         return this.setValue(obj,is.split('.'), value);
       else if (is.length === 1 && value!==undefined)
         return this.setState({obj: obj[is[0]] = value});
       else if (is.length === 0)
         return obj;
       else
         return this.setValue(obj[is[0]],is.slice(1), value);
  }

  render() {
    return (
        <form className="container-fluid" onSubmit={this.handleFormSubmit}>

            <Input inputType={'text'}
                   title= {'Name'}
                   name= {'name'}
                   value={this.state.newCoop.name}
                   placeholder = {'Enter cooperative name'}
                   handleChange = {this.handleInput}

                   /> {/* Name of the cooperative */}
            <Input inputType={'text'}
                   title= {'Type'}
                   name= {'type.name'}
                   value={this.state.newCoop.type.name}
                   placeholder = {'Enter cooperative type'}
                   handleChange = {this.handleInput}

                   /> {/* Type of the cooperative */}

            <Input inputType={'text'}
                   title= {'Street'}
                   name= {'address.formatted'}
                   value={this.state.newCoop.address.formatted}
                   placeholder = {'Enter address street'}
                   handleChange = {this.handleInput}

                   /> {/* Address street of the cooperative */}

            <Input inputType={'text'}
                   title= {'City'}
                   name= {'address.locality.name'}
                   value={this.state.newCoop.address.locality.name}
                   placeholder = {'Enter address city'}
                   handleChange = {this.handleInput}

                   /> {/* Address city of the cooperative */}

          <Country title={'Country'}
                  name={'address.country'}
                  options = {this.state.countries}
                  value = {this.state.newCoop.address.country}
                  placeholder = {'Select Country'}
                  handleChange = {this.handleInput}
                  /> {/* Country Selection */}
          <Province title={'State'}
                  name={'address.locality.state'}
                  options = {this.state.provinces}
                  value = {this.state.newCoop.address.locality.state}
                  placeholder = {'Select State'}
                  handleChange = {this.handleInput}
                  /> {/* State Selection */}

          <Input inputType={'text'}
                   title= {'Postal Code'}
                   name= {'address.locality.postal_code'}
                   value={this.state.newCoop.address.locality.postal_code}
                   placeholder = {'Enter postal code'}
                   handleChange = {this.handleInput}

                   /> {/* Address postal code of the cooperative */}

          <Input inputType={'text'}
                   title= {'Email'}
                   name= {'email'}
                   value={this.state.newCoop.email}
                   placeholder = {'Enter email'}
                   handleChange = {this.handleInput}

                   /> {/* Email of the cooperative */}

          <Input inputType={'text'}
                   title= {'Phone'}
                   name= {'phone'}
                   value={this.state.newCoop.phone}
                   placeholder = {'Enter phone number'}
                   handleChange = {this.handleInput}

                   /> {/* Phone number of the cooperative */}
          <Input inputType={'text'}
                   title= {'Web Site'}
                   name= {'web_site'}
                   value={this.state.newCoop.web_site}
                   placeholder = {'Enter web site'}
                   handleChange = {this.handleInput}

                   /> {/* Web site of the cooperative */}


          <Button
              action = {this.handleFormSubmit}
              type = {'primary'}
              title = {'Submit'}
            style={buttonStyle}
          /> { /*Submit */ }

          <Button
            action = {this.handleClearForm}
            type = {'secondary'}
            title = {'Clear'}
            style={buttonStyle}
          /> {/* Clear the form */}

        </form>
    );
  }

  componentDidMount() {
    let initialCountries = [];
    let initialProvinces = [];
    // Get initial countries
    fetch('/countries/')
        .then(response => {
            return response.json();
        }).then(data => {
        initialCountries = data.map((country) => {
            return country
        });
        console.log("output ...");
        console.log(initialCountries);
        this.setState({
            countries: initialCountries,
        });
    });
    // Get initial provinces (states)
    fetch('/states/484/')
        .then(response => {
            return response.json();
        }).then(data => {
        console.log(data);
        initialProvinces = data.map((province) => {
            return province
        });
        this.setState({
            provinces: initialProvinces,
        });
    });
  }
}

const buttonStyle = {
  margin : '10px 10px 10px 10px'
}

export default FormContainer;

But the country value is not getting set. Any ideas?

Edit: Adding Country.jsx component code

import React from 'react';

class Country extends React.Component {
    constructor() {
        super();
    }

    render () {
        let countries = this.props.options;
        let optionItems = countries.map((country) =>
                <option key={country.id} value={country.id}>{country.name}</option>
            );

        return (
          <div className="form-group">
                        <label for={this.props.name}> {this.props.title} </label>
            <select
                      id = {this.props.name}
                      name={this.props.name}
                      value={this.props.value}
                      onChange={this.props.handleChange}
                      className="form-control">
                      <option value="" disabled>{this.props.placeholder}</option>
                      {optionItems}
            </select>
          </div>
        )
    }
}

export default Country;
like image 887
Dave Avatar asked Mar 02 '23 17:03

Dave


2 Answers

There is a small issue with your code, you are not passing props in constructor and the super in the Country component. You have to pass the props to the constructor and super or just remove the constructor as you arenot doing anything in the constructor.

Try this.

class Country extends React.Component {
    constructor(props) {
        super(props);
    }

    render () {
        let countries = this.props.options;
        let optionItems = countries.map((country) =>
                <option key={country.id} value={country.id}>{country.name}</option>
            );

        return (
          <div className="form-group">
                        <label for={this.props.name}> {this.props.title} </label>
            <select
                      id = {this.props.name}
                      name={this.props.name}
                      value={this.props.value}
                      onChange={this.props.handleChange}
                      className="form-control">
                      <option value="" disabled>{this.props.placeholder}</option>
                      {optionItems}
            </select>
          </div>
        )
    }
}

export default Country;
like image 121
Sohail Ashraf Avatar answered Mar 05 '23 16:03

Sohail Ashraf


If you simplify your state to a simple object of properties with string values and update those values after the API call, then you can set a dynamic default value very easily (read below for a more in-depth explanation).

Working example:

Edit Initialize Select Option


containers/FormContainer

import React, { Component } from "react";
import Input from "../../components/Input";
import Select from "../../components/Select";
import Button from "../../components/Button";
import { fakeAPI } from "../../api";
import { fields1, fields2, fields3 } from "./fields";

const buttonStyle = {
  margin: "10px 10px 10px 10px"
};

/* moving state outside of class for reuseability */
const initialState = {
  countries: [],
  providences: [],
  name: "",
  typeName: "",
  formattedAddress: "",
  localityName: "",
  postalCode: "",
  providence: "",
  country: 484,
  enabled: true,
  email: "",
  phone: "",
  website: ""
};

class FormContainer extends Component {
  constructor(props) {
    super(props);

    /* spreading initial state above with isLoading and err properties */
    this.state = { ...initialState, isLoading: true, err: "" };
    this.handleFormSubmit = this.handleFormSubmit.bind(this);
    this.handleClearForm = this.handleClearForm.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  /* This life cycle hook gets executed when the component mounts */
  componentDidMount() {
    this.fetchCountryData();
  }

  /* Get initial countries/providences */
  async fetchCountryData() {
    try {
      /* 
        since the two (countries and providences) are intertwined, 
        I'd recommend creating one API call and setting state once 
      */
      const res = await fakeAPI.getCountryData();
      const data = await res.json();

      // throw new Error("No data available!");

      this.setState({
        countries: data.countries,
        providences: data.providences,
        country: data.countries[0].name,
        providence: data.providences[0].name,
        isLoading: false,
        err: ""
      });

      /*
         const res = await fetch("/countries/");
         const data = await res.json();

         this.setState(...);
      */
    } catch (err) {
      /* catch any errors returned from API call */
      this.setState({ err: err.toString() });
    }
  }

  /* Handles form submissions */
  async handleFormSubmit(e) {
    e.preventDefault();

    /* build the JSON object here to send to the API */
    const newCoop = {
      name: this.state.name,
      type: {
        name: this.state.typeName
      },
      address: {
        formatted: this.state.formattedAddress,
        locality: {
          name: this.state.localityName,
          postal_code: this.state.postalCode,
          state: this.state.providence
        },
        country: this.state.country
      },
      enabled: true,
      email: this.state.email,
      phone: this.state.phone,
      web_site: this.state.website
    };

    /* 
        try {
          const res = await fetch("/coops/", {
            method: "POST",
            body: JSON.stringify(newCoop),
            headers: {
              Accept: "application/json",
              "Content-Type": "application/json"
            }
          });
          const data = await res.json();

          console.log(data);
        } catch (err) {
          console.error(err.toString());
          this.setState({ err });
        }
    */

    alert("Sent to API: " + JSON.stringify(newCoop, null, 4));
  }

  /* Clears the form while maintaining API data */
  handleClearForm() {
    this.setState(({ countries, providences }) => ({
      ...initialState,
      countries,
      country: countries[0].name, // sets a default selected value
      providence: providences[0].name, // sets a default selected value
      providences
    }));
  }

  /* Updates form state via "event.target.name" and "event.target.value" */
  handleChange({ target: { name, value } }) {
    this.setState({ [name]: value });
  }

  /* 
    Renders the view according to state: 
    - If there's an error: show the error,
    - Else if it's loading: show a loading indicator
    - Else show the form with API data
  */
  render() {
    return this.state.err ? (
      <p>{this.state.err}</p>
    ) : this.state.isLoading ? (
      <p>Loading...</p>
    ) : (
      <form
        className="container-fluid"
        style={{ padding: 20 }}
        onSubmit={this.handleFormSubmit}
      >
        {fields1.map(({ name, placeholder, title, type }) => (
          <Input
            key={name}
            type={type}
            title={title}
            name={name}
            value={this.state[name]}
            placeholder={placeholder}
            onChange={this.handleChange}
          />
        ))}
        {fields2.map(({ name, placeholder, title, options }) => (
          <Select
            key={name}
            title={title}
            name={name}
            options={this.state[options]}
            value={this.state[name]}
            placeholder={placeholder}
            onChange={this.handleChange}
          />
        ))}
        {fields3.map(({ name, placeholder, title, type }) => (
          <Input
            key={name}
            type={type}
            title={title}
            name={name}
            value={this.state[name]}
            placeholder={placeholder}
            onChange={this.handleChange}
          />
        ))}
        <Button
          buttonType="primary"
          type="submit"
          title="Submit"
          style={buttonStyle}
        />
        <Button
          onClick={this.handleClearForm}
          buttonType="secondary"
          type="button"
          title="Clear"
          style={buttonStyle}
        />
      </form>
    );
  }
}

export default FormContainer;

There's a few anti-pattern choices in your code that may it more difficult to do what you want. To be brief:

  • Don't delete properties from the this.state object as it will break React. React expects the state and props to be immutable (you'll only be shallow copying/overriding the state object using this.setState(); and update props from a parent higher order component)
  • Since you're only dealing with one form with several fields, simplify it by keeping your state properties as simple string values. When working with nested object properties, it makes it harder to maintain and update -- you'll essentially have to find the parent property that was changed, then iterate over the child properties to find the changed child property, override the child value and then rebuild the nested parent property, over and over and over. You can avoid this traversing by simply using strings and then building the structured fields JSON object before sending it to the API.
  • Avoid using let when the value doesn't change within the same execution/render. This a common mistake that could potentially lead to breaking React/your component. Instead, use const as it declares it as a read-only variable and attempting to override it will throw an error.

Common mistake (as you can see, neither countries nor optionItems is updated/overwritten after it's been declared within the same execution cycle):

let options = this.props.options;
let optionItems = options.map((country) => <option key={country.id} value={country.id}>{country.name}</option>);

return ( ... );

Instead (just like in a function, these variables are redefined each time the function is called, but neither will be overwritten within the same execution period):

const { options } = this.props; // same as: const options = this.props.options;
const optionItems = options.map((country) => <option key={country.id} value={country.id}>{country.name}</option>);

return ( ... );

Taking the first example above, we could accidentally override options, when we don't want it to be overwritten:

let options = this.props.options;
options = "Hello world";
let optionItems = options.map((country) => <option key={country.id} value={country.id}>{country.name}</option>);

return ( ... );

Now, instead of rendering a select element with options, we broken it by trying to map over a string. This may seem arbitrary, but it ensures a strict compliance to avoid potential bugs within the component and its children.

  • If a component doesn't utilize state, then it can be a pure function. In the CodeSandbox example above, my Select component is a simple function that receives a single object argument (using ES6 destructuring we can pull out the properties from the object) and returns JSX.
like image 45
Matt Carlotta Avatar answered Mar 05 '23 15:03

Matt Carlotta