How to map routes with individual ListItem in reactjs? - javascript

Im trying to use material Ui with my react frontend project. I have implemented the AppBar and drawer component and they are working just fine. This is what I have achieved till now :
export default function MiniDrawer() {
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<div className={classes.root}>
<CssBaseline />
<AppBar style={{background: 'black'}}
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography className={classes.mystyle} variant="h5" >
CodeBasics
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
open={open}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
<List>
{['Home', 'Algorithms', 'DataStructures'].map((text, index) => (
<ListItem button key={text}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
<Divider />
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
<Layout>
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/DataStructures" component={DataStructures} />
</Switch>
</Router>
</Layout>
</main>
</div>
);
}
Now I want to map each 'Home', 'DataStructures' and 'Algorithms' with a different route. I have achieved a way to a route the List component in this way.
<List>
{['Home', 'Algorithms', 'DataStructures'].map((text, index) => (
<ListItem button key={text} component="a" href="www.google.com">
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
but with its only getting me go to the same link for every item in the list. How do I map individual list items with individual routes?

I've put together a small sample. This structure works for me for all of my projects and quite fast to implement once you've got the hang of it.
Demo link
Route Structure
This pathIds variable is an object which holds all id values for routes. This will be useful when needing a reference to auto generate breadcrumb links.
const pathIds = {
home: 'home',
...
}
This pathRouting variable is an object which holds all path routing. Just path routing.
const pathRouting = {
home: '/home',
...
}
Primary structure object:
const pageRoutes = {
[pathIds.home]: {
path: pathRouting.home,
sidebarName: 'Homepage',
icon: Dashboard,
noRender: false,
component: Home,
},
...
}
This is primary structure object, holding all necessary info to generate onto HTML (sidebar, breadcrumbs, routes etc).
path: get the path value from pathRouting object. (Required)
sideName: text showing on Sidebar. (Required)
icon: usually this icon only shows on the sidebar. You can omit this if your menu/sidebar/navigation doesn't need it. (Optional)
noRender: boolean to indicate whether this object will be rendered to the sidebar. This is useful for routes that doesn't need to directly access (like a 404 Page Not Found page) (Optional)
component: The React component which will be used to render. (Required)
Then, in your app.js, you import pageRoutes as usual. Before doing mapping or iteration work, you might need to convert this object to an array (or use a library like lodash to directly iterating on the object).
const routeArray = Object.values(pageRoutes);
Then:
<Switch>
{routeArray.map((prop, key) => {
return (
<Route
path={prop.path}
component={prop.component}
exact={prop.exact || false}
key={`route-${key}`}
/>
);
})}
<Route component={pageRoutes[pathIds.error404].component} />
</Switch>
Notice the very last line where I explicitly declare a fallback <Route /> path in case user is on a wrong/ not defined path, that's how useful an object is in this kind of situation. It doesn't need path prop.
You sidebar is just a simple component receives a list of routes as props (as converted in app.js), then it can be used to show on the view.
<List component="nav" aria-label="main mailbox folders">
{routes.map(({ path, noRender, sidebarName, ...prop }, index) => {
if (noRender) return null;
return (
<NavLink to={path} key={`route-${index}}`}>
<ListItem button>
<ListItemIcon>
<prop.icon />
</ListItemIcon>
<ListItemText primary={sidebarName} />
</ListItem>
</NavLink>
);
})}
</List>
You will see it only renders Homepage, Inbox and Reset Password on sidebar. But you can directly enter /register to make <Register /> show up. The same for /page-not-found. Or even when you enter a wrong address like /register-user, the <PageNotFound /> component will be used for fallback case.
You might wonder why I don't put all path routes and path Ids in its own place, why do I need to move them into a separate object? The answer is, in my case I usually need quick access to those values. This might be useful for generating breadcrumbs or do some iteration without pulling the whole object. Depend on your specific project needs, those two might be shortened.
For full code, you can visit here: Sample Link

I have found a way to achieve this.
First I created an array of objects like:
const topics= [{
topic: "Home",
path: "home"
},
{
topic: "DataStructures",
path: "datastructures"
}]
then I did this:
<List>
{topics.map((text, index) => (
<ListItem button key={text.topic} component="a" href={text.path === "home"? "\\":text.path}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text.topic} />
</ListItem>
))}
</List>

Related

MUI Persistent Drawer - Routing Issues

I want to create a persistent drawer using the material-ui library on react. I have created a partial navbar and i was testing if i could route to other pages if i click the buttons. The address routes properly if i click on the buttons, however, i am unable to see the content of those pages. This is my first time working with react in general, so i am not sure where im going wrong and i would really appreciate some assistance.
This is the code i have so far:
App.js:
function App() {
return (
<div>
<Box sx={{display: 'flex'}}>
<NavBar>
<Routes>
<Route exact path='/' element={<Navigate to='/home' />} />
<Route exact path='/home' element={<HomePage />} />
<Route exact path='/clients' element={<ClientsPage />} />
<Route exact path='/resources' element={<ResourcesPage />} />
<Route exact path='/projects' element={<ProjectPage />} />
</Routes>
</NavBar>
</Box>
</div>
)
}
export default App;
index.js:
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);
NavBar.js:
const drawerWidth = 240;
const useStyles = makeStyles({
drawer: {
width: drawerWidth
},
drawerPaper: {
width: drawerWidth
},
root: {
display: 'flex'
}
})
export const NavBar = () => {
const classes = useStyles()
const navigate = useNavigate()
const [open, setOpen] = useState(true)
const menuItems = [
{
text: 'Home',
icon: <Home sx={{ color: orange[500] }} />,
path: '/home',
component: <HomePage />
},
{
text: 'Projects',
icon: <Work sx={{ color: orange[500] }} />,
path: '/projects',
component: <ProjectPage />
},
{
text: 'Clients',
icon: <People sx={{ color: orange[500] }} />,
path: '/clients',
component: <ClientsPage />
},
{
text: 'Resources',
icon: <Settings sx={{ color: orange[500] }} />,
path: '/resources',
component: <ResourcesPage />
}
]
return (
<>
<Grid container>
<div className={classes.root}>
<Drawer className={classes.drawer} variant="persistent" anchor="left" open={open} classes={{paper: classes.drawerPaper}}>
<div>
<Typography variant="h5">
Navigation
</Typography>
<List>
{menuItems.map(item => (
<ListItem key={item.text} button onClick={() => navigate(item.path)}>
<ListItemIcon> {item.icon} </ListItemIcon>
<ListItemText primary={item.text} />
</ListItem>
))}
</List>
</div>
</Drawer>
</div>
</Grid>
</>
)
}
The HomePage, and all the other pages being called is just a header tag which says Hello . I cannot see the Hello when i click on the buttons. Thank you guys for the help!
After looking at my code for a while, i figured what the error was. The Navbar was routing to the page, which was right, but i was not displaying the contents of the page. The great part was I was unconsciously wrapping my router and all the routes in NavBar component, so all i had to do was pass a prop called children like this:
export const NavBar = ({children}) => {
//code here
}
and then after i finish with the drawer, just add a small div where i display the children. So it would look like this:
</Drawer>
<div>
{children}
</div
The UI is still a bit messy, but the content of the pages are being shown.

Clicking a button shows the the url but not taking me to the page I wanted

I'm using useHistory() for routing different page
I'm using materialui drawer and when clicking to the listitem button I want to go to another page
but its only showing me the url but not taking me to the page
<List>
{sec.map((text, index) => (
<ListItem
button
key={text.title}
onClick={() => history.push(text.path)}>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText
primary={text.title} />
</ListItem>
))}
</List>
In app.js I create route :
<BrowserRouter >
< SideDrawer />
<Switch>
<Route path="/" component={Home} exact />
<Route path="/design" component={Design} />
<Route path="/technology" component={Technology} />
</Switch>
</BrowserRouter>
in hooks use this approach
import { useHistory} from "react-router-dom";
const yourComponent = () => {
const history = useHistory();
const yourPath = '/your/url/path';
const openUrl = () =>{ // handler to open url
history.push({
pathname: yourPath
})
}
}
This shall fix your issue
import { useHistory } from "react-router-dom";
const HomeButton = () => {
const history = useHistory();
const handleClick = () => {
// add your path here
history.push("/home");
};
return (
<List>
{sec.map((text, index) => (
<ListItem button key={text.title} onClick={handleClick}>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text.title} />
</ListItem>
))}
</List>
);
};
export default HomeButton;

Link collision in React Router

I'm trying to implement 'Not Found' page to redirect user if the url is not valid. However, not found page link somehow collides with search results and redirects user to 'Not Found' after submitting search input.
'Search', 'Result' and 'Not Found' are different components and links
are different.
Browser Router code in parent app component
(State for search results defined in here )
const [searchResult, setSearchResult] = useState([]);
<BrowserRouter>
<Switch>
<Route path=''>
<NotFound />
</Route>
<Route path='*'>
<Redirect to ='/404' />
</Route>
<Route path='/search'>
<Result
searchResult={searchResult}
setSearchResult={setSearchResult}
/>
</Route>
</Switch>
</BrowserRouter>
In the current situation, search bar component works and shows
results if I remove 'Not Found' from browser router.
Search results are displayed in 'Result' component.
Search Component
(If I remove first '/search', from history it doesn't work.)
Search result link
> http://localhost:3000/search/search?q=adidas
Item is available and can be found if I remove 'Not Found' from browser router. Else, paths collide and redirects to 'Not Found Component'
function Search({setSearchResult}) {
const history = useHistory();
const location = useLocation();
const searchInput = useRef();
const params = new URLSearchParams(location.search);
const q = params.get('q');
function handleSubmit(e){
e.preventDefault();
history.push(`/search/search?q=${searchInput.current.value}`)
}
useEffect(()=>{
if(q){
searchInput.current.value=q ? q : '';
const productSearch = products.results
.filter(item => item.title.toLowerCase().includes(q.toLowerCase()))
.map((item)=>
<Col sm={4} key={item.id} className="mt-3">
<Link to ={`/ProductDetails/${item.id}`} >
<Card>
<Card.Img variant="top" src={item.src[0]}/>
<Card.Body className="text-dark text-center">
<Card.Title className="font-secondary">{item.title}</Card.Title>
<Card.Text className="font-secondary">
{item.detail}
</Card.Text>
</Card.Body>
</Card>
</Link>
</Col>
);
setSearchResult(productSearch);
}
},[q])
return (
<>
<Form onSubmit={handleSubmit} inline>
<FormControl
htmlFor='search'
type="text"
id="search"
placeholder="Search items"
ref={searchInput}
name={q}
/>
<Button type='submit' id="searchBtn" onClick={handleSubmit} className="font-secondary" >{searchIcon}</Button>
</Form>
</>
)
}
export default Search
Result Component
Search results are displayed in here.
function Result({searchResult}) {
return (
<>
<Container>
<Row className="mt-5">
<h2 className="font-display">Search Results</h2>
</Row>
<Row>
{searchResult}
</Row>
</Container>
</>
)
}
export default Result
So I need to prevent that collision and make components work.
React Router will use the first component that it matches in the Switch statement, just like a regular switch statement.
So the NotFound and 404 components should be moved to the end of the Switch component, with search, and whatever other routes you're going to want, above them

Sharing component states with useState() for active link in Nav

I'm trying to learn react and fairly new to the framework. I am trying to create a simple navbar component wih material-ui that is responsive (will show all links on medium devices and up, and open a side drawer on small devices). I have most of it setup to my liking, however, the issue I am currently having, is getting and setting the active link according to the page I am on.
It seems to works correctly on the medium devices and up, but when transitioning to a smaller device, the link is not updated correctly, as it will keep the active link from the medium screen set, while updating the side drawer active link.
Navbar.js
const Navbar = () => {
const classes = useStyles();
const pathname = window.location.pathname;
const path = pathname === '' ? '' : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
return (
<>
<HideNavOnScroll>
<AppBar position="fixed">
<Toolbar component="nav" className={classes.navbar}>
<Container maxWidth="lg" className={classes.navbarDisplayFlex}>
<List>
<ListItem
button
component={RouterLink}
to="/"
selected={selectedItem === ''}
onClick={event => handleItemClick(event, '')}
>
<ListItemText className={classes.item} primary="Home" />
</ListItem>
</List>
<Hidden smDown>
<List
component="nav"
aria-labelledby="main navigation"
className={classes.navListDisplayFlex}
>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === 'account/login'}
onClick={event => handleItemClick(event, 'account/login')}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/register"
selected={selectedItem === 'account/register'}
onClick={event => handleItemClick(event, 'account/register')}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</Hidden>
<Hidden mdUp>
<SideDrawer />
</Hidden>
</Container>
</Toolbar>
</AppBar>
</HideNavOnScroll>
<Toolbar id="scroll-to-top-anchor" />
<ScrollToTop>
<Fab aria-label="Scroll back to top">
<NavigationIcon />
</Fab>
</ScrollToTop>
</>
)
}
SideDrawer.js
const SideDrawer = () => {
const classes = useStyles();
const [state, setState] = useState({ right: false });
const pathname = window.location.pathname;
const path = pathname === "" ? "" : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
const toggleDrawer = (anchor, open) => (event) => {
if (
event &&
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setState({ ...state, [anchor]: open });
};
const drawerList = (anchor) => (
<div
className={classes.list}
role="presentation"
onClick={toggleDrawer(anchor, false)}
onKeyDown={toggleDrawer(anchor, false)}
>
<List component="nav">
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/login"}
onClick={(event) => handleItemClick(event, "account/login")}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/register"}
onClick={(event) => handleItemClick(event, "account/register")}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</div>
);
return (
<React.Fragment>
<IconButton
edge="start"
aria-label="Menu"
onClick={toggleDrawer("right", true)}
>
<Menu fontSize="large" style={{ color: "white" }} />
</IconButton>
<Drawer
anchor="right"
open={state.right}
onClose={toggleDrawer("right", false)}
>
{drawerList("right")}
</Drawer>
</React.Fragment>
);
};
Code Sandbox - https://codesandbox.io/s/async-water-yx90j
I came across this question on SO: Is it possible to share states between components using the useState() hook in React?, which suggests that I need to lift the state up to a common ancestor component, but I don't quite understand how to apply this in my situation.
I would suggest to put aside for a moment your code and do a playground for this lifting state comprehension. Lifting state is the basic strategy to share state between unrelated components. Basically at some common ancestor is where the state and setState will live. there you can pass down as props to its children:
const Parent = () => {
const [name, setName] = useState('joe')
return (
<>
<h1>Parent Component</h1>
<p>Child Name is {name}</p>
<FirstChild name={name} setName={setName} />
<SecondChild name={name} setName={setName} />
</>
)
}
const FirstChild = ({name, setName}) => {
return (
<>
<h2>First Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Mary')}>My Name is Mary</button>
</>
)
}
const SecondChild = ({name, setName}) => {
return (
<>
<h2>Second Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Joe')}>My Name is Joe</button>
</>
)
}
As you can see, there is one state only, one source of truth. State is located at Parent and it passes down to its children. Now, sometimes it can be troublesome if you need your state to be located at some far GreatGrandParent. You would have to pass down each child until get there, which is annoying. if you found yourself in this situation you can use React Context API. And, for most complicated state management, there are solutions like redux.

How can you use JSON data to generate menu components dynamically using map?

Im trying to dynamically create a set of menuitems, using data pulled from json. I am currently trying to do this by mapping values with props, but i am failing somewhere.
Here is the code i use to attempt to do this:
Const Generate = () => {
{data.items.map(({id, url, title}) => (
<MenuItem key={id}>
<Link to={url}> {title} </Link>
</MenuItem>
))
console.log('lol')}
}
and here is the component that is trying to utlize this. Commented out is out it looks and works well whilst hardcoded.
const Navigation = ({}) => (
<React.Fragment>
<Layout>
<Sider
breakpoint="xs"
collapsedWidth="0"
onBreakpoint={broken => {
}}
onCollapse={(collapsed, type) => {
}}
>
<div className="logo">
<h1 style={{ color: 'white', paddingLeft: 20, paddingTop: 26}}>
{Generate}
</h1>
</div>
<Menu theme="dark" mode="inline" defaultSelectedKeys={['1']}>
{Generate()}
{/* <Menu.Item key="1">
<Link to="/" > Hjem </Link>
</Menu.Item>
<Menu.Item key="2">
<Link to="/skjema"> Mine Skjema </Link>
</Menu.Item>
<Menu.Item key="3">
<Link to="/pasient"> Mine Pasienter </Link>
</Menu.Item>
<Menu.Item key="4">
Søk
</Menu.Item>
<Menu.Item key="5">
Filtrer
</Menu.Item> */}
</Menu>
</Sider>
<Layout>
<Header className="site-layout-sub-header-background" style={{ padding: 0 }} />
<Content style={{ margin: '24px 16px 0' }}>
<div className="content">
<Switch>
<Route exact path={"/"} component={Dashboard} />
<Route exact path="/Skjema" component={MineSkjema} />
<Route exact path="/Pasient" component={MinePasienter} />
</Switch>
</div>
</Content>
<Footer />
</Layout>
</Layout>
</React.Fragment>
)
export default withRouter(Navigation);
for reference, here is the json structure:
const data =
{
"items": [
{
"id": 1,
"url": "/",
"title": "Hjem"
},
{
"id": 2,
"url": "/Skjema",
"title": "Mine Skjema"
},
{
"id": 3,
"url": "/Pasient",
"title": "Hjem"
},
{
"id": 4,
"url": "/Search",
"title": "Søk"
}
]
}
Where am I making a mistake?
Try something like this:
<Menu theme="dark" mode="inline" defaultSelectedKeys={['1']}>
{data.items.map(({id, url, title}) => (
<MenuItem key={id}>
<Link to={url}> {title} </Link>
</MenuItem>
)}
</Menu>
Or, if you want use the function, call it:
const Generate = items =>
items.map(({id, url, title}) => (
<MenuItem key={id}>
<Link to={url}> {title} </Link>
</MenuItem>
)
;
So:
{Generate(data.items)}
Pass parameter is a good idea because it makes Generate a pure function!
you may want to use .map() function when you're mapping over a list of objects, and in your case, why don't you try something like this ?
Const Generate = () => {
{data.items.map((e,i) => (
<MenuItem key={i}>
<Link to={e.url}> {e.title} </Link>
</MenuItem>
))}
}
This should be working for you.
It looks like you have the right idea. The first thing that pops out to me is that Generate is a function, but you never call it. Instead you have {Generate} which evaluates the function object directly, not what it returns. Instead you probalby should have {Generate()}.
I suggest you debug your code more by adding more console.log() calls. Read this article for more tips on debuging your code.
Another problem I see is in the syntax for your Generate() function:
Const Generate = () => {
{data.items.map(({id, url, title}) => (
<MenuItem key={id}>
<Link to={url}> {title} </Link>
</MenuItem>
))
console.log('lol')}
}
In particular, I think you have some extra braces. The correct syntax should be something like this:
const Generate = () => {
console.log('lol');
return data.items.map(({id, url, title}) => (
<MenuItem key={id}>
<Link to={url}> {title} </Link>
</MenuItem>
));
}
There is only one set of curly braces enclosing the body of the fat arrow function. The syntax here is (<params>) => {<statements>} where <params> is the list of parameters and <statements> is the list of statements to execute.
When using curly braces around the statements in a fat arrow function, you must include a return statement in order to return a result. This is necessary because of the console.log() statement. If there is only one statement, then the curly braces can be removed and the single statement doesn't need a return:
const Generate = () =>
data.items.map(({id, url, title}) => (
<MenuItem key={id}>
<Link to={url}> {title} </Link>
</MenuItem>
));

Categories

Resources