2

I am making a simple react application where there are dropdowns in which one dependent on another.

-> Here dropdown 1 has the value as type of game like Indoor and Outdoor.

-> Here dropdown 2 has the value as type of sport like Chess , Tennis and Football .

Requirement:

The following different use cases needs to be covered,

Scenarios:

-> User selects Indoor from dropdown 1, then in dropdown 2 only the value of Chess needs to be enabled and others needs to be disabled.

enter image description here

-> User selects Outdoor from dropdown 1, then in dropdown 2 only the value of Tennis and Football needs to be enabled and option Chess needs to be disabled.

enter image description here

Vice versa:

-> User selects Chess from dropdown 2, then in dropdown 1 only the value of Indoor needs to be enabled and others needs to be disabled.

enter image description here

-> User selects Tennis or Football from dropdown 2, then in dropdown 1 only the value of Outdoor needs to be enabled and others needs to be disabled.

enter image description here

Here we provide option of allowClear so that user can reset their selection in any select box selection (the close icon) enter image description here and do the above mentioned scenario in any way like selecting option from first dropdown or in second dropdown based on which the another dropdown make the option enable or disable.

Right now I have a data like this and open for modification to achieve the expected result.

const data = {
  games: {
    type: [
      { id: 1, value: "Indoor", sportId: [2] },
      { id: 2, value: "Outdoor", sportId: [1, 3] }
    ],
    sport: [
      { id: 1, value: "Tennis", typeId: [2] },
      { id: 2, value: "Chess", typeId: [1] },
      { id: 3, value: "Football", typeId: [2] }
    ]
  }
}

The property names may vary so I cannot rely on the hard coded/static name inside code like data.games.type or data.games.sport.

And hence I tried with dynamic approach like,

{Object.entries(data.games).map((item, index) => {
        return (
          <div className="wrapper" key={index}>
            <h4> {item[0]} </h4>
            <Select
              defaultValue="selectType"
              onChange={handleChange}
              allowClear
            >
              <Option value="selectType"> Select {item[0]} </Option>
              {item[1].map((option, j) => (
                <Option key={j} value={option.value}>
                  {option.value}
                </Option>
              ))}
            </Select>
            <br />
          </div>
        );
 })}

Reactjs sandbox:

Edit React Typescript (forked)

Note: The options needs to be disabled (only) and should not be removed from select box as user can clear any select box selection and select value from any of the dropdown.

Pure Javascript Approach: (Ignore reset of dropdown in this JS example which handled in reactjs with help of clear icon (close icon))

Also here is the Pure JS (working) way of approach tried with hard coded select boxes with id for each element respectively and also with some repetition of code in each addEventListener,

const data = {
  games: {
    type: [
      { id: 1, value: "Indoor", sportId: [2] },
      { id: 2, value: "Outdoor", sportId: [1, 3] }
    ],
    sport: [
      { id: 1, value: "Tennis", typeId: [2] },
      { id: 2, value: "Chess", typeId: [1] },
      { id: 3, value: "Football", typeId: [2] }
    ]
  }
}

const typeSelect = document.getElementById('type')
const sportSelect = document.getElementById('sport')

const createSelect = (values, select) => {
  values.forEach(t => {
    let opt = document.createElement('option')
    opt.value = t.id
    opt.text = t.value
    select.append(opt)
  })
}

createSelect(data.games.type, typeSelect)
createSelect(data.games.sport, sportSelect)

typeSelect.addEventListener('change', (e) => {
  const val = e.target.value
  const type = data.games.type.find(t => t.id == val)
  Array.from(sportSelect.querySelectorAll('option')).forEach(o => o.disabled = true)
  type.sportId.forEach(sId =>
    sportSelect.querySelector(`option[value="${sId}"]`).disabled = false)
})

sportSelect.addEventListener('change', (e) => {
  const val = e.target.value
  const sport = data.games.sport.find(s => s.id == val)
  Array.from(typeSelect.querySelectorAll('option')).forEach(o => o.disabled = true)
  sport.typeId.forEach(sId =>
    typeSelect.querySelector(`option[value="${sport.typeId}"]`).disabled = false)
})
<select id="type"></select>
<select id="sport"></select>

Could you please kindly help me to achieve the result of disabling the respective options from respective select box based on the conditions mentioned in the above mentioned scenario's in pure reactjs way?

For the comment given by @Andy, there is a reset option available in the select I am using, with close icon, so using that user can clear the select box and select the other dropdown option. This option is provided under allowClear in the antd select . Kindly please see the select box that I have in the above codesandbox, it has clear icon in the last.

Hello World
  • 94
  • 1
  • 6
  • 23
  • This kind of goes against how dependent selects are meant to work. If I select chess, for example, that would disable the outdoor option in the first dropdown so how are you meant to select it to update the second select with outdoor options? From a UX point-of-view it doesn't make much sense. It might make more sense to have [a select with checkboxes](https://stackoverflow.com/questions/17714705/how-to-use-checkbox-inside-select-option). – Andy Oct 13 '22 at 12:17
  • @Andy, If you hover at the select box, there you see a **close icon** https://i.stack.imgur.com/CA2GO.png at the right which I have included as an image in the question as well. So using this close icon, user can reset any select box. – Hello World Oct 13 '22 at 12:33
  • So user can reset any dropdown anytime using this close icon. – Hello World Oct 13 '22 at 12:40

