MUI Select custom MenuItem not working properly - reactjs

I have a problem regarding MUI's MenuItem when combined with Select and rendering it in a separate component.
Here's the codesandbox
Basically, I have something like this:
import { Select } from "#material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
return (
<Select
id="user"
name="User"
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
alert(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} userId={userId} />
))}
</Select>
);
}
And this is the custom item:
import { MenuItem, Typography } from "#material-ui/core";
import React from "react";
interface CustomMenuItemProps {
userId: number;
}
const CustomMenuItem = React.forwardRef<HTMLLIElement, CustomMenuItemProps>(
(props: CustomMenuItemProps, ref) => {
const { userId, ...rest } = props;
return (
<MenuItem value={userId} {...rest} ref={ref}>
<Typography>{userId}</Typography>
</MenuItem>
);
}
);
export default CustomMenuItem;
At first, I've done this without any refs, but this gave me an error in the console (Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?), so after googling a while, I found out that I have to pass this ref. I also pass the ...rest of the props, as I understand that the MenuItem needs them.
Expected behavior: when I click on the MenuItem, it gets selected in the Select component.
Actual behavior: nothing happens.
The thing is, I made the CustomMenuItem to make it reusable. But before that, I had a simple function like: renderItem which I used both in Select.renderValue and in userIds.map and it had the same code as CustomMenuItem - it returned the same JSX tree. And it worked then, but it doesn't work now, for some reason. So if I would do:
<Select
id="user"
name="User"
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
alert(event.target.value as number);
}}
>
{userIds.map((userId) => (
<MenuItem key={userId} value={userId}>
<Typography>{userId}</Typography>
</MenuItem>
))}
</Select>
it simply works :(
Am I missing something here?

There are a few implementation details of Select that get in the way of trying to customize MenuItem in this way.
Select uses the value prop of its immediate children. The immediate children of the Select in your case are CustomMenuItem elements which only have a userId prop -- not a value prop; so Select finds undefined as the new value when you click on one of your custom menu items.
You can fix this aspect by duplicating your userId prop as a value prop:
import { Select } from "#material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} value={userId} userId={userId} />
))}
</Select>
);
}
This then successfully changes the value of the Select if you look at the console logs. The new value is not successfully displayed due to a separate problem I'll explain later.
You may think "then I can just use the value prop instead of the userId prop rather than having both", but the value prop won't actually reach your custom component. Select uses React.cloneElement to change the value prop to undefined and instead puts it in data-value to avoid a value prop being specified in the final html (which wouldn't be a valid attribute for the html element that gets rendered).
In my sandbox above, you'll notice that when you select a value, the new value is not successfully displayed as the selected value. This is because Select uses the children prop of the selected child as the display value unless you specify the renderValue prop. The children prop of the CustomMenuItem element is undefined.
You can fix this by either using the renderValue prop on the Select or by specifying the userId yet again as a child:
import { Select } from "#material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} value={userId} userId={userId}>
{userId}
</CustomMenuItem>
))}
</Select>
);
}
This works, but also removes all of the value the custom menu item component was trying to provide. I think the simplest way to achieve this (while still working well with the Material-UI Select design) is to put the reusable code in a function for rendering the menu items rather than making a custom menu item component:
import { Select } from "#material-ui/core";
import React from "react";
import { MenuItem, Typography } from "#material-ui/core";
const renderMenuItem = (value: number) => {
return (
<MenuItem key={value} value={value}>
<Typography>{value}</Typography>
</MenuItem>
);
};
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map(renderMenuItem)}
</Select>
);
}

Related

React - Passing a ref into a component which uses ForwardRef

I am building a custom Dropdown component which uses React-Select under the hood. I'd like to add support for refs. I have declared a ref in the app using useRef and am passing this into the component like this:
import Dropdown from "./Dropdown";
import { useRef } from "react";
const App = () => {
const dropdownRef = useRef<HTMLSelectElement>(null);
return (
<div>
<Dropdown id="hello" ref={dropdownRef} />
</div>
);
};
export default App;
In the Dropdown component I'm using ForwardRef like this:
import { forwardRef } from "react";
import Select from "react-select";
export interface DropdownProps {
id: string;
}
const Dropdown = forwardRef(({ id }: DropdownProps, ref) => (
<Select id={id} ref={ref} />
));
export default Dropdown;
However I am getting a Typescript error:
Type 'MutableRefObject' is not assignable to type 'Ref<Select<unknown, false, GroupBase>> | undefined'.
I have tried swapping out the React Select for another component and I get a similar issue so I think this is a general React/Typescript issue rather than anything specific to React Select.
Code Sandbox
Any help would be much appreciated.
I'm slightly surprised, because normally when you specify the hook's initial value as null the type inference is clever enough.
I don't know react-select very well but have you tried passing a string as an initial value, and deleting the <HTMLSelectElement> from your app component? Let the package's TS definitions do the work...
You should defind forwardRef type, it's a generic function:
const Dropdown = forwardRef<HTMLSelectElement,DropdownProps>(( { id }: DropdownProps, ref) => (
<select id={id} ref={ref}>
<option>1</option>
<option>2</option>
</select>
));
You should specify the props to your forwardRef so it knows what types you are trying to forward, as such:
import { forwardRef } from "react";
export interface DropdownProps {
id: string;
}
const Dropdown = forwardRef<HTMLSelectElement, DropdownProps>(({ id }, ref) => (
<div>
<select id={id} ref={ref}>
<option>1</option>
<option>2</option>
</select>
</div>
));
export default Dropdown;
The first template argument will be helpful to determine the type of ref you are trying to forward, the second will define the arguments your component will receive.
A good cheatsheet here for reference.

