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 ofChess
needs to be enabled and others needs to be disabled.
-> User selects
Outdoor
from dropdown 1, then in dropdown 2 only the value ofTennis
andFootball
needs to be enabled and option Chess needs to be disabled.
Vice versa:
-> User selects
Chess
from dropdown 2, then in dropdown 1 only the value ofIndoor
needs to be enabled and others needs to be disabled.
-> User selects
Tennis
orFootball
from dropdown 2, then in dropdown 1 only the value ofOutdoor
needs to be enabled and others needs to be disabled.
Here we provide option of allowClear so that user can reset their selection in any select box selection (the close icon) 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:
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.
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>
);
}
)}
</>
);
}