2 Answers2

2

Here's what I have as a working solution with my understanding of your question. You want dynamic options that can easily validate against other dynamic options. It's about the best I could come up with that wasn't completely unmaintainable. It's about 98% dynamic but for the validation purposes some properties do need to be defined.

Example:

Setup the interfaces and types

interface IState { // <-- need to be known
  type: number;
  sport: number;
}

interface IOption {
  id: number;
  value: string;
  valid: Record<keyof IState, number[]>;
}

type Valid = "sport" & "type"; // <-- this needs to be known

interface Data {
  games: {
    [key: string]: Array<Record<Valid, IOption[]>>;
  };
}

Data

const data: Data = {
  games: {
    type: [
      { id: 1, value: "Indoor", valid: { sport: [2] } },
      { id: 2, value: "Outdoor", valid: { sport: [1, 3] } }
    ],
    sport: [
      { id: 1, value: "Tennis", valid: { type: [2] } },
      { id: 2, value: "Chess", valid: { type: [1] } },
      { id: 3, value: "Football", valid: { type: [2] } }
    ],
  }
};

Create component state to hold the selected option values. These should match the known selection types in the data. The idea here is that we are converting the select inputs to now be controlled inputs so we can validate options against selected state.

export default function App() {
  const [state, setState] = React.useState<IState>({
    type: -1,
    sport: -1,
    category: -1
  });

  const changeHandler = (key: keyof IState) => (value: number) => {
    setState((state) => ({
      ...state,
      [key]: value
    }));
  };

This is the meat of the addition. Validates options against currently selected state values according to the data configuration. Looks through each option's valid object and compares against current selected state. Returns if a current option is a valid selectable option or not.

  const isValid = (key: keyof IState, option: IOption) => {
    const { valid } = option;

    return (Object.entries(valid) as [[keyof IState, number[]]]).every(
      ([validKey, validValues]) => {
        const selectedValue = state[validKey];
        if (!selectedValue || selectedValue === -1) return true;

        return validValues.includes(state[validKey]);
      }
    );
  };

  return (
    <>
      <br />
      {(Object.entries(data.games) as [[keyof IState, IOption[]]]).map(
        ([key, options]) => {
          return (
            <div className="wrapper" key={key}>
              <h4>{key}</h4>
              <Select
                value={state[key] || -1}
                onChange={changeHandler(key)}
                allowClear
              >
                <Option disabled value={-1}>
                  Select {key}
                </Option>
                {options.map((option) => (
                  <Option
                    key={option.id}
                    value={option.id}
                    disabled={!isValid(key, option)} // if not valid, then disable
                  >
                    {option.value}
                  </Option>
                ))}
              </Select>
              <br />
            </div>
          );
        }
      )}
    </>
  );
}