Set each active className on react Tab components

I'm trying to add active class on each tabtitle so that it can have some styles when clicked. but I have no idea how to add active class on this components, please tell me some solutions, I really appreciate all the help.
App.tsx
import React from "react"
import Tabs from "../Tabs"
import Tab from "../Tabs/Tab"
function App() {
return (
<Tabs>
<Tab title="Lemon">Lemon is yellow</Tab>
<Tab title="Strawberry">Strawberry is red</Tab>
<Tab title="Pear">Pear is green</Tab>
</Tabs>
)
}
Tabs.tsx
import React, { ReactElement, useState } from "react"
import TabTitle from "./TabTitle"
type Props = {
children: ReactElement[]
}
const Tabs: React.FC<Props> = ({ children }) => {
const [selectedTab, setSelectedTab] = useState(0)
return (
<div>
<ul>
{children.map((item, index) => (
<TabTitle
key={index}
title={item.props.title}
index={index}
setSelectedTab={setSelectedTab}
/>
))}
</ul>
{children[selectedTab]}
</div>
)
}
export default Tabs
Tabtitle.tsx
import React, { useCallback } from "react"
type Props = {
title: string
index: number
setSelectedTab: (index: number) => void
}
const TabTitle: React.FC<Props> = ({ title, setSelectedTab, index }) => {
const onClick = useCallback(() => {
setSelectedTab(index)
}, [setSelectedTab, index])
return (
<li>
<button onClick={onClick}>{title}</button>
</li>
)
}
export default TabTitle
Tab.tsx
import React from 'react'
type Props = {
title: string
}
const Tab: React.FC<Props> = ({ children }) => {
return <div>{children}</div>
}
export default Tab
Here is the source.
https://medium.com/weekly-webtips/create-basic-tabs-component-react-typescript-231a2327f7b6
So personally what I would do is:
<TabTitle
key={index}
title={item.props.title}
click={() => setSelectedTab(index)}
selected = (selectedTab === index)
/>
and
const TabTitle: React.FC<Props> = ({ key, title, click, selected}) => {
return (
<li className={selected ? "shiny" : "not-shiny"} key={key}>
<button onClick={click}>{title}</button>
</li>
)
}
That way we don't need to worry about passing the index from tabTitle ALL the way back up, we can simply hand it a function to trigger with the variables pre-filled (we already have access to them within the map method!).
As for actually passing the info about whether or not the tab is selected, we can simply check in the map method whether the current index is = the one held in state. If this evaluates to true we give the tabtitle one class, and if not we give it another.
Note there are lots of different ways to achieve this, this is just the first one that came to mind.
If you wanted to share state information between many different components lower in the tree you might want to check out using React Context providers and the useContext hook.
Also I'm not sure item.props works like that?? I might be wrong though? Maybe someone else will confirm.
Hope this is helpful.
import React from "react"
import Tabs from "../Tabs"
import Tab from "../Tabs/Tab"
function App() {
return ( <Tabs>
<Tab title="Lemon">Lemon is yellow</Tab>
<Tab title="Strawberry">Strawberry is red</Tab>
<Tab title="Pear">Pear is green</Tab>
</Tabs> )
}

How to filter and sort product in ReactJs by Dropdown component of React bootstrap

I am creating a filter function for the product list in UI for the user. But I have an issue that, I do not know and never ever try with this function before, so I really difficult to resolve it. I have only 1 day left to do that, so I was very confused
This is my Dropdown Component
import React from "react";
import { Dropdown as BootstrapDropdown } from "react-bootstrap";
import PropTypes from "prop-types";
import "../Dropdown/index.css";
const Dropdown = ({ items }) => {
return (
<BootstrapDropdown className="sort-dropdown">
<BootstrapDropdown.Toggle
className="sort-dropdown-toggle"
variant="success"
id="dropdown"
>
<span className="toggle-text">Selection</span>
</BootstrapDropdown.Toggle>
<BootstrapDropdown.Menu className="sort-dropdown-menu">
{items.map((name, index) => (
<BootstrapDropdown.Item
className="sort-dropdown-item"
key={index}
href={`#/action-${index}`}
>
{name}
</BootstrapDropdown.Item>
))}
</BootstrapDropdown.Menu>
</BootstrapDropdown>
);
};
Dropdown.propTypes = {
items: PropTypes.array,
};
Dropdown.defaultProps = {
items: [],
};
export default Dropdown;
And this is my page, which the place I get the Dropdown component
import React from "react";
import { Row } from "react-bootstrap";
import Group from "../../../components/Group/index";
import Dropdown from "../../../components/Dropdown/index";
import "../GroupBar/index.css";
const GroupBar = () => {
return (
<Row className="group-bar">
<Group
title="Product group"
element={<Dropdown items={["Milk Tea", "Juice"]} />}
/>
<Group
title="Sort by price"
element={<Dropdown items={["Low to hight", "Hight to low"]} />}
/>
</Row>
);
}
export default GroupBar;
I would like to filter (by category) and sort (by price) my product page by items of the dropdown. When I select that item, the product will be filtered according to the item I chose.
This is my product list page
import React, { useEffect } from "react";
import { Container, Row, Col } from "react-bootstrap";
import ProductItem from "../../../components/ProductItem/index";
import Loading from "../../../components/Loading";
import PropTypes from "prop-types";
import "../../../common/index.css";
import "../ProductList/index.css";
const ProductList = ({ products, loading, fetchProductRequest }) => {
useEffect(() => {
fetchProductRequest();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (loading) {
return (
<Container>
<Row>
<Col>
<Loading />
</Col>
</Row>
</Container>
);
}
return (
<Container>
<Row>
{!!products && products.length > 0 ? (
products.map((product, index) => {
return (
<ProductItem
key={index}
image={product.image}
name={product.name}
price={product.price}
/>
);
})
) : (
<h4 className="center-title">Product list is empty!</h4>
)}
</Row>
</Container>
);
};
export default ProductList;
This is the page for that,
The list of product and the filter/sort are located in the same folder but different files. Like this
The Group bar it is contain the filter/sort. I get all values by redux, saga
The main page, contain all of them is here
import React from "react";
import { Container } from "react-bootstrap";
import GroupBar from "./GroupBar";
import ProductContainer from "../../containers/ProductContainer";
import Carousel from "../../components/Carousels";
import "../Product/index.css";
const Product = () => {
return (
<Container fluid className="p-0">
<Carousel />
<Container>
<GroupBar />
<ProductContainer />
</Container>
</Container>
);
};
export default Product;
How can I filter related to my list product when it different file like that.
Please anyone help me with this my problem, I just have one day to finish that function, I already research on the internet but it's doesn't make me understand more because it so different from my code and I can not apply that code for mine.
I really really need your support and help as well as you can, the full the better. It's not just helped me to understand also for others like me who are doesn't try it before also see the code is easy to understand too.
I always welcome all of your comments. That is my pleasure. Thank you so much.
You will need your components to keep track of some state. You can read about how to do that here and here.
Once you understand the concept of state, you need to keep track of which item in the list is selected. So for example, you need to keep a state variable that tracks whether "Milk Tea" is selected or "Juice" is selected.
Then, once you have that state, you can display your items using filter or sort on the items list.
Personally, I recommend using class components instead of function components, but here is a minimal working example using function components:
import React, { useState } from 'react';
import { Dropdown as BootstrapDropdown } from 'react-bootstrap';
import './App.css';
const Dropdown = (props) => {
return (
<BootstrapDropdown>
<BootstrapDropdown.Toggle variant='success' id='dropdown'>
<span>Selection</span>
</BootstrapDropdown.Toggle>
<BootstrapDropdown.Menu>
{props.items.map((name, index) => (
<BootstrapDropdown.Item
key={index}
onClick={(event) => {
console.log(event.target.text);
props.setSelected(event.target.text);
}}
value={name}
>
{name}
</BootstrapDropdown.Item>
))}
</BootstrapDropdown.Menu>
</BootstrapDropdown>
);
};
function App() {
const [typeFilter, setTypeFilter] = useState('');
const allItems = [
{ name: 'Coffee Milk Tea', type: 'Tea' },
{ name: 'Earl Gray Milk Tea', type: 'Tea' },
{ name: 'Orange Juice', type: 'Juice' },
{ name: 'Wheatgrass Juice', type: 'Juice' },
];
const itemsToShow = allItems
.filter((item) => {
if (typeFilter) {
return item.type === typeFilter;
}
return true;
})
.map((item, i) => {
return <li key={i}>{item.name}</li>;
});
return (
<div>
<Dropdown items={['Tea', 'Juice']} setSelected={setTypeFilter} />
<ol>{itemsToShow}</ol>
</div>
);
}
export default App;
Notice that the App component stores the state, and passes its state setter to the Dropdown component. The Dropdown gets the setter in its props and uses it to set the App's state when an option is clicked. The App then uses its state to determine which items to show (using items.filter).
This is an example of Lifting state up. Normally, we would think of tracking which item is selected as the job of the dropdown. But, since we need to access that state in another component, we have to "lift up" that state to something higher in the tree. In this small example case, it was App that stored the state. In general, if the tree looks like this:
A
B
C
D
E
F
G
H
and you want to share state between G and D, you need to put that state inside of A because A is the closest parent of both G and D. If you want to share state between C and D, then you need to put that state inside B, because B is the parent of C and D.
In reference to the comment below, you probably want to keep the state for which thing in the dropdown is selected inside of your Product component. Then you need to pass the state setter down the props chain all the way into the Dropdown component, which can call that setter and update the state.
Sorry to hear about your tight schedule. Hopefully this answer can be of some use to you.

How to set multi value to autocomplete component in reactjs

I need to set multi selected value to autocomplete in reactjs. I'm using Material-UI components in my project.
F.e you can see above. First data is coming first user and second data is coming from another user. I want to fill in the value like that. Then, user can remove selected values or add new values.
If you can do it with dummy data, I can use with data from database. All i need how to do this.
import React from 'react';
import Chip from '#material-ui/core/Chip';
import Autocomplete from '#material-ui/lab/Autocomplete';
import { makeStyles } from '#material-ui/core/styles';
import TextField from '#material-ui/core/TextField';
const useStyles = makeStyles((theme) => ({
root: {
width: 500,
'& > * + *': {
marginTop: theme.spacing(3),
},
},
}));
export default function Multi({callbackFromMultiSelect,reference,favBooks}) {
const classes = useStyles();
return (
<div className={classes.root}>
<Autocomplete
multiple
id="tags-standard"
options={favBooks}
getOptionLabel={(option) => (option.name)}
// onClick={()=>alert('test')}
onChange={(event, value) =>callbackFromMultiSelect({value:value,reference:reference})}
// defaultValue={[top100Films[1]]}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label="favBooks"
placeholder="favBooks"
/>
)}
/>
</div>
);
}
And my parent component
import React from 'react'
import AutoCompleteTest from './AutoComplete'
export const Test = () => {
const callbackFromMultiSelect = (item) => {
console.log(item)
}
const favBooks=[
{name:"LOTR",from:"a",to:"a"},
{name:"GOT",from:"b",to:"b"},
{name:"HP",from:"c",to:"c"}
]
return (
<div className={'mainStore'}>
Test
<AutoCompleteTest callbackFromMultiSelect={callbackFromMultiSelect} reference={'test'} favBooks={favBooks}/>
<br />
<AutoCompleteTest callbackFromMultiSelect={callbackFromMultiSelect} reference={'test'} favBooks={favBooks}/>
</div>
)
}
I've made a codesandbox
https://codesandbox.io/s/wizardly-saha-gor3s?file=/src/App.js
You basicly have to make the component a controlled component.
https://material-ui.com/components/autocomplete/#playground
You can do this by using the 'getOptionSelected' prop

How to persist intermittent text on `react-select`?

I use below code which is to render a multi selection react-select component. But what I found is that when I type any characters and leave the focus before select any items, the characters are not persisted.
import React from 'react';
import Select from 'react-select';
import makeAnimated from 'react-select/animated';
import { colourOptions } from '../data';
const animatedComponents = makeAnimated();
export default function AnimatedMulti() {
return (
<Select
closeMenuOnSelect={false}
components={animatedComponents}
defaultValue={[colourOptions[4], colourOptions[5]]}
isMulti
options={colourOptions}
/>
);
}
As an example of that, see below screenshot. I typed f and move the focus on other component. Then the f will be removed from the select component. Is there a way to persist the f when it loses focus?
You can achieve this by combining inputValue, value, onInputChange and onChange props.
Something like this
<Select
value={this.state.value}
onChange={value => this.setState({ value })}
inputValue={this.state.inputValue}
onInputChange={inputValue => this.setState({ inputValue })}
/>
You can read more about this in the docs

Resources