Search code examples
javascriptreactjsselectdynamic-programmingantd

Disable dependent dropdown option in Reactjs


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.


Solution

  • 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