Edit disable-dependent-dropdown-option-in-reactjs

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • @HelloWorld Yeah, mixing in a third (*or more*) input was the next case beyond two I thought I had finally resolved. Switching from `.some` to `.every` looks like it might've resolved that issue. Can you check the sandbox again? – Drew Reese Oct 14 '22 at 02:16
  • Yes bro, you are right, I have more additional dropdown's like your third `Category` one. Yes `.some` to `.every` solved that issue ;) . Thanks much. Another one query bro, Is it possible to reset the current dropdown and dependent dropdown values when we reset the current one? For eg.., Now in dropdown 1, user selects `Indoor` and in second `Chess` . And now user reset the second dropdown, and what happens right now is, If I reset the second dropdown then still only `Chess` is enabled. – Hello World Oct 14 '22 at 02:32
  • But it needs to completely reset this dropdown 2 as well. So what can be done now is that user can now select the `Football` in dropdown 2 and so in dropdown 1, the value of `Indoor` will be disabled. So whenever the user reset the dropdown using *close icon* , then the current dropdown and its dependent dropdown's needs to be reset as well. Then again when user selects any value from any dropdown, the dependent needs to be disabled but while reset, then the current and dependent needs to be disabled. – Hello World Oct 14 '22 at 02:32
  • Could you kindly check and help me in this one scenario alone bro? – Hello World Oct 14 '22 at 02:37
  • @HelloWorld Are you asking if resetting the sport input can also reset all the other select inputs? It looks like there exists an `onClear` handler for the `Select` component. – Drew Reese Oct 14 '22 at 02:39
  • Reese bro, On reset of the select dropdown, the current and dependent dropdown's (if value already selected in dependednt) alone needs to be reset. Let me explain you bit with scenario. Select `Indoor` from dropdown 1, then in dropdown 2, only chess is enabled and automatically the `Outdoor` option in dropdown 1 is disabled. So far everything is correct. But now user try to reset the dropdown 2 (sport) by using the clear icon and try to select `Football` in the same dropdown, but it is not possible right now and it is in disabled state. – Hello World Oct 14 '22 at 04:08
  • To put it in simple word's, Select `Indoor` in type and `Chess` in sport. Then click close icon in the sport dropdown. Now user should be able to select `Football` or `Tennis` which is not possible right now. – Hello World Oct 14 '22 at 04:13
  • @HelloWorld Ok, check sandbox code update. – Drew Reese Oct 14 '22 at 04:19
  • @Reese bro, Now you made change to reset all dropdown values, but I oly need to reset the related dropdown's. Again let me go with scenario basis, Select `Indoor` in type dropdown and select `fast` in category dropdown and try reset the `type` dropdown, here the `category` dropdown also got reset but there is no relation between both of these. So on reset of `type` dropdown, only the `type` dropdown and `sport` dropdown (if value is selected here) needs get reset as it both oly dependent on each other. – Hello World Oct 14 '22 at 04:31
  • @HelloWorld These extraneous requirements weren't in your question. The antd `Select` component only clears itself, so your code can respond to one of the states getting cleared (*antd doesn't say which one*) and either broadly clear them all, or run some custom logic to figure out which one was cleared and then run through the data options to figure out what also needs to be updated. TBH I'm still not quite following the use case here, what you are expecting to happen. I can play around with the codesandbox a bit more to see what I come up with. – Drew Reese Oct 14 '22 at 04:35
  • I apologies for not making it clear for you in question bro. – Hello World Oct 14 '22 at 04:52
  • And now only one last question bro related to this current solution, remaining questions I will post down in a separate question later. If one of the option dropdown doesn't have any rules then how to handle it? Consider that `valid` is removed from category dropdown option `fast` then I tried to check only if the object has valid property then consider the logic and you can look at my try here https://codesandbox.io/s/disable-dependent-dropdown-option-in-reactjs-forked-81lgdv but it makes the `fast` option under category dropdown by default on component render itself. – Hello World Oct 14 '22 at 05:06
  • Drew Reese bro, I have accepted the answer and awarded the bounty for you as this solution almost solve my problem. But I would kindly request you to check and update on the last two queries *(Reset only related dropdown (s) and handling the option that doesn't have `valid` property in the object)* that I raised in the last comments. But I whole heartedly appreciate your outstanding solution considering my struggle well. – Hello World Oct 14 '22 at 05:53
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248791/discussion-between-drew-reese-and-hello-world). – Drew Reese Oct 14 '22 at 05:59
0

https://codesandbox.io/s/react-typescript-forked-gt7gvy?file=/src/App.tsx

I added keeping state of chosen values in each select and conditional disabling of options in the select.

import "antd/dist/antd.min.css";
import { Select } from "antd";
import * as React from "react";
import "./styles.css";

const { Option } = Select;

const data = {
  games: {
    type: [
      { id: 1, value: "Indoor", sportId: [2] },
      { id: 2, value: "Outdoor", sportId: [1, 3] }
    ],
    sport: [
      { id: 1, value: "Tennis", typeId: [2] },
      { id: 2, value: "Chess", typeId: [1] },
      { id: 3, value: "Football", typeId: [2] }
    ]
  }
};

export default function App() {
  const [category, setCategory] = React.useState(null);
  const [sport, setSport] = React.useState(null);

  const handleChange = (value: any, index: number) => {
    console.log(value);
    const valueToSet = value.startsWith("select") ? null : value;
    if (index === 0) {
      setCategory(valueToSet);
    } else if (index === 1) {
      setSport(valueToSet);
    }
  };

  return (
    <>
      <br />
      {Object.entries(data.games).map((item, index) => {
        return (
          <div className="wrapper" key={index}>
            <h4> {item[0]} </h4>
            <Select
              defaultValue="selectType"
              onChange={(value) => handleChange(value, index)}
              allowClear
            >
              <Option value="selectType"> Select {item[0]} </Option>
              {item[1].map((option, j) => (
                <Option
                  key={j}
                  value={option.value}
                  disabled={
                    'typeId' in option && // index === 1 or just belong to sports
                    category &&
                    data.games.type.find((x) => x.value === category)?.id !==
                      option.typeId[0]
                  }
                >
                  {option.value}
                </Option>
              ))}
            </Select>
            <br />
          </div>
        );
      })}
    </>
  );
}
Vulwsztyn
  • 2,140
  • 1
  • 12
  • 20
  • This is not the expected result as I am unable to see vice versa scenario.. Like selecting the option first in dropdown 2 (`Chess`) and then in dropdown 1 (`Outdoor`) is **not disabled** . Also clear select gives error as `Cannot read properties of undefined (reading 'startsWith')` – Hello World Oct 13 '22 at 06:03
  • Also If I add some additional properties like `type` , `sport`, `...`, `...`, `...` , so on.. Then for every time I am in the need to create state for each and also for disable, need to add condition for every property. – Hello World Oct 13 '22 at 09:35
  • Also on click the clear icon in each select, it needs to reset to first option `i.e..,` , `select type` or `select sport` . – Hello World Oct 13 '22 at 11:38
  • What "clear icon"? – Vulwsztyn Oct 14 '22 at 07:59