How to change React Dropdown's title? - javascript

I'm creating a custom dropdown list, where a button (Trigger) plays the role as the dropdown's trigger. Here I'm trying to change the dropdown title into the name of any selected options. To do this, I store the new selected value in selectedOption and use them to replace the title. However receive an error of: Cannot read property 'label' of undefined.
How to resolve and make the dropdown works?
Really appreciate any enlightenment! Thank you
const Dropdown = props => {
const { onChange, label, disabled } = props;
const options = [
{ value: '0', label: 'All Flavour' },
{ value: '1', label: 'Strawberry' },
{ value: '2', label: 'Rum Raisin' },
{ value: '3', label: 'Hazelnut' },
{ value: '4', label: 'Chocochip' },
{ value: '5', label: 'Coffee' },
];
const [open, setOpen] = useState(false);
const handleTriggerClick = useCallback(() => setOpen(prev => !prev), []);
const handleChange = useCallback(
newValue => {
if (!disabled) {
onChange(newValue);
setOpen(false);
}
},
[onChange]
);
const selectedOption = options.find(option => option.label === label);
const displayMenu = open && !disabled;
return (
<>
<Container>
<OutletIcon />
<Trigger
disabled={disabled}
title={selectedOption.label || ''}
onClick={handleTriggerClick}
>
<TriggerText>{selectedOption.label || ''}</TriggerText>
<SortIcon />
</Trigger>
<DropdownMenu isDisplayed={displayMenu}>
{options.map(option => {
const isSelected = option.label === label;
const otherProps = {};
if (!isSelected) {
otherProps.onClick = () => handleChange(option.label);
}
return (
<DropdownMenuItem
key={option.value}
title={option.label}
selected={isSelected}
{...otherProps}
>
<DropdownMenuItemText onClick={handleTriggerClick}>
{option.label}
</DropdownMenuItemText>
<GreenCheckIcon />
</DropdownMenuItem>
);
})}
</DropdownMenu>
</Container>
</>
);
};
Hereby is the props declaration
Dropdown.defaultProps = {
disabled: false,
onChange: () => {},
label: '',
};
Dropdown.propTypes = {
disabled: PropTypes.bool,
onChange: PropTypes.func,
label: PropTypes.string,
};

Related

Adding handlers and state dynamically in React

