I made a component containing two dropdown lists. The options in the second dropdown menu is supposed to be filtered depending on the selected option from the first dropdown menu.
Now, I want to map a filtered array that is stored in a const similary to the way i map options1:
render() {
const options1 = [
{value: 'one', label: 'One'},
{value: 'two', label: 'Two'}
];
const options2 = [
{value: 'one-a', label: 'One A', link: 'one'},
{value: 'one-b', label: 'One B', link: 'one'},
{value: 'two-a', label: 'Two A', link: 'two'},
{value: 'two-b', label: 'Two B', link: 'two'}
];
const filteredOptions = options2.filter(o => o.link === this.state.selectedOption.value);
return (
<div style={style}>
<div>
<label>Select one</label>
<select
value={this.state.selectedOption.value}
onChange={this.handleChange1}
>
{options1.map(tag => <option>{tag.value}</option>)}
</select>
</div>
<div>
<label>Then the other</label>
<select
value={this.state.selectedOption2.value}
onChange={this.handleChange2}
>
{filteredOptions.map(tag => <option>{tag.value}</option>)}
</select>
</div>
</div>
)
}
The first mapping of options1 works just fine. However, my select tag gets rendered empty for the mapping of filteredOptions.
I have no idea why it won't work. Anyone happen to have an idea?
Full code: https://www.codepile.net/pile/evNqergA
Here is a working example for what you're trying to do.
import React, { Component } from "react";
const options1 = [
{ value: "one", label: "One" },
{ value: "two", label: "Two" }
];
const options2 = [
{ value: "one-a", label: "One A", link: "one" },
{ value: "one-b", label: "One B", link: "one" },
{ value: "two-a", label: "Two A", link: "two" },
{ value: "two-b", label: "Two B", link: "two" }
];
export default class SelectsComponent extends Component {
handleChange1 = e => {
console.log(e);
this.setState({
selectedOption: { value: e.target.value }
});
};
handleChange2 = e => {
this.setState({
selectedOption2: { value: e.target.value }
});
};
constructor(props) {
super(props);
this.state = {
selectedOption: { value: "one" },
selectedOption2: { value: "one-a" }
};
}
render() {
const filteredOptions = options2.filter(
o => o.link === this.state.selectedOption.value
);
return (
<div>
<div>
<label>Select one</label>
<select
value={this.state.selectedOption.value}
onChange={this.handleChange1}
>
{options1.map(tag => (
<option>{tag.value}</option>
))}
</select>
</div>
<div>
<label>Then the other</label>
<select
value={this.state.selectedOption2.value}
onChange={this.handleChange2}
>
{filteredOptions.map(tag => (
<option>{tag.value}</option>
))}
</select>
</div>
</div>
);
}
}
In your scenario filteredOptions would be an empty Array.
The check for o.link === this.state.selectedOption.value is doing something wrong.
Check the value of this.state.selectedOption.value, this is not set correctly.
The best way to do this wouldn't be inside of the render method.
1) move your arrays into state or other instance members
2) make sure to only trigger the sorting once
this.setState(lastState => ({
...lastState,
options2: lastState.options2.filter(yourFilterFn)
}))
3) map the filtered array into JSX inside of your render method
Side-note: this uses immutable setState (which I gather is important given you create a new filtered array from the options2 in your example). If you want to follow an even more functional pattern, you can do the filtering inside of your render method (although I don't recommend it). If you decided to filter inside of your render method, consider using a memoization technique from React 16.7 (which is currently in Alpha).
Related
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.
-> 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.
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.
-> 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.
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>
);
}
)}
</>
);
}
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>
);
})}
</>
);
}
I have a set of select menus and I am trying to change a value when I select an option using onChange={updateValue} event. When I first select an option, the value is not being updated in the select menu.
It only changes the second time I try to choose an option. Not sure what I am doing wrong.
Edit: I did some more research (OnChange event using React JS for drop down) and I believe I need the value of the select to be updated as well, using setState. I cant figure out how to do it without having a variable for each value and set the state again.
let selectMenus = [
{
id: 'id1',
name: 'name1',
label: 'label1',
value: '0',
options: [
{
text: 'All ages',
value: '0',
},
{
text: '35 - 37 yrs',
value: '1',
},
],
buttonLabel: 'Refresh',
},
{
id: 'id2',
name: 'name2',
label: 'label2',
value: '1',
options: [
{
text: 'All ages',
value: '0',
},
{
text: '45 - 50 yrs',
value: '1',
},
],
buttonLabel: 'Refresh',
},
];
const [url, setUrl] = useState('http://localhost:5000/selectDropdowns1');
const updateValue = () => {
setUrl('http://localhost:5000/selectDropdowns2');
};
<form>
{selectMenus.map((select) => (
<div key={select.id} className='select-container'>
<label htmlFor={select.id}>{select.label}</label>
<select id={select.id} name={select.name} value={select.value} onChange={updateValue}>
{select.options.map((option) => (
<option value={option.value} key={uuid()}>
{option.text}
</option>
))}
</select>
<button>{select.buttonLabel}</button>
</div>
))}
</form>;
The problem is that when you provide onChange prop to select component it become a controlled component.
For more information: React Docs - Forms #controlled components
When you dealing with controlled components you must provide a value to it and when onChange triggerd it should update that value to work properly. Since you did not provide the full code, I imagine you have an array of select menus and options attached to it.
So in this case every select component should have own onChange method and own value to work properly. To achive this we should create another component for only Select Options. Like this;
function SelectComponent({ optionList, onSelected }) {
const [value, setValue] = useState();
const updateValue = ({ target }) => {
setValue(target.value);
if (onSelected) onSelected(target.value);
};
return (
<>
<label htmlFor={optionList.id}>{optionList.label}</label>
<select
id={optionList.id}
name={optionList.name}
value={value}
onChange={updateValue}
>
{optionList.options.map((option) => (
<option value={option.value} key={uuid()}>
{option.text}
</option>
))}
</select>
<button>{optionList.buttonLabel}</button>
</>
);
}
This component accepts to props; optionList and onSelected
optionList is the list of options to render
onSelected is a method that we call when user select and option
On main component, we should change the select section with our select component with props optionList and onSelected
return (
<div>
{selectMenus.map((select) => (
<div key={select.id} className="select-container">
<SelectComponent optionList={select} onSelected={updateValue} />
</div>
))}
</div>
);
So overall code is like this:
import { useState } from "react";
import { v4 as uuid } from "uuid";
export default function App() {
const [url, setUrl] = useState();
const updateValue = (value) => {
setUrl(value);
};
const selectMenus = [
{
id: 1,
label: "Menu 1",
name: "menu1",
buttonLabel: "Menu 1",
options: [
{
text: "option 1",
value: "option1"
},
{
text: "option 2",
value: "option2"
},
{
text: "option 3",
value: "option3"
}
]
},
{
id: 2,
label: "Menu 2",
name: "menu2",
buttonLabel: "Menu 2",
options: [
{
text: "option 1",
value: "option1"
},
{
text: "option 2",
value: "option2"
},
{
text: "option 3",
value: "option3"
}
]
},
{
id: 3,
label: "Menu 3",
name: "menu3",
buttonLabel: "Menu 3",
options: [
{
text: "option 1",
value: "option1"
},
{
text: "option 2",
value: "option2"
},
{
text: "option 3",
value: "option3"
}
]
}
];
return (
<div className="App">
<h1>URL Value: {url}</h1>
{selectMenus.map((select) => (
<div key={select.id} className="select-container">
<SelectComponent optionList={select} onSelected={updateValue} />
</div>
))}
</div>
);
}
function SelectComponent({ optionList, onSelected }) {
const [value, setValue] = useState();
const updateValue = ({ target }) => {
setValue(target.value);
if (onSelected) onSelected(target.value);
};
return (
<>
<label htmlFor={optionList.id}>{optionList.label}</label>
<select
id={optionList.id}
name={optionList.name}
value={value}
onChange={updateValue}
>
{optionList.options.map((option) => (
<option value={option.value} key={uuid()}>
{option.text}
</option>
))}
</select>
<button>{optionList.buttonLabel}</button>
</>
);
}
Working example is overhere codesandbox
I have a window where a group of 3 HTML combo box(select box) are generated on every button click.
lets say category,subcategory and data. options in sub category and data are dynamically rendered based on its previous selected value.
i am storing the selected values as an array of object like
const [exerciseData, setExerciseData] = useState([[]]);
Sample data:
exerciseData : [
//first group
[
category:"",
subcategory:"",
name:""
],
//second group
[
category:"",
subcategory:"",
name:""
]
]
So literally first group of input can be indicated by exerciseData[0] and next by exerciseData[1].category etc..
The problem is i want there groups like to be reordered by the user like moving up 1 group or moving down by 1.So i need the select boxes to be a controlled element. But how can i set that?
I tried like this :
<select name="category" value={exerciseData[i].category} ... > {options} </select>
when i give like this the select box value cannot be changed.it always stays at its default value.
any suggestion will be appreciated
Thanks in advance
To make an input controlled, you need to manually handle the input value via state. In your example, the input value seems to be fixed/static.
The following snippet is an example of how to create a controlled select input in React.
Update:
Made a full demo snippet. Please let me know if that is what you was looking for.
const {useState, useCallback, Fragment} = React;
const categories = [
{
id: 'CAT.1',
data: [
{
id: 'CAT.1 SUB.A',
data: [
{id: 'CAT.1 SUB.A OPT.1'},
{id: 'CAT.1 SUB.A OPT.2'},
{id: 'CAT.1 SUB.A OPT.3'}
]
},
{
id: 'CAT.1 SUB.B',
data: [
{id: 'CAT.1 SUB.B OPT.1'},
{id: 'CAT.1 SUB.B OPT.2'},
{id: 'CAT.1 SUB.B OPT.3'}
]
}
]
},
{
id: 'CAT.2',
data: [
{
id: 'CAT.2 SUB.A',
data: [
{id: 'CAT.2 SUB.A OPT.1'},
{id: 'CAT.2 SUB.A OPT.2'},
{id: 'CAT.2 SUB.A OPT.3'}
]
},
{
id: 'CAT.2 SUB.B',
data: [
{id: 'CAT.2 SUB.B OPT.1'},
{id: 'CAT.2 SUB.B OPT.2'},
{id: 'CAT.2 SUB.B OPT.3'}
]
}
]
},
];
function Select(props) {
const {
name,
options = [],
onIndex
} = props;
const [value, setValue] = useState(options[0] && options[0].id);
const onChangeHandler = useCallback((e) => {
onIndex && onIndex(options.findIndex((item) => item.id === e.target.value));
setValue(e.target.value);
}, [onIndex, options]);
return (
<label>
{name}:
<select value={value} onChange={onChangeHandler}>
{
options.map((item) => {
const {id} = item;
return <option value={id}>{id}</option>;
})
}
</select>
</label>
);
}
function Field() {
const [catIndex, setCatIndex] = useState(0);
const [subIndex, setSubIndex] = useState(0);
return (
<div>
<Select name='Category' options={categories} onIndex={setCatIndex}/>
<Select name='Sub Category' options={categories[catIndex].data} onIndex={setSubIndex}/>
<Select name='Option' options={categories[catIndex].data[subIndex].data}/>
</div>
);
}
function App() {
const [fields, setFields] = useState(() => [<Field />]);
const addField = useCallback(() => {
setFields((prevFields) => {
const index = prevFields.lenght;
return [...prevFields, <Field key={index} />];
});
}, []);
return (
<Fragment>
<form>
{fields}
</form>
<button type='button' onClick={addField}>Add Field</button>
</Fragment>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
form {
display: flex;
flex-direction: column;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id='root'></div>
Please let me know if you have any doubt.
I'm creating an app where I need to store selected values in array nested in object (category below). State looks like:
state = {
data: {
user: "",
title: "",
text: "",
category: [], // should store values
},
updateNoteId: null,
}
In my render() I have following form:
<form onSubmit={this.submitNote}>
<Select
name="category"
value={this.state.data.category}
options={options}
onChange={this.handleMultiChange}
multi
/>
<input type="submit" value="Save" />
</form>
Options are:
const options = [
{ value: 1, label: 'one' },
{ value: 2, label: 'two' },
{ value: 3, label: 'three' }
]
So the question is how this.handleMultiChange function should looks like to work. Category[] need to keep all values selected in Select which is react-select component (eg. it should be category = [1,3], when 'one' and 'three' were chosen). I tried many combination but none of them worked so far. I prefer to use ES6 without any external libraries/helpers to do that.
handleMultiChange(selectedOptions) {
this.setState({
data: {
...this.state.data,
categories: selectedOptions
}
})
}
<el-tab-pane
v-for="(item, index) in systemOptionsHeaderList"
:key="index"
:name="item.value"
:lazy="true"
>
<span
slot="label"
>
{{item.label}}
<tips
v-if="formHasModify"
content="本页数据暂未保存"
/>
</span>
</el-tab-pane>
// The systemOptionsHeaderList:
systemOptionsHeaderList:
[{
label: '拨打策略',
value: '1'
}, {
label: '资源设置',
value: '2'
}, {
label: '打断设置',
value: '3'
}, {
label: '静默设置',
value: '4'
}, {
label: '分句设置',
value: '5'
}]
I use Element-ui "el-tab-pane" component
Now, I want use "formHasModify" (a Boolean Data) to control the "Tips"
component show or hidden
However, When I change the "formHasModify" the view can't update immediately
I solve this problem:
el-tab-pane
ref="elTabs" // lock the ref
// When The data change, force update the component
let childrenRefs = this.$refs.elTabs.$children
this.$nextTick(() => {
childrenRefs.forEach(child => child.$forceUpdate())
})