What I'm trying to do
I'm trying to create a category list with Material UI
Parent Menu Item
Child Menu Item by Parent Menu Category
What I'm getting
I'm getting only parent menu value.
Whenever I click on child item, than it showing it's parent Menu.
Code
Here I've create a codesanbox, Please help to get out from this.
Code
Please check console for see event.
<Select
displayEmpty
defaultValue=""
value={search}
onChange={handleChange}
id="grouped-select"
input={<OutlinedInput />}
label="Category"
size="small"
sx={{
width: "160px",
height: "100%",
borderRadius: 0,
textTransform: "uppercase"
}}
renderValue={(selected) => {
if (selected.length === 0) {
return (
<Typography variant="subtitle2">Select Category</Typography>
);
}
return <Typography variant="subtitle2">{selected}</Typography>;
}}
>
<MenuItem value="select category">
<Typography variant="subtitle2">Select Category</Typography>
</MenuItem>
{categories.map((category) => (
<MenuItem
key={category}
sx={{
lineHeight: "20px",
display: "block",
backgroundColor: "none"
}}
value={category}
>
<Typography variant="subtitle2">{category}</Typography>
{category === "Baby Products" &&
babyProducts.map((item) => (
<List key={item} sx={{ padding: 0 }}>
<MenuItem
sx={{
lineHeight: "12px",
display: "block"
}}
value={item}
onChange={handleChange}
>
<Typography variant="subtitle2">{item}</Typography>
</MenuItem>
</List>
))}
</MenuItem>
))}
</Select>
const babyProducts = [
"Baby",
"Baby Cereal",
"Baby Honey",
"Biscotti",
"Formula",
"Milk",
"Juice",
"Puree",
"Bath & Skincare",
"Diapers & Wipes",
"Huggies",
"Kidz",
"MamyPoko",
"Molfix",
"Pampers & Wipes",
"Oral Care",
];
const chocolates = [
"Assorted",
"Cadbury",
"Candy",
"Chocolate Balls",
"Chocolate Bars",
"Kit-Kat",
"Lolipops",
"Marshmallow",
"Mints&Gums",
"Toblenore",
];
The problem is due to nested MenuItems. You can use nested arrays instead with the marginLeft on a subcategory:
{categories.map((cat) => [
renderCategory(cat),
renderCategoryItems(cat)
])}
const renderCategory = (category) => {
return (
<MenuItem
key={category}
sx={{
lineHeight: "20px",
display: "block",
backgroundColor: "none"
}}
value={category}
>
<Typography variant="subtitle2">{category}</Typography>
</MenuItem>
);
};
const renderCategoryItems = (category) => {
const list = category === "Baby Products" ? babyProducts : [];
return list.map((item) => (
<MenuItem
key={item}
sx={{
lineHeight: "12px",
display: "block"
}}
value={item}
>
<Typography
sx={{
marginLeft: "16px"
}}
variant="subtitle2"
>
{item}
</Typography>
</MenuItem>
));
};
Update
If more subcategories are required, you can describe them as follows:
const subCategories = {
"Baby Products": [
...
],
"Chocolate": [
...
]
};
And update renderCategoryItems function:
const list = subCategories[category] || [];
Working example
Related
I'm new to react with Typescript & MUI.
I'm working on a project that was originally built by someone else.
I need to modify an existing code and turn it into a <Autocomplete /> dropdownlist
Can someone please help with this?
I'm not really sure how to do this and I have been working on this for hours.
So basiclly the code below is working fine, I just need to make some changes to wrap the Language list in a dropdown list.
Currently it is working more like a dropdown menu.
const { borderRadius, locale, onChangeLocale } = useConfig();
const theme = useTheme();
const matchesXs = useMediaQuery(theme.breakpoints.down('md'));
const [open, setOpen] = useState(false);
const anchorRef = useRef<any>(null);
const [language, setLanguage] = useState<string>(locale);
const handleListItemClick = (
event:
| React.MouseEvent<HTMLAnchorElement>
| React.MouseEvent<HTMLDivElement, MouseEvent>
| undefined,
lng: string
) => {
setLanguage(lng);
onChangeLocale(lng);
setOpen(false);
};
const handleToggle = () => {
setOpen(prevOpen => !prevOpen);
};
const handleClose = (event: MouseEvent | TouchEvent) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
const prevOpen = useRef(open);
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus();
}
prevOpen.current = open;
}, [open]);
useEffect(() => {
setLanguage(locale);
}, [locale]);
return (
<>
<Box
sx={{
ml: 3,
mr: 2,
[theme.breakpoints.down('md')]: {
ml: 1,
},
}}
>
<Avatar
variant="rounded"
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
border: '1px solid',
borderColor: 'rgba(255,255,255, 0)',
background: 'rgba(255,255,255, 0)',
color: theme.palette.primary.dark,
transition: 'all .2s ease-in-out',
'&[aria-controls="menu-list-grow"],&:hover': {
color: theme.palette.primary.main,
},
}}
ref={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
color="inherit"
>
{language !== 'en' && (
<Typography
variant="h2"
color="inherit"
sx={{ textTransform: 'uppercase', fontWeight: '500' }}
>
{language}
</Typography>
)}
{language === 'en' && (
<TranslateTwoToneIcon sx={{ fontSize: '2.2rem' }} />
)}
</Avatar>
</Box>
<Popper
placement={matchesXs ? 'bottom-start' : 'bottom'}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [matchesXs ? 0 : 0, 20],
},
},
],
}}
>
{({ TransitionProps }) => (
<ClickAwayListener onClickAway={handleClose}>
<Transitions
position={matchesXs ? 'top-left' : 'top'}
in={open}
{...TransitionProps}
>
<Paper elevation={16}>
{open && (
<List
component="nav"
sx={{
width: '100%',
minWidth: 200,
maxWidth: 280,
bgcolor: theme.palette.background.paper,
borderRadius: `${borderRadius}px`,
[theme.breakpoints.down('md')]: {
maxWidth: 250,
},
}}
>
<ListItemButton
selected={language === 'en'}
onClick={event => handleListItemClick(event, 'en')}
>
<ListItemText
primary={
<Grid container>
<Typography color="textPrimary">English</Typography>
<Typography
variant="caption"
color="textSecondary"
sx={{ ml: '8px' }}
>
(UK)
</Typography>
</Grid>
}
/>
</ListItemButton>
<ListItemButton
selected={language === 'sv'}
onClick={event => handleListItemClick(event, 'sv')}
>
<ListItemText
primary={
<Grid container>
<Typography color="textPrimary">Svenska</Typography>
<Typography
variant="caption"
color="textSecondary"
sx={{ ml: '8px' }}
>
(SE)
</Typography>
</Grid>
}
/>
</ListItemButton>
</List>
)}
</Paper>
</Transitions>
</ClickAwayListener>
)}
</Popper>
</>
);
};
export default LocalizationSection;```
first of all look at these below screenshots:
There are two tasks which I want to achieve:
There are two questions shown on the page using the array map method, by default I'm showing only one question, and when I press the next part button the second question will appear with the same question and a TextField (multiline). Now I've implemented a word counter in TextField but when I type something in 1st question the counter works properly. But when I go to the next question the here counter shows the previous question's word counter value, I want them to separately work for both questions.
When I click on the next part and again when I click on the previous part then the values from TextField are removed automatically. I want the values there if I navigate to the previous and next part questions. Also, I want to get both TextField values for a form submission when I press the Submit Test button.
Below are my codes for this page. I'm using Next.js and MUI
import { Grid, Typography, Box, NoSsr, TextField } from '#mui/material';
import PersonIcon from '#mui/icons-material/Person';
import Timer from '../../../components/timer';
import Button from '#mui/material/Button';
import { useState } from 'react';
import ArrowForwardIosIcon from '#mui/icons-material/ArrowForwardIos';
import { Scrollbars } from 'react-custom-scrollbars';
import AppBar from '#mui/material/AppBar';
import Toolbar from '#mui/material/Toolbar';
import ArrowBackIosIcon from '#mui/icons-material/ArrowBackIos';
import axios from '../../../lib/axios';
import { decode } from 'html-entities';
import { blueGrey } from '#mui/material/colors';
export default function Writing({ questions }) {
const [show, setShow] = useState(false);
const [show1, setShow1] = useState(true);
const [showQuestionCounter, setShowQuestionCounter] = useState(0);
const [wordsCount, setWordsCount] = useState(0);
return (
<>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="fixed" style={{ background: blueGrey[900] }}>
<Toolbar>
<Grid container spacing={2} alignItems="center">
<Grid item xs={4} display="flex" alignItems="center">
<PersonIcon
sx={{ background: '#f2f2f2', borderRadius: '50px' }}
/>
<Typography variant="h6" color="#f2f2f2" ml={1}>
xxxxx xxxxx-1234
</Typography>
</Grid>
<Grid item xs={4} container justifyContent="center">
<Timer timeValue={2400} />
</Grid>
<Grid item xs={4} container justifyContent={'right'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Settings
</Button>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0px 10px',
}}
size="small">
Hide
</Button>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Help
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
</Box>
<Box
sx={{
background: blueGrey[50],
height: '100%',
width: '100%',
position: 'absolute',
}}
pt={{ xs: 13, sm: 11, md: 10, lg: 11, xl: 11 }}>
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Box
key={question.id}
px={3}
sx={{ background: '#f2f2f2', pb: 4 }}
position={{
xs: 'sticky',
sm: 'sticky',
lg: 'initial',
md: 'initial',
xl: 'initial',
}}>
<Box
style={{ background: '#f7fcff', borderRadius: '4px' }}
py={1}
px={2}>
<Box>
<Typography variant="h6" component="h6" ml={1}>
Part {question.id}
</Typography>
<Typography variant="subtitle2" component="div" ml={1} mt={1}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.questions[0].question, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Box>
</Box>
<Box
style={{
background: '#f7fcff',
borderRadius: '4px',
marginBottom: '75px',
}}
pt={1}
px={3}
mt={{ xs: 2, sm: 2, md: 2, lg: 0, xl: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} lg={6} md={6} xl={6}>
<Box
py={{ lg: 1, md: 1, xl: 1 }}
style={{ height: '50vh' }}>
<Scrollbars universal>
<Typography
variant="body1"
component="div"
style={{ textAlign: 'justify' }}
mr={2}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.question_text, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Scrollbars>
</Box>
</Grid>
<Grid
item
xs={12}
sm={12}
lg={6}
md={6}
xl={6}
mt={{ md: 4, lg: 4, xl: 4 }}>
<TextField
id={`${question.id}`}
label="Type your answer here"
multiline
name={`answer_${question.id}`}
rows={12}
variant="outlined"
fullWidth
helperText={`Words Count: ${wordsCount}`}
onChange={(e) => {
setWordsCount(
e.target.value.trim().split(/\s+/).length
);
}}
/>
</Grid>
</Grid>
</Box>
</Box>
) : null
)}
<Box sx={{ position: 'fixed', width: '100%', left: 0, bottom: 0 }}>
<Grid
container
style={{ background: blueGrey[300], display: 'flex' }}
py={2}
px={3}>
<Grid
item
xs={3}
sm={3}
lg={6}
md={6}
xl={6}
container
justifyContent={'start'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Save Draft
</Button>
</Grid>
<Grid
item
xs={9}
sm={9}
lg={6}
md={6}
xl={6}
container
justifyContent={'end'}>
<Button
variant="contained"
size="small"
style={{
background: 'white',
color: 'black',
visibility: show1 ? 'visible' : 'hidden',
}}
endIcon={<ArrowForwardIosIcon />}
onClick={() => {
setShow((prev) => !prev);
setShowQuestionCounter(showQuestionCounter + 1);
setShow1((s) => !s);
}}>
Next Part
</Button>
{show && (
<>
<Box>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0 10px',
visibility: show ? 'visible' : 'hidden',
}}
startIcon={<ArrowBackIosIcon />}
size="small"
onClick={() => {
setShow1((s) => !s);
setShowQuestionCounter(showQuestionCounter - 1);
setShow((prev) => !prev);
}}>
previous Part
</Button>
<Button variant="contained" color="success">
Submit Test
</Button>
</Box>
</>
)}
</Grid>
</Grid>
</Box>
</Box>
</>
);
}
export async function getServerSideProps(context) {
const { id } = context.query;
const token = context.req.cookies.token;
if (!token) {
context.res.writeHead(302, {
Location: '/',
});
context.res.end();
}
const res = await axios.get(`test/${id}/questions`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.data.success) {
return {
props: {
questions: res.data.data.questions,
},
};
}
}
The wordsCount state is shared between both questions, which means that when you go to the next question the state remains unchanged and shows the wordsCount from the first question. To solve it, each question needs to have it's own state which you can do by creating a Question component and mapping over it:
export default function Question({ question }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box
// ...
>
{/* ... */}
<TextField
// ...
helperText={`${wordsCount} words`}
onChange={(e) => {
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
Then map over it:
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question key={question.id} question={question} />
) : null
)}
Currently, the value of TextField gets reset you the component is unmounted (i.e. when you go to the next question). You need to make the TextField component a controlled component, meaning that you store the value of the field in useState. And if you need to submit the value of TextField later, then you probably need to store the values in the parent component:
export default function Writing({ questions }) {
// ...
const [answers, setAnswers] = useState([])
function handleChange(id, answer) {
// copy the current answers
let newAnswers = [...answers]
// find the index of the id of the answer in the current answers
const index = newAnswers.findIndex((item) => item.id === id)
// if the answer does exist, replace the previous answer with the new one, else add the new answer
if (index) {
newAnswers[index] = { id, answer }
setAnswers(newAnswers)
} else {
setAnswers([...answers, { id, answer }])
}
}
return (
<>
{/* ... */}
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question
key={question.id}
question={question}
value={answers.find((item) => item.id === question.id)?.answer || ''}
handleChange={handleChange}
/>
) : null
)}
</>
}
In the Question component, add the handler.
export default function Question({ question, value, handleInputChange }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box>
{/* ... */}
<TextField
helperText={`${wordsCount} words`}
value={value}
onChange={(e) => {
handleInputChange(question.id, e.target.value)
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
In the parent component (Writing) you should be able to use the values for form submission.
I am trying to figure out how to create as many useState as cleanly (keeping DRY principle in mind) as possible.
The problem I am trying to solve is that currently I have a menu list of items and its relative prices. The website lets the customer use a drop down menu to choose how many orders of that item he/she wants.
My question is that I know for one menu item, I can use useState to make something like
const [order, setOrder] = useState({});
where the key-value pair would be name and number of orders
Now my question is: suppose I have N many menu items, how can I create as many const [order, setOrder] = useState({}) as I need? I know I can't put useState in a for-loop so that's out of the question (?)
The below is my code I'm trying to work out:
export default function MenuPage() {
// getting the menu items and prices from firebase
const [menuItems, setMenuItems] = useState({});
React.useEffect(async () => {
const result = await querySnapShot();
result.forEach((doc) => {
setMenuItems(doc.data());
})
}, []);
return(
{
Object.keys(menuItems).map((key) => {
return (
<Box
key={key}
display='flex'
flexDirection='row'
alignItems='center'
justifyContent='center'
textAlign='center'
borderBottom='0.5px solid black'
sx={{ my: 10, mx: 10 }}>
<Box flex={1} textAlign='center' sx={{ fontSize: '35px', }}>
{key}:
</Box>
<br />
<Box flex={1} textAlign='center' sx={{ fontSize: '25px' }}>
${menuItems[key]}0
</Box>
<Box flex={1} display='flex' flexDirection='row' alignItems='center' justifyContent='end' sx={{ width: '50px' }}>
{/* <Typography flex={0} sx={{ mr: 5, fontFamily: 'Poppins', fontSize: '25px' }}>Order: </Typography> */}
<Box flex={0.5} textAlign='center'>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Order</InputLabel>
<Select
value={order}
name={key}
label="Order"
onChange={handleChange}>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
</Select>
</FormControl>
</Box>
</Box>
</Box>
);
})
}
);
}
Supposing that you have an array as response you can:
const data = [
{ name: 'Foo', value: 'bar' },
{ name: 'Foo2', value: 'bar2' },
];
setState(data.map({ name: 'Foo', value: 'bar' }));
// or if no change has to be done you can set it directly into the state
setState(data);
I'm currently facing a issue with the MultiCascader from rsuite.
I planned to put the cascader inside a CustomDialog. The selection is showh however the dropdown to select new items is not shown in the Dialog itself but in the background. See attached screenshots.
Code of which is rendered inside the CustomDialog
return (
<>
<Grid container item xs={12} spacing={8}>
<Grid item xs={12}>
<Typography sx={{ color: 'black' }}>Please select skills!</Typography>
</Grid>
<Grid container item xs={12}>
<Box style={{ display: 'block', width:800 }}>
<MultiCascader
style={{ width: 800 }}
placeholder="Select Skills"
data={serviceThesaurus}
menuWidth={350}
onCheck={(value) => handleSelect(value)}
uncheckableItemValues={notSelectList}
defaultValue={serviceList}
onClean={handleClear}
placement="autoVerticalStart"
/>
</Box>
</Grid>
<Grid item xs={12}>
<Box sx={{ border: 1 }} minHeight="50px" minWidth="200px">
{skillList.map((service, ind) => (
// eslint-disable-next-line react/no-array-index-key
<Box margin="1%" key={ind}>
<Typography>{service}</Typography>
</Box>
))}
</Box>
</Grid>
</Grid>
Code of the custom Dialog:
const useStyles = makeStyles({
dialogPaper: {
minHeight: '97%',
minWidth: '50%',
},
});
interface ComponentProps {
children: ReactNode;
handleClose: () => void;
handleSave: (() => void) | undefined;
open: boolean;
}
export default function CustomDialog({
open,
handleClose,
handleSave,
children,
}: ComponentProps) {
const classes = useStyles();
const { t } = useTranslation('common');
return (
<Box
sx={{
backgroundColor: 'background.default',
display: 'flex',
flexDirection: 'column',
height: '100%',
justifyContent: 'center',
}}
>
<Dialog
open={open}
onClose={handleClose}
scroll="paper"
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
classes={{ paper: classes.dialogPaper }}
>
<DialogTitle id="scroll-dialog-title">{t('preferences')}</DialogTitle>
<DialogContent dividers>
<DialogContentText
id="scroll-dialog-description"
// ref={descriptionElementRef}
tabIndex={-1}
>
{children}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
{t('cancelButton')}
</Button>
{handleSave && (
<Button onClick={handleSave} variant="contained" color="primary">
{t('saveButton')}
</Button>
)}
</DialogActions>
</Dialog>
</Box>
);
}
Picture of the situation
I have a menu component that pops open in a table. When I copy the material ui example into the cell in the table it works perfectly.
https://codesandbox.io/s/gitl9
The material ui example uses hooks and I want to change it to a class component and use redux.
When I made the change the pop-up menu does not align beside the the button you press anymore.
The anchorEl attribute is responsible for passing the location of the button that has been called.
I added these attributes allow me to move the menu pop but it does not align with button that you click to open the menu.
const options = ["View", "Edit", "Delete"];
const ITEM_HEIGHT = 48;
class ActionsOptionMenu extends Component {
state = { anchorEl: null };
render() {
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
>
<MoreVertIcon />
</IconButton>
<Menu
getContentAnchorEl={null}
anchorOrigin={{
height: "54px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: `0 ${padding} 0 ${padding}`,
margin: "0 auto 7px auto"
}}
transformOrigin={{ vertical: "top", horizontal: "right" }}
id='long-menu'
anchorEl={anchorEl}
keepMounted
open={true}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: 120
}
}}
>
{options.map(option => (
<MenuItem key={option}>{option}</MenuItem>
))}
</Menu>
</div>
);
}
}
I solved the issue doing it this way.
render() {
const { open } = this.state;
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
buttonRef={node => {
this.anchorEl = node;
}}
onClick={event => this.handleClick(event)}
>
<MoreVertIcon />
</IconButton>
<Popper
open={open}
anchorEl={this.anchorEl}
transition
disablePortal
style={{ zIndex: 100 }}
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
id='menu-list-grow'
style={{
zIndex: 1001,
transformOrigin:
placement === "bottom" ? "center top" : "center bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={event => this.handleClose(event)}
>
<MenuList>
<MenuItem onClick={event => this.handleClose(event)}>
Profile
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
My account
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
Logout
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
);
}