6

I have a problem where I have to render 4 dropdowns, each with similar options(i just rendered 1 here). If I select an option in any one of the dropdowns, that option should not be available in the other three.

How should I update the selected_planets in the state? The below code updates the selected_planets from 1 select dropdown. But still that same option is available everywhere and I could not able to get 4 different options inside selected_planets array? How should I proceed?

Also, the response from API fetch is an array of objects, which I mapped through and update in planets Array. For demo purpose, let's consider, planets: [Neptune, Saturn, Mars, Earth, Venus, Jupiter]

import React, { Component } from 'react';

export default class Dashboard extends Component {
  state = {
    planets: [],
    selected_planets: []
  };

  componentDidMount() {
    fetch('url')
    .then(response => {
      return response.json();
    })
    .then(data => {
      this.setState({ planets: data });
    });
  }

  handleSelect = event => {
    this.setState({ selected_planets: [event.target.value] });
  };

  render() {
    let select_Planets = this.state.planets.map(planet => {
      return planet.name;
    });
    let options = select_Planets.map(planet => (
      <option key={planet} value={planet}>
        {planet}
      </option>
    ));

    return (
      <select onChange={this.handleSelect}>
        <option defaultChecked></option>
          {options}
      </select>
    );
  }
}
2
  • I would treat planet and dropdown as one component and parent component would hold all planets with their dropdowns. Commented Sep 29, 2019 at 17:45
  • lift state up to parent component or use something like useContext or redux. Commented Sep 29, 2019 at 17:48

5 Answers 5

5

This can be achieved by producing a new set of options for each dropdown on render based off of what are the currently selected options, and what is the selected option for that dropdown.

First make sure each dropdown is binding to a property in your component's state and updating on change:

  constructor() {
    super();
    this.state = {
      planets: ["a", "b", "c", "d"],
      inputs: {
        d1: "",
        d2: ""
      }
    };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange({ target }) {
    this.setState({
      ...this.state,
      inputs: {
        ...this.state.inputs,
        [target.name]: target.value
      }
    });
  }
<select
  name="d1"
  value={this.state.inputs.d1}
  onChange={this.handleChange}>

Then you can obtain a list of the selected planets within the render method by converting the input object into an array using Object.values():

const selectedPlanets = Object.values(this.state.inputs);

Then create a new array for each of the dropdowns which will omit any planets which have already been selected unless it is selected by that particular dropdown itself:

const d1Options = this.state.planets.filter(
  p => !selectedPlanets.find(sP => sP === p) || p === this.state.inputs.d1
);
const d2Options = this.state.planets.filter(
  p => !selectedPlanets.find(sP => sP === p) || p === this.state.inputs.d2
);
<select
 name="d1"
 value={this.state.inputs.d1}
 onChange={this.handleChange}>
    <option></option>
    {d1Options.map(o => (
      <option key={o}>{o}</option>
    ))}
</select>

I've put together a working example here:

https://codepen.io/curtis__/pen/pozmOmx

Sign up to request clarification or add additional context in comments.

Comments

2

You can solve this is multiple ways. Here is pseudo-code. Create an object or array to hold the values of selected index. I will suggest to use useState API. Instead of setState.

class extends Component {
  static state = {
    dropdown1: "",
    dropdown2: "",
    dropdown3: "",
    dropdown4: ""
  }
  constructor(props) {
    super(props)
  }
  handleClick = (id, {target: {value}}) => {
    this.setState({
      [id]: value
    })
  }
  render()  {
    <div>
      <select onChange={this.handleClick.bind(null, "dropdown1")}>

      </select>
      <select onChange={this.handleClick.bind(null, "dropdown2")}>

      </select>
      <select onChange={this.handleClick.bind(null, "dropdown3")}>

</select>
<select onChange={this.handleClick.bind(null, "dropdown4")}>

</select>
    </div>
  }

}

Comments

2

You should replace this line

  let select_Planets = this.state.planets.map(planet => {
    return planet.name;
   });

with

 let select_Planets = this.state.planets.filter(planet => !this.state.selected_planets.includes(planet))

This will make sure that the only available options are those that have not been selected already. you can do it for all the other dropdowns.

you also replace the following lines

handleSelect = event => {
   this.setState({ selected_planets: [event.target.value] });
 };

with

 handleSelect = event => {
    this.setState({ selected_planets: [...this.state.selected_planets, event.target.value] });
  };

Comments

2

You can manage all value in a Mother component, passing the selected option to all child components.

function DropdownBoxes({ url }) {
    const [options, setOptions] = useState([]);
    const [selected, setSelected] = useState({ a: 'ABC', b: 'CDE' });

    useEffect(() => { // componentDidMount
        fetch(url).then(setOptions);
    }, [url]);

    const onChange = (key, value) => {
        setSelected(prev => ({ // this.setState
            ...prev,
            [key]: value,
        }));
    }

    const filterOptions = Object.values(selected); // ['ABC', 'CDE']

    return (
        <div>
            <DropdownBox
                options={options}
                filterOptions={filterOptions}
                value={selected['a']}
                onChange={val => onChange('a', val)}
            />
            <DropdownBox
                options={options}
                filterOptions={selected}
                value={selected['b']}
                onChange={val => onChange('b', val)}
            />
        </div>
    )
}

When you render the options, add a filter to show the option only if it is equal to the value, or it is not a subset of filterOptions. If you cannot/ do not want to chnage any code of dropdown box, you can add the filter to mother component when to passing options.

function DropdownBox({options, filterOptions, value, onChange}) {
    ...
    const filter = (opt) => {
        return opt.value === value || !filterOptions.includes(opt.value);
    }
    return (
        <select>
            {options.filter(filter).map(opt => (
                <option value={opt.value} ...>{opt.label}</option>
            ))}
        </select>
    )
}

Comments

1

You can create a SelectDropDown view component and render it in the parent component. And the selected value for all dropdown is maintained by its parent component eg: selectedPlanets state.

// structure for selectedPlanets state.

type SelectedPlanets = {
  [dropDownId: string]: string
}; 

Rusable SelectDropDown.js


import * as React from 'react';

type Props = {
  valueField: string,
  primaryKeyField: string, // field like id or value.
  selectedValues: Array<string>, // values of the primaryField attribute that uniquely define the objects like id, or value
  options: Array<Object>,
  handleSelect: (event: SystheticEvent<>) => void
}

export default class SelectDropDown extends React.Component<Props> {

  render() {
    const { 
      options, 
      selectedValues, 
      primaryKeyField, 
      valueField,
      handleSelect
    } = this.props;

    const optionsDom = options.map(option => {
      if(!selectedValues.includes(option[primaryKeyField])){
        return (
          <option key={option[primaryKeyField]} value={option[valueField]}>
           {planet}
          </option>
        );
      }
    });

    return (
      <select onChange={handleSelect}>
        <option defaultChecked></option>
          {optionsDom}
      </select>
    );
  }
}

Sudo code Dashboard.js

import * as React from 'react';
import SelectDropDown from "./SelectDropDown"

export default class Dashboard extends React.Component {
  state = {
    planets: [],
    selectedPlanets: {}
  };

  /*
   Assumimg planets struct 
   [{
      planetId: 1,
      planet: "Earth" 
   }]
  */
  componentDidMount() {
    fetch('url')
    .then(response => {
      return response.json();
    })
    .then(data => {
      this.setState({ planets: data });
    });
  }

  handleSelect = (dropDownId, event) => {
    const { selectedPlanets } = this.state;
    const selectedPlanetsCopy = {...selectedPlanets};
    selectedPlanetsCopy[dropDownId] = event.target.value;
    this.setState({ selectedPlanets: selectedPlanetsCopy });
  };

  getSelectedValues = (dropDownId) => {
   const {selectedPlanets} = this.state;
   const selectedValues = [];
   Object.keys(selectedPlanets).forEach((selectedPlanetDropDownId) => {
    if(dropDownId !== selectedPlanetDropDownId) {
      selectedValues.push(selectedPlanets[selectedPlanetDropDownId]);
    }
   });

   return selectedValues;
  }

  render() {
    const { planets } = this.state;
    return (
      <SelectDropDown 
         valueField={"planet"} 
         primaryKeyField={"planetId"}
         selectedValues={this.getSelectedValues("dropDown1")}
         options={planets}
         handleSelect={this.handleSelect.bind(this, "dropDown1")}
      />

      <SelectDropDown 
         valueField={"planet"} 
         primaryKeyField={"planetId"}
         selectedValues={this.getSelectedValues("dropDown2")}
         options={planets}
         handleSelect={this.handleSelect.bind(this, "dropDown2")}
      />
    );
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.