I need to render plenty of inputs for every name of the array, but the problem is creating dynamically useState const and onChange handler for every rendered input.
I try to create handleChange key for every item in an array and pass it to input onChange but got the "String Instead Fuction" error. How to resolve the problem in another way and also avoid code duplication?
export const myArr = [
{ name: "Crist", data: "crist", color: "lightblue", handleChange: "cristHandleChange"},
{ name: "Ruby", data: "ruby", color: "lightpink", handleChange: "rubyHandleChange"},
{ name: "Diamond", data: "diamond", color: "white", handleChange: "diamondHandleChange"},
];
export const myComponent = () => {
const [cristQuantity, setCristQuantity] = useState(0);
const [rubyQuantity, setRubyQuantity] = useState(0);
const [diamondQuantity, setDiamondQuantity] = useState(0);
function cristHandleChange(event) {
setCristQuantity(event.target.value);
}
function rubyHandleChange(event) {
setRubyQuantity(event.target.value);
}
function diamondHandleChange(event) {
setDiamondQuantity(event.target.value);
}
return (
<>
{myArr
? myArr.map((item, index) => (
<div className="main-inputs__wrapper" key={`item${index}`}>
<label htmlFor={item.data}>{item.name}</label>
<input
type="number"
name={item.data}
style={{ background: item.color }}
onChange={item.handleChange} //???
/>
</div>
))
: null}
</>
);
};
You should create one handler for all inputs, and save values in a object with a key as item.data. Such way: {crist: 1, ruby: 3, diamond: 5}
export const myArr = [
{
name: "Crist",
data: "crist",
color: "lightblue"
},
{
name: "Ruby",
data: "ruby",
color: "lightpink"
},
{
name: "Diamond",
data: "diamond",
color: "white"
}
];
export function MyComponent() {
// Lazy initial state
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [quantities, setQuantities] = useState(() =>
myArr.reduce((initialQuantities, item) => {
initialQuantities[item.data] = 0;
return initialQuantities;
}, {})
);
// common handler for every onChange with computed property name
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names
const handleChange = (event) => {
setQuantities((prevQuantities) => ({
...prevQuantities,
[event.target.name]: event.target.value
}));
};
return (
<>
{Array.isArray(myArr)
? myArr.map((item, index) => (
<div className="main-inputs__wrapper" key={`item${index}`}>
<label htmlFor={item.data}>{item.name}</label>
<input
type="number"
name={item.data}
style={{ background: item.color }}
onChange={handleChange}
value={quantities[item.data] || ""}
/>
</div>
))
: null}
</>
);
}
I haven't actually tested this but this is something I would do which will get you on the way.
export const myArr = [
{
name: `Crist`,
data: `crist`,
color: `lightblue`,
handleChange: `cristHandleChange`,
},
{
name: `Ruby`,
data: `ruby`,
color: `lightpink`,
handleChange: `rubyHandleChange`,
},
{
name: `Diamond`,
data: `diamond`,
color: `white`,
handleChange: `diamondHandleChange`,
},
]
export const myComponent = () => {
const [quantities, setQuantities] = useState({
crist: 0,
ruby: 0,
diamond: 0,
})
const onChangeHandler = ({ name, value }) => {
setQuantities((prevState) => ({ ...prevState, [name]: value }))
}
return (
<>
{myArr.length > 0
? myArr.map(({ data, name, color }, index) => {
// if you need to do an update to push more items to the object for any dynamic reason
if (!quantities.name)
setQuantities((prevState) => ({ ...prevState, [name]: 0 }))
return (
<div className="main-inputs__wrapper" key={`item${index}`}>
<label htmlFor={data}>{name}</label>
<input
type="number"
name={name}
value={quantities.name}
style={{ background: color }}
onChange={(e) =>
onChangeHandler({ name, value: e.currentTarget.value })
}
/>
</div>
)
})
: null}
</>
)
}
You're passing a string (not a function) to onChange which is causing that problem.
To fix it, you can wrap all functions into another object
const onChangeFunctions = {
cristHandleChange: (event) => {
setCristQuantity(event.target.value);
},
rubyHandleChange: (event) => {
setRubyQuantity(event.target.value);
},
diamondHandleChange: (event) => {
setDiamondQuantity(event.target.value);
},
};
and call onChangeFunctions[item.handleChange] like below
export const myArr = [
{
name: "Crist",
data: "crist",
color: "lightblue",
handleChange: "cristHandleChange",
},
{
name: "Ruby",
data: "ruby",
color: "lightpink",
handleChange: "rubyHandleChange",
},
{
name: "Diamond",
data: "diamond",
color: "white",
handleChange: "diamondHandleChange",
},
];
export const myComponent = () => {
const [cristQuantity, setCristQuantity] = useState(0);
const [rubyQuantity, setRubyQuantity] = useState(0);
const [diamondQuantity, setDiamondQuantity] = useState(0);
const onChangeFunctions = {
cristHandleChange: (event) => {
setCristQuantity(event.target.value);
},
rubyHandleChange: (event) => {
setRubyQuantity(event.target.value);
},
diamondHandleChange: (event) => {
setDiamondQuantity(event.target.value);
},
};
return (
<>
{myArr
? myArr.map((item, index) => (
<div className="main-inputs__wrapper" key={`item${index}`}>
<label htmlFor={item.data}>{item.name}</label>
<input
type="number"
name={item.data}
style={{ background: item.color }}
onChange={onChangeFunctions[item.handleChange]}
/>
</div>
))
: null}
</>
);
};
Let me start by saying that you might want to use a library like React Hook Form for this, although, if this is the only form and you don't need all the fancy features (or additional bundle size), you can do it like this as well.
The first step is to store your form data in an Object. After this change, you can use a single useState({}) to store and read your form data and drastically simplifies your handlers.
For example:
export const myArr = [
{ name: "Crist", data: "crist", color: "lightblue"},
{ name: "Ruby", data: "ruby", color: "lightpink"},
{ name: "Diamond", data: "diamond", color: "white"},
];
// generate Object with data and initialValue or empty string
// e.g. `{ 'crist': '', 'ruby': '', 'diamond': '' }`
const getInitialFormValues = (arr) => Object.fromEntries(
arr.map(({ data, initialValue }) => [data, initialValue || ''])
);
export const MyComponent = () => {
const [formValues, setFormValues] = useState(getInitialFormValues(myArr));
function handleChange(event) {
// update the changed form value
setFormValues(current => ({
...current, // other form values
[event.target.name]: event.target.value // updated value
}));
}
return (
<>
{myArr
? myArr.map((item, index) => (
<div className="main-inputs__wrapper" key={`item${index}`}>
<label htmlFor={item.data}>{item.name}</label>
<input
type="number"
name={item.data}
style={{ background: item.color }}
onChange={handleChange}
value={formValues[item.data]}
/>
</div>
))
: null}
</>
);
};

REACT-SELECT defaultValue in CustomDropdown not working

I want the default value of my dropdown to be defaultValue={item.taste} (value from match.json) but it's not working... (go to /menu/Menu1 and Pea Soup)
import Select from "react-select";
export default function CustomDropdown({ style, options, defaultValue }) {
return (
<div style={style}>
<Select options={options} defaultValue={defaultValue} />
</div>
);
}
MenuItemDisplay:
export default function MenuItemDisplay() {
const { menuId, itemId } = useParams();
const { match } = JsonData;
const matchData = match.find((el) => el._id_menu === menuId)?._ids ?? [];
const item = matchData.find((el) => el._id === itemId);
const styles = {
select: {
width: "100%",
maxWidth: 150
}
};
const TASTE = [
{ label: "Good", value: "Good" },
{ label: "Medium", value: "Medium" },
{ label: "Bad", value: "Bad" }
];
...
return (
<>
<div className="TextStyle">
{"Taste "}
<CustomDropdown
style={styles.select}
options={TASTE}
defaultValue={item.taste}
//The default value is not working only if it's
//TASTE[0]
/>
</div>
...
</>
);
}
Here the link for the code
As defaultValue you need to pass one of the objects of the TASTE array. You can do this:
<CustomDropdown
style={styles.select}
options={TASTE}
defaultValue={TASTE.find(t => t.label === item.taste)}
/>

Better way to access property in React.useState and update it?

I have a simple component that renders a menu with items. What I am trying to do is have a value called isLoggedIn that accesses the value of the Italian Food item, change it's value to true and later hide the Italian Food item. Currenly my code works, the Italian Restaurant item gets hidden, but is there a better way to access the available property, change it based on a condition and to hide the element? Here is my code:
import React, { useState, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import {
Drawer,
DrawerContent,
DrawerItem,
} from '#progress/kendo-react-layout';
import { Button } from '#progress/kendo-react-buttons';
const CustomItem = (props) => {
const { visible, ...others } = props;
const [isLoggedIn, setIsLoggedIn] = React.useState(
props.available ? false : true
);
const arrowDir = props['data-expanded']
? 'k-i-arrow-chevron-down'
: 'k-i-arrow-chevron-right';
React.useEffect(() => {
setIsLoggedIn(props.available);
}, [props.available]);
return (
<React.Fragment>
{isLoggedIn === false ? null : (
<DrawerItem {...others}>
<span className={'k-icon ' + props.icon} />
<span className={'k-item-text'}>{props.text}</span>
{props['data-expanded'] !== undefined && (
<span
className={'k-icon ' + arrowDir}
style={{
position: 'absolute',
right: 10,
}}
/>
)}
</DrawerItem>
)}
</React.Fragment>
);
};
const DrawerContainer = (props) => {
const [drawerExpanded, setDrawerExpanded] = React.useState(true);
const [items, setItems] = React.useState([
{
text: 'Education',
icon: 'k-i-pencil',
id: 1,
selected: true,
route: '/',
},
{
separator: true,
},
{
text: 'Food',
icon: 'k-i-heart',
id: 2,
['data-expanded']: true,
route: '/food',
},
{
text: 'Japanese Food',
icon: 'k-i-minus',
id: 4,
parentId: 2,
route: '/food/japanese',
},
{
text: 'Italian Food',
icon: 'k-i-minus',
id: 5,
parentId: 2,
route: '/food/italian',
available: false,
},
{
separator: true,
},
{
text: 'Travel',
icon: 'k-i-globe-outline',
['data-expanded']: true,
id: 3,
route: '/travel',
},
{
text: 'Europe',
icon: 'k-i-minus',
id: 6,
parentId: 3,
route: '/travel/europe',
},
{
text: 'North America',
icon: 'k-i-minus',
id: 7,
parentId: 3,
route: '/travel/america',
},
]);
const handleClick = () => {
setDrawerExpanded(!drawerExpanded);
};
const onSelect = (ev) => {
const currentItem = ev.itemTarget.props;
const isParent = currentItem['data-expanded'] !== undefined;
const nextExpanded = !currentItem['data-expanded'];
const newData = items.map((item) => {
const {
selected,
['data-expanded']: currentExpanded,
id,
...others
} = item;
const isCurrentItem = currentItem.id === id;
return {
selected: isCurrentItem,
['data-expanded']:
isCurrentItem && isParent ? nextExpanded : currentExpanded,
id,
...others,
};
});
props.history.push(ev.itemTarget.props.route);
setItems(newData);
};
const data = items.map((item) => {
const { parentId, ...others } = item;
if (parentId !== undefined) {
const parent = items.find((parent) => parent.id === parentId);
return { ...others, visible: parent['data-expanded'] };
}
return item;
});
return (
<div>
<div className="custom-toolbar">
<Button icon="menu" look="flat" onClick={handleClick} />
<span className="title">Categories</span>
</div>
<Drawer
expanded={drawerExpanded}
mode="push"
width={180}
items={data}
item={CustomItem}
onSelect={onSelect}
>
<DrawerContent>{props.children}</DrawerContent>
</Drawer>
</div>
);
};
export default withRouter(DrawerContainer);
If I understood your request properly you want to calculate the isLoggedIn property based on props.available, right? If this is correct then you may just use the useMemo hook in the following way:
const CustomItem = (props) => {
const { visible, ...others } = props;
const isLoggedIn = React.useMemo(() => {
return !props.available
});
const arrowDir = props['data-expanded']
? 'k-i-arrow-chevron-down'
: 'k-i-arrow-chevron-right';
return (
<React.Fragment>
{isLoggedIn === false ? null : (
<DrawerItem {...others}>
<span className={'k-icon ' + props.icon} />
<span className={'k-item-text'}>{props.text}</span>
{props['data-expanded'] !== undefined && (
<span
className={'k-icon ' + arrowDir}
style={{
position: 'absolute',
right: 10,
}}
/>
)}
</DrawerItem>
)}
</React.Fragment>
);
};
Here the doc of the hook if you want to go deeper.

Multi React-Select not setting value

I am trying to create a searchable dropdown that allows users to select only 2 genres which are then added to genreName. I currently have it working but the values will not set or all the values set ?
I have it working with another set of code but I couldn't add the search feature for the material ui select.
Anyone have any idea how to fix this ?
const genres = [
{ value: 'acoustic', label: 'acoustic' },
{ value: 'afrobeat', label: 'afrobeat' },
{ value: 'alt-rock', label: 'alt-rock' },
{ value: 'alternative', label: 'alternative' },
{ value: 'brazil', label: 'brazil' },
{ value: 'breakbeat', label: 'breakbeat' },
]
const AddGenre = ({ }) => {
const [ariaFocusMessage, setAriaFocusMessage] = useState('');
const [isMenuOpen, setIsMenuOpen] = useState(false);
const onFocus = ({ focused, isDisabled }) => {
const msg = `You are currently focused on option ${focused.label}${isDisabled ? ', disabled' : ''
}`;
setAriaFocusMessage(msg);
return msg;
};
const onMenuOpen = () => setIsMenuOpen(true);
const onMenuClose = () => setIsMenuOpen(false);
//trying to set the values here
const [genreName, setGenreName] = useState([]);
const handleInputChange = (value, e) => {
if (e.action === 'input-change') {
setGenreName(value);
console.log(genreName)
}
}
return (
<div className="bodyComp">
<form>
<label style={style.label} id="aria-label" htmlFor="aria-example-input">
Select a Genre
</label>
<Select
isMulti
aria-labelledby="aria-label"
ariaLiveMessages={{
onFocus,
}}
onInputChange={handleInputChange}
options={genres}
//if I set the value as genres all values are set ?
value={genres}
inputId="aria-example-input"
name="aria-live-color"
onMenuOpen={onMenuOpen}
onMenuClose={onMenuClose}
/>
</form>
</div>
)
};
export default AddGenre

object object in antd input field

Any idea how to get rid of [object object] inside of antd input, it seems that inside map option i have < br /> which is causing this, how to have that inside map but not have it inside input ? inside map it will cause them to come under eachother thats reason i'm using it there. So any idea how to not have it in input ?
import "antd/dist/antd.css";
import { Button, AutoComplete } from "antd";
import { CloseOutlined } from "#ant-design/icons";
const EventsSection = () => {
const autoControl = React.createRef();
const defaultOptions = [
{ value: "1", text: "Nicholas" },
{ value: "2", text: "Alex" },
{ value: "3", text: "Putin" },
{ value: "4", text: "Biden" },
{ value: "5", text: "Peka" },
{ value: "6", text: "James" },
{ value: "7", text: "James" }
];
const [options, setOptions] = useState(defaultOptions);
const [selectedOption, setSelectedOption] = useState({ value: "", text: "" });
const [dropdownOpen, setDropdownOpen] = useState(true);
const { Option } = AutoComplete;
const changeHandler = (_, option) => {
const value = option.children;
setSelectedOption({ value: option.key, text: value });
};
function handleClick() {
console.log(`value: ${selectedOption.value}, text: ${selectedOption.text}`);
}
function onClear() {
setSelectedOption({ value: "", text: "" });
}
function onFocusChange() {
if (!dropdownOpen) setDropdownOpen(true);
}
function onSearch(value) {
setOptions(
defaultOptions.filter((f) =>
f.text.toLowerCase().includes(value.toLowerCase())
)
);
}
return (
<div>
{/* when found in search i want this button take to 'onChange' address also*/}
<button disabled={!selectedOption.value} onClick={handleClick}>
click me when found in search
</button>
<AutoComplete
ref={autoControl}
open={dropdownOpen}
style={{ width: 200 }}
placeholder="Search..."
listHeight={220}
onSearch={(e) => onSearch(e)}
onChange={changeHandler}
value={selectedOption.text}
onFocus={onFocusChange}
onBlur={() => setDropdownOpen(false)}
>
{options.map((option) => (
<Option key={option.value} value={option.value}>
{option.text}
<br />
{option.value}
</Option>
))}
</AutoComplete>
<Button
disabled={!selectedOption.value}
onClick={onClear}
type="primary"
icon={<CloseOutlined />}
/>
</div>
);
};
export default EventsSection;
Try filter out object element from your input field value since it is an array
const changeHandler = (_, option) => {
const value = option.children.filter((each) => typeof each !== 'object');
setSelectedOption({ value: option.key, text: value });
};

Categories