Reputation: 313
I am trying to develop a sidebar menu using material ui. I am able to make it for simple list. In my project I have a requirement of nested sidebar menu which I am not able to achieve. If I am trying to use recursive function it is providing only main title menu and not rendering child elements. Please help me develop it.
The code for nested sidebar menu is as below,
import React, {useState} from 'react';
import { makeStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Collapse from '@material-ui/core/Collapse';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
maxWidth: 360,
backgroundColor: theme.palette.background.paper,
},
nested: {
paddingLeft: theme.spacing(4),
},
}));
export const Menu = ({items}) => {
const classes = useStyles();
const [open, setOpen] = useState(true);
const handleClick = () => {
setOpen(!open);
};
return (
items.map(item =>
!item.children ? (
<div key={item.title}>
<ListItem button>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
</div>
) : (
<div
component="nav"
key={item.title}
>
<ListItem button onClick={handleClick}>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.title} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
<ListItem button className={classes.nested}>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText>
<Menu items={item} />
</ListItemText>
</ListItem>
</List>
</Collapse>
</div>
)
)
);
}
menu item code is here,
import HomeOutlinedIcon from "@material-ui/icons/HomeOutlined";
import LocalLibraryOutlinedIcon from "@material-ui/icons/LocalLibraryOutlined";
import TrendingUpOutlinedIcon from "@material-ui/icons/TrendingUpOutlined";
import DescriptionOutlinedIcon from "@material-ui/icons/DescriptionOutlined";
import React from "react";
export const menu = [
{
icon: <HomeOutlinedIcon/>,
title: 'Home',
items: []
},
{
icon: <LocalLibraryOutlinedIcon/>,
title: 'Education',
items: [
{
title:'Technical Analysis',
items: [
{
title: 'The Dow Theory',
to: '/thedowtheory'
},
{
title: 'Charts & Chart Patterns',
to: '/chart'
},
{
title: 'Trend & Trend Lines',
to: '/trendlines'
},
{
title: 'Support & Resistance',
to: '/sandr'
},
]
},
{
title:'Fundamental Analysis',
items: [
{
title: 'The Dow Theory',
to: '/thedowtheory'
},
{
title: 'Charts & Chart Patterns',
to: '/chart'
},
{
title: 'Trend & Trend Lines',
to: '/trendlines'
},
{
title: 'Support & Resistance',
to: '/sandr'
},
]
},
{
title:'Elliot Wave Analysis',
items: [
{
title: 'The Dow Theory',
to: '/thedowtheory'
},
{
title: 'Charts & Chart Patterns',
to: '/chart'
},
{
title: 'Trend & Trend Lines',
to: '/trendlines'
},
{
title: 'Support & Resistance',
to: '/sandr'
},
]
},
]
},
{
icon: <TrendingUpOutlinedIcon/>,
title: 'Options'
},
{
icon: <DescriptionOutlinedIcon/>,
title: 'Blog'
},
]
Upvotes: 4
Views: 42340
Reputation: 1
you can check this GitHub repository I've updated some code with useState.react-sidebar-material-ui
Upvotes: 0
Reputation: 515
You can use material-ui-nested-menu-item. It is a very easy npm library to use and works for Material UI 4. :)
Unfortunately, it didn't work for me using version 5.
Upvotes: 0
Reputation: 4706
I have fixed your code and refactored a little bit. I hope you can make it cleaner..
import React, { useState } from "react";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Collapse from "@material-ui/core/Collapse";
import ExpandLess from "@material-ui/icons/ExpandLess";
import ExpandMore from "@material-ui/icons/ExpandMore";
const ListItemBody = ({config}) => {
return (<>
<ListItemIcon>{config.icon}</ListItemIcon>
<ListItemText primary={config.title} />
</>);
}
const MenuItem = ({ config }) => {
return (
<ListItem button>
<ListItemBody config={config}/>
</ListItem>
);
};
const ExpandableMenuItem = ({ config }) => {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen(!open);
};
return (
<div component="nav">
<ListItem button onClick={handleClick}>
<ListItemBody config={config}/>
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<Menu items={config.items} />
</Collapse>
</div>
);
};
export default function Menu({ items }) {
const createList = (items) => {
let menu = [];
items.map((menuItem) => {
if (Array.isArray(menuItem.items) && menuItem.items.length > 0) {
menu.push(<ExpandableMenuItem
config={menuItem}
key={menuItem.title}
/>);
} else {
menu.push(<MenuItem
config={menuItem}
key={menuItem.title}
/>);
}
});
return menu.concat();
};
return <List>{createList(items)}</List>;
}
index.js (Usage):
import React from "react";
import ReactDOM from "react-dom";
import Demo from "./demo";
import HomeOutlinedIcon from "@material-ui/icons/HomeOutlined";
import LocalLibraryOutlinedIcon from "@material-ui/icons/LocalLibraryOutlined";
import TrendingUpOutlinedIcon from "@material-ui/icons/TrendingUpOutlined";
import DescriptionOutlinedIcon from "@material-ui/icons/DescriptionOutlined";
export const menu = [
{
icon: <HomeOutlinedIcon />,
title: "Home",
items: []
},
{
icon: <LocalLibraryOutlinedIcon />,
title: "Education",
items: [
{
title: "Technical Analysis",
items: [
{
title: "The Dow Theory",
to: "/thedowtheory"
},
{
title: "Charts & Chart Patterns",
to: "/chart"
},
{
title: "Trend & Trend Lines",
to: "/trendlines"
},
{
title: "Support & Resistance",
to: "/sandr"
}
]
},
{
title: "Fundamental Analysis",
items: [
{
title: "The Dow Theory1",
to: "/thedowtheory"
},
{
title: "Charts & Chart Patterns",
to: "/chart"
},
{
title: "Trend & Trend Lines",
to: "/trendlines"
},
{
title: "Support & Resistance",
to: "/sandr"
}
]
},
{
title: "Elliot Wave Analysis",
items: [
{
title: "The Dow Theory",
to: "/thedowtheory"
},
{
title: "Charts & Chart Patterns",
to: "/chart"
},
{
title: "Trend & Trend Lines",
to: "/trendlines"
},
{
title: "Support & Resistance",
to: "/sandr"
}
]
}
]
},
{
icon: <TrendingUpOutlinedIcon />,
title: "Options"
},
{
icon: <DescriptionOutlinedIcon />,
title: "Blog"
}
];
ReactDOM.render(<Demo items={menu} />, document.querySelector("#root"));
Upvotes: 3
Reputation: 5288
First, you have a typo. You're looping on children
key instead of items
. But even though you fix that, your code won't still work the way you'll want.
I would simplify my approach by creating a SingleLevel
and MultiLevel
reusable components to handle the current menu item. If the current item has children
/items
then I would use the MultiLevel
component else SingleLevel
.
SingleLevel Component
const SingleLevel = ({ item }) => {
return (
<ListItem button>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
);
};
MultiLevel Component
const MultiLevel = ({ item }) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
To identify which component to use, I would then create a hasChildren
helper function that returns true
if the current item meets all my defined conditions to be considered as a parent menu item.
utils.js
export function hasChildren(item) {
const { items: children } = item;
if (children === undefined) {
return false;
}
if (children.constructor !== Array) {
return false;
}
if (children.length === 0) {
return false;
}
return true;
}
I would then abstract all of these on a another MenuItem
component.
MenuItem Component
const MenuItem = ({ item }) => {
const Component = hasChildren(item) ? MultiLevel : SingleLevel;
return <Component item={item} />;
};
And lastly, this is how will I loop the menu
items
export default function App() {
return menu.map((item, key) => <MenuItem key={key} item={item} />);
}
Upvotes: 28