In React-Select V2, we're able to create option groups by passing an options param like so:
options = [
{ label: 'Group', options: [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' }
]}
]
I need to be able to go another layer deep, something like:
options = [
{ label: 'Group', options: [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', options: [
{ label: 'Option 2-a', value: '2a' },
{ label: 'Option 2-b', value: '2b' },
]}
]}
]
Which would display the options "Option 2-a" and "Option 2-b" in a group under "Option 2". The approach above doesn't work out of the box, so I want to know if there's a way to create nested groups in React-Select V2.
For anyone who comes here with a similar need, I implemented this recursive work-around.
const renderNestedOption = (props, label, nestedOptions) => {
const {
cx,
getStyles,
innerProps,
selectOption,
} = props;
// Will be applied to nested optgroup headers
const nestedLabelClassName = cx(
css(getStyles('groupHeading', props)),
{ option: true },
'nested-optgroup-label',
);
return (
<div className="nested-optgroup">
<div className={nestedLabelClassName}>
{label}
</div>
{nestedOptions.map((nestedOption) => {
if (nestedOption.options) {
// Read below
// Read above
return renderNestedOption(props, nestedOption.label, nestedOption.options);
}
const nestedInnerProps = innerProps;
nestedInnerProps.onClick = () => selectOption(nestedOption);
return (
<div className="nested-optgroup-option" key={nestedOption.value}>
<components.Option {...props} innerProps={nestedInnerProps}>
{nestedOption.label}
</components.Option>
</div>
);
})}
</div>
);
};
const Option = (props) => {
const {
children,
data,
} = props;
const nestedOptions = data.options;
if (nestedOptions) {
const label = data.label;
return renderNestedOption(props, label, nestedOptions);
}
return (
<components.Option {...props}>
{children}
</components.Option>
);
};
Then in your select component, replace the Option component with the custom Option component we just created.
EDIT
There's an open pull request to support this functionality:
https://github.com/JedWatson/react-select/pull/2750
Related
I have an excersise app which I am trying to select exercises that are to be recorded for a workout. For what I have so far, I generate a list which can select a muscle category which then displays a list of excersises to be added to the workout. I would like the the options array of a specific muscle category to be generated from a seperate .js file of said category (AbsExerciseList.js)
//AbsExerciseList.js
const abList = [
{ id: '1', label: 'Crunches' },
{ id: '2', label: 'Leg Raises' }
]
export default abList
I then have the supOptions property - which is the list of exercises generated after the category is selected - appear. How would I take the object from AbsExerciseList.js and insert it to the subOptions object/array (specifically for the id: '1' , label: 'abs') element?
I would like to do the same for all other muscle categories as well.
//New WorkourtList.js
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, FlatList } from 'react-native';
import abList from './exercises/AbsExerciseList';
const MyDropDown = () => {
const [selectedOption, setSelectedOption] = useState(null);
const [showOptions, setShowOptions] = useState(false);
const [options, setOptions] = useState([
{
id: '1',
label: 'Abs',
subOptions: [
//Place abList from AbsExerciseList.js here
]
},
{
id: '2',
label: 'Biceps',
subOptions: [
{ id: '1', label: 'Preacher Curl' },
{ id: '2', label: 'EZ-Bar Curl' },
{ id: '3', label: 'Alternating Dumbell Curl' }
]
}
//... rest of muscle categories not listed
]);
const handleOptionSelect = (option) => {
setSelectedOption(option);
setShowOptions(false);
};
const renderOption = ({ item }) => (
<TouchableOpacity style={{ padding: 10 }} onPress={() => handleOptionSelect(item)}>
<Text>{item.label}</Text>
</TouchableOpacity>
);
const renderSubOption = ({ item }) => (
<TouchableOpacity style={{ padding: 10 }}>
<Text>{item.label}</Text>
</TouchableOpacity>
);
return (
<View>
<TouchableOpacity onPress={() => setShowOptions(!showOptions)}>
<Text>{selectedOption ? selectedOption.label : 'Select a Category'}</Text>
</TouchableOpacity>
{showOptions && (
<FlatList
data={options}
renderItem={renderOption}
keyExtractor={(item) => item.id}
/>
)}
{selectedOption && (
<FlatList
data={selectedOption.subOptions}
renderItem={renderSubOption}
keyExtractor={(item) => item.id}
/>
)}
</View>
);
};
export default MyDropDown;
I have tried using the map function within the useState() however i am met with, "Warning: Each child in a list should have a unique "key" prop."
I am not sure if I need to create a seperate function outside of useState() or use a different React hook.
If you save all sub Options seperate, then it could look like this:
const abList = [
{ id: "1", label: "Crunches" },
{ id: "2", label: "Leg Raises" }
];
const bicepsList = [
{ id: "1", label: "Preacher Curl" },
{ id: "2", label: "EZ-Bar Curl" },
{ id: "3", label: "Alternating Dumbell Curl" }
];
Then in your functional component, since you are not updating your initial "options" state, you can just alter your "selectedOption" state and append sub Otions into respective array.
Just alter the select handler like this:
const handleOptionSelect = (option) => {
switch (option.id) {
case "1": {
setSelectedOption({
...option,
subOptions: option.subOptions.concat(abList)
});
break;
}
case "2": {
setSelectedOption({
...option,
subOptions: option.subOptions.concat(bicepsList)
});
break;
}
default: {
setSelectedOption({
...option,
subOptions: []
});
break;
}
}
setShowOptions(false);
};
Benefit of option.subOptions.concat(anyList) is you can have default exercises from "options" state already set and append more exercies.
I'm trying to render menu values from a JSON. Considering that this is a multilevel menu, I'm trying to do a nested map to render the full menu.
const menu = {
data:[
{
title: "Home",
child: [
{
title: "SubLevel1",
child: {
title: "SubSubLevel1"
}
},
{
title: "SubLevel2",
child: [
{title: "SubSubLevel1"},
{title: "SubSubLevel2"}
]
}
]
},
{
title: "About",
},
{
title: "Contact",
}
]}
And here is the part when I use the map function :
const MenuNavigation = () => {
return (
{menu.data.map((item) => (
<div>
<ul>{item.title}</ul>
{item.map((sub, id) =>
<li>{sub.child[id].title}</li>
)}
</div>
))}
)
};
I managed to render main level for the menu (Home, About, Contact), but how can I print sublevel & subsublevel values?
Another question: Is there a way to map recursively a tree structure?
Try below:
The menu should be like this. Your menu had one issue in child of "SubLevel1" not defined inside an array.
const menu = {
data: [
{
title: "Home",
child: [
{
title: "SubLevel1",
child: [{
title: "SubSubLevel1",
}],
},
{
title: "SubLevel2",
child: [{ title: "SubSubLevel1" }, { title: "SubSubLevel2" }],
},
],
},
{
title: "About",
},
{
title: "Contact",
},
],
};
Render it recursively like below. I have added a margin-left to see the levels properly.
const renderMenu = (menu) => {
return menu.map((item) => (
<div style={{ marginLeft: '25px' }}>
{item.title}
{item.child && renderMenu(item.child)}
</div>
))
}
return <div>{renderMenu(menu.data)}</div>;
Good afternoon, I rarely write here. But now I really can't understand.
I am using React Select to display select. In the onChange attribute, I pass a function that forms the object and writes it to UseStat. But then I try to find an object using the find and
take an array of values from it.
const [selectedSpecificationValues, setSelectedSpecificationValues] = useState([])
const setSelectedSpecificationValuesHandler = (e, s) => {
const maybeSelectedSpecification = selectedSpecificationValues.find(
ss => ss._id === s._id
)
const objForWrite = {
_id: s._id,
name: s.name,
values: e,
}
if (maybeSelectedSpecification) {
const index = selectedSpecificationValues.indexOf(
maybeSelectedSpecification
)
let newArr = [...selectedSpecificationValues]
newArr[index] = objForWrite
setSelectedSpecificationValues(newArr)
} else {
setSelectedSpecificationValues([
...selectedSpecificationValues,
objForWrite,
])
}
}
const ssTestVal = Id => {
let result = []
if (selectedSpecificationValues.length > 0) {
const foundItem = selectedSpecificationValues.find(i => i._id === Id)
console.log(Id, foundItem)
if (foundItem) {
result = foundItem.values
}
}
return result
}
/* specifications = [
{
values: [
{
value: 0,
label: '480 min',
},
{
value: 1,
label: '120 min',
},
],
_id: '5fe74eae07905e53ebf263ec',
name: 'Duration',
slug: 'duration',
createdAt: '2020-12-26T14:54:38.362Z',
updatedAt: '2020-12-29T08:37:18.962Z',
__v: 1,
},
{
values: [
{
value: 0,
label: 'Photobook',
},
{
value: 1,
label: 'Photocard',
},
{
value: 2,
label: 'Album',
},
{
value: 3,
label: 'DVD',
},
{
value: 4,
label: 'Stickers',
},
{
value: 5,
label: 'CD',
},
],
_id: '5fe74e9107905e53ebf263eb',
name: 'Includes',
slug: 'includes',
createdAt: '2020-12-26T14:54:09.267Z',
updatedAt: '2020-12-26T16:10:16.283Z',
__v: 9,
},
] */
{
specifications &&
specifications.map((s, idx) => (
<Select
classNamePrefix='select2-selection'
options={s.values}
value={() => ssTestVal(s._id)}
onChange={e => setSelectedSpecificationValuesHandler(e, s)}
isMulti
/>
))
}
It is also important to understand that I loop a lot of selections in order to select different characteristics and their values.
I will be glad to help!
https://codesandbox.io/s/serverless-night-kez18?file=/src/App.js
Looks like minor issue with how you were computing the value for the sub-select inputs. You were defining it as though it were a callback.
<Select
classNamePrefix="select2-selection"
options={s.values}
value={() => ssTestVal(s._id)} // <-- not correct, not a callabck
onChange={(e) => setSelectedSpecificationValuesHandler(e, s)}
isMulti
/>
It should just be immediately invoked to compute and return an input's value.
<Select
classNamePrefix="select2-selection"
options={s.values}
value={ssTestVal(s._id)} // <-- invoke immediately for return value
onChange={(e) => setSelectedSpecificationValuesHandler(e, s)}
isMulti
/>
I am using react-select to store multiple elements and am using the map function to display elements which is working fine. But when I am using the same element in another class to display in a list element it shows a blank.
Here is the code where I am displaying the multiple options.
const Departments = [
{ label: "OneIT", value: "OneIT" },
{ label: "HR", value: "HR" },
{ label: "Vigilance", value: "Vigilance" },
{ label: "Ethics", value: "Ethics" },
{ label: "Corporate Services", value: "Corporate Services" },
{ label: "Legal", value: "Legal" },
{ label: "Sports", value: "Sports" },
{ label: "TQM", value: "TQM" },
{ label: "Iron Making", value: "Iron Making" },
{ label: "TMH", value: "TMH" }
];
class MultiSelect2 extends Component {
state = {
selectedOptions: []
};
handleChangeField = selectedOptions => {
this.setState({ selectedOptions });
};
render() {
const { selectedOption } = this.state;
return (
<div className="container">
<div className="row">
<div className="col-md-2"></div>
<div className="col-md-8">
<span>Select Department</span>
<Select
value={selectedOption}
options={Departments}
onChange={this.handleChangeField}
isMulti
/>
{this.state.selectedOptions.map(o => (
<p>{o.value}</p>
))}
</div>
<div className="col-md-4"></div>
</div>
</div>
);
}
}
I am trying to display this in another class in the list item but it is not showing.
export class Confirm extends Component {
state = {
selectedOptions: []
};
render() {
const {
values: {selectedOptions
}
} = this.props;
return (
<List>
<ListItemText primary="Departments" secondary={selectedOptions} />
</List>
so I have the following component that is a dropdown list created using react-select.
import React from 'react'
import Select from 'react-select';
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
];
class MealsFilters extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOption: null,
};
}
handleChange = (selectedOption) => {
this.setState({ selectedOption });
console.log(`Option selected:`, selectedOption);
}
render() {
const { selectedOption } = this.state;
return (
<div className="container my-3">
<div className="row">
<div className="col-lg-4 col-md-6 col-sm-8">
<Select
isMulti
isSearchable
placeholder={"catégories"}
value={selectedOption}
onChange={this.handleChange}
options={options}
/>
</div>
</div>
</div>
)
}
}
export default MealsFilters;
the options variable is the default one from the docs. I actually need to replace its values by each meal category available.
To do so, as you can see, I need to create an array of objects with a value and a label.
this component accesses meal categories through props called meals that are like so:
console.log(this.props.meals);
=> [{
id: 0,
name: spaghettis,
category: italian,
price: 5.99},
{
id: 1,
name: hamburger,
category: american,
price: 7.99},
{
etc.
}, {}]
How can I take advantage of this.props.meals to get my options array of objects ?
EDIT: multiple meals can have the same category, and I need each category to only appear once in the options.
Map over your this.props.meals array, and create the needed options array,
<Select
isMulti
isSearchable
placeholder={"catégories"}
value={selectedOption}
onChange={this.handleChange}
options={this.props.meal.map(item=>({value: item.id, label: item.name}))}
/>
You could do something like this:
options={this.props.meals.map(
({id, name})=>({value:id,label:name})
)}
You could also use redux connect to create a container that will map the data to dropdown values for you
You can merge the data by category in the following way:
var items = [
{
id: 0,
name: 'spaghettis',
category: 'italian',
price: 5.99,
},
{
id: 1,
name: 'hamburger',
category: 'american',
price: 7.99,
},
{
id: 2,
name: 'other hamburger',
category: 'american',
price: 7.99,
},
];
console.log(
[
...items.reduce(
(result, item) => (
result.get(item.category)
? result.get(item.category).push(item.id)
: result.set(item.category, [item.id]),
result
),
new Map(),
),
].map(([label, value]) => ({ label, value })),
);
In the component it'll look like this:
options={[
...this.props.meals.reduce(
(result, item) => (
result.get(item.category)
? result.get(item.category).push(item.id)
: result.set(item.category, [item.id]),
result
),
new Map(),
),
].map(([label, value]) => ({ label, value }))}
You only need the "name" property so when you map through meals, simply retrieve it. Then upper case the first letter.
const meals = [{
id: 0,
name: "spaghettis",
category: "italian",
price: 5.99
},
{
id: 1,
name: "hamburger",
category: "american",
price: 7.99
}
]
const result = meals.map(({name}) => ({
label: `${name[0].toUpperCase()}${name.slice(1)}`,
value: name
}))
console.log(result);
You can use getOptionLabel and getOptionValue props.
<Select
options={this.props.meals},
getOptionLabel={m => m.name}
getOptionValue={m => m.id} />
https://react-select.com/props
getOptionLabel generic = (option) => string
Resolves option data to a string to be displayed as the label by components
getOptionValue generic = (option) => string
Resolves option data to a string to compare options and specify value attributes