Close a submenu on click another submenu reactjs - javascript

I have researched and tried to implement some of the solutions provided but I failed when trying to achieve this. I was able to make my dropdown menu and click so each submenu will open and close when its parent is clicked. I would like to have an opened submenu to be closed when a different menu is clicked, so I don´t have all of them stacked at the menu bar. Could someone point out how can I achieve this? Thank you for helping me.
Menu.js
import React from 'react'
import MenuItem from '../MenuItem';
import { SidebarData } from '../../helpers/SidebarData';
import * as C from './styles';
const Menu = () => {
return (
<C.Container>
<C.MenuArea>
{SidebarData.map((item, index) => {
return <MenuItem item={item} key={index} />;
})}
</C.MenuArea>
</C.Container>
)
};
export default Menu
MenuItem.js
import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import * as C from './styles';
const MenuItem = ({ item }) => {
const [opened, setOpened] = useState(false);
const showSubnav = () => setOpened(!opened);
return (
<C.NavUnlisted>
<NavLink to={item.path} onClick={item.subNav && showSubnav} activeClassName='current' exact={item.path === '/' ? true : false} >
<C.SidebarLink>
<div>
{item.icon}
<C.SidebarLabel>{item.title}</C.SidebarLabel>
</div>
<div>
{item.subNav && opened
? item.iconOpened
: item.subNav
? item.iconClosed
: null}
</div>
</C.SidebarLink>
</NavLink>
{opened &&
item.subNav.map((item, index) => {
return (
<NavLink to={item.path} key={index} activeClassName='current' >
<C.DropdownLink>
{item.icon}
<C.SidebarLabel>{item.title}</C.SidebarLabel>
</C.DropdownLink>
</NavLink>
);
})}
</C.NavUnlisted>
);
};
export default MenuItem;

I was able to find a solution. I added an ID for each Menu so I could change the state based upon it. I had all the menu in a single component. It didn't seem to be necessary to have a separate component for the Menu Item.
Here is my code:
import React, { useState } from 'react'
import { SidebarData } from '../../helpers/SidebarData';
import * as C from './styles';
import { NavLink } from "react-router-dom";
const Menu = () => {
const [open, setOpen] = useState('');
const toggle = (id) => setOpen(id);
return (
<C.Container>
<C.MenuArea>
{SidebarData.map((item, index) => (
<C.NavUnlisted key={index}>
<NavLink to={item.path} onClick={() => toggle(item.navId)} activeClassName='current' exact={item.path === '/' ? true : false}>
<C.SidebarLink>
<div>
{item.icon}
<C.SidebarLabel>{item.title}</C.SidebarLabel>
</div>
</C.SidebarLink>
</NavLink>
{open === item.navId && (
<div>
{item.subNav.map((item, index) => (
<NavLink to={item.path} key={index} activeClassName='current' >
<C.DropdownLink>
{item.icon}
<C.SidebarLabel>{item.title}</C.SidebarLabel>
</C.DropdownLink>
</NavLink>
))}
</div>
)}
</C.NavUnlisted>
))}
</C.MenuArea>
</C.Container>
)
};
export default Menu

Try to close the menu when you click outside your menu component, if it's a solution you're interested in you can learn more about how to achieve this in react there :
https://stackoverflow.com/a/42234988/16956436

An elegant way to handle this would be to keep track of the currently opened submenu in the Menu component and displaying/hiding the submenus depending on a prop passed down from the parent component.
import React from 'react'
import MenuItem from '../MenuItem';
import { SidebarData } from '../../helpers/SidebarData';
import * as C from './styles';
const Menu = () => {
const [currentlyOpen, setCurrentlyOpen] = useState(null);
return (
<C.Container>
<C.MenuArea>
{SidebarData.map((item, index) => {
return <MenuItem item={item} key={index} isOpen={index === currentlyOpen} onClick={() => setCurrentlyOpen(index)} />;
})}
</C.MenuArea>
</C.Container>
)
};
export default Menu
You would then call handleClick with the respective index in MenuItem.js.
import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import * as C from './styles';
const MenuItem = ({ item, onClick: handleClick }) => {
const [opened, setOpened] = useState(false);
const showSubnav = () => setOpened(!opened);
return (
<C.NavUnlisted>
<NavLink to={item.path} onClick={item.subNav && handleClick} activeClassName='current' exact={item.path === '/' ? true : false} >
<C.SidebarLink>
<div>
{item.icon}
<C.SidebarLabel>{item.title}</C.SidebarLabel>
</div>
<div>
{item.subNav && opened
? item.iconOpened
: item.subNav
? item.iconClosed
: null}
</div>
</C.SidebarLink>
</NavLink>
{opened &&
item.subNav.map((item, index) => {
return (
<NavLink to={item.path} key={index} activeClassName='current' >
<C.DropdownLink>
{item.icon}
<C.SidebarLabel>{item.title}</C.SidebarLabel>
</C.DropdownLink>
</NavLink>
);
})}
</C.NavUnlisted>
);
};
export default MenuItem;

Related

Why is Active class not shown correctly in Inspect Element when NavLink is written next to another class name?

import React from "react";
import {NavLink} from "react-router-dom";
const MenuItem = ({items}) => {
const activeClassMenuItem = ({ isActive }) => (isActive ? "active_link" : "");
return (
<>
{items.map((item) => {
return (
<li className="menu-item" key={item.id}>
<NavLink
to={item.url}
className={`menu-link ${activeClassMenuItem}`}
>
<div data-i18n={item.title}>{item.title}</div>
</NavLink>
</li>
);
})}
</>
);
};
export default MenuItem;
Show result in inspect element : ( 'active_link' class is also placed)
<li class="menu-item ">
<a class="menu-link _ref2 => { let { isActive } = _ref2; return isActive ? "active_link" : "" } active_link" href="/Login">
Login
</a>
</li>
When I try from NavLink to have the 'active_link' class next to other classes of that element, unfortunately, the output is not placed correctly in Inspect Element.
activeClassMenuItem is a function you need to do it like this (I assume item has a property called isActive):
import React from "react";
import {NavLink} from "react-router-dom";
const MenuItem = ({items}) => {
const activeClassMenuItem = ({ isActive }) => (isActive ? "active_link" : "");
return (
<>
{items.map((item) => {
return (
<li className="menu-item" key={item.id}>
<NavLink
to={item.url}
className={`menu-link ${activeClassMenuItem(item)}`}
>
<div data-i18n={item.title}>{item.title}</div>
</NavLink>
</li>
);
})}
</>
);
};
export default MenuItem;

Too many re-renders in React.JS

Hi I'm currenlty doing a side project for my portfolio and was wondering how did this error happen. It's a navbar component in react, with Link to other pages and button to set an active theme. Any Idea what is causing the infinite re-render? Thank you.
import React, {useEffect} from 'react'
import AppBar from '#mui/material/AppBar';
import Box from '#mui/material/Box'
import { Button } from '#mui/material';
import { Link } from 'react-router-dom';
const MenuBar = () => {
const [activeItem, setActiveItem] = React.useState('')
const handleItemClick = (e, {name}) => {
setActiveItem(name)
}
useEffect(() => {
const pathname = window.location.pathname
const path = pathname === '/' ? 'home' : pathname.substring(1)
setActiveItem(path)
}, [])
return (
<div>
<AppBar color='primary' position={'static'}>
<Box className='flex items-center justify-between'>
<Link to={'/'} onClick={setActiveItem('home')}>
<Button variant='text' sx={{ bgcolor: activeItem === 'home' ? '#FDFFA9' : '#FFD365'}}>Home</Button>
</Link>
<Box>
<Link to={'/login'} onClick={setActiveItem('login')}>
<Button variant='text'>Login</Button>
</Link>
<Link to={'/register'} onClick={setActiveItem('login')}>
<Button variant='text'>Register</Button>
</Link>
</Box>
</Box>
</AppBar>
</div>
)
}
export default MenuBar
I think the problem is with the way you assign onClick handlers
onClick={setActiveItem('home')}
during rendering you actually invoke the action, not assigning the callback, so
render -> setActiveItem -> render -> ..
please try to assign as the fn value instead:
onClick={() => setActiveItem('home')}

"Warning: Encountered two children with the same key" for infinite scroll

I've implemented infinite scroll in React app, however, I get Warning: Encountered two children with the same key <...>. Keys should be unique so that components maintain their identity across updates. <...>.
If I replace key={movie.id} by key={i}, infinite scroll stops working.
This is the code of my component:
import{ useContext, useRef, useEffect } from "react";
import { Card, Grid, CardActionArea, CardMedia } from '#material-ui/core';
import FavoriteBorderIcon from '#material-ui/icons/FavoriteBorder';
import { MoviesContext } from "../../services/context";
import { Movie } from "../../services/movies.service";
import '../../App.scss';
import './Catalog.scss';
import noImage from '../../images/no-image-available.png';
import loadingSpinner from '../../images/loading-spinner.gif';
import { NavLink } from 'react-router-dom';
import useIntersectionObserver from '../../services/useIntersectionObserver';
import { fetchMovies } from "../../services/movies.service";
const posterBaseUrl = "https://image.tmdb.org/t/p/w300";
const CatalogCards = () => {
const { movies, updateMovies, searchedMovie, moviesPage, setMoviesPage, setSelectedMovie, setIsMoviePageFirstTimeOpened } = useContext(MoviesContext);
const loadingRef = useRef<HTMLDivElement | null>(null);
const entry = useIntersectionObserver(loadingRef, {})
const isVisible = !!entry?.isIntersecting;
const SetSelectedMovieId = (id: number) => {
setIsMoviePageFirstTimeOpened(true);
setSelectedMovie(id);
}
useEffect (
() => {
if ( isVisible ) {
setMoviesPage(moviesPage+1);
fetchMovies(String(moviesPage))
.then(nextPage => {
updateMovies((movies: Movie[]) => movies.concat(nextPage));
})
.catch(() => updateMovies([]))
}
},
[isVisible]
);
return (
<div >
<Grid container spacing={1} className="container-content">
{
movies.length > 0
?
movies.map((movie, i) => (
<Grid item key={movie.id}>
<NavLink to={'/movie/' + movie.id}>
<Card className="card-list" onClick={() => SetSelectedMovieId(movie.id)} >
<CardActionArea>
<CardMedia
component="img"
alt={"Poster of " + movie.title}
image={movie.poster_path ? posterBaseUrl + movie.poster_path : noImage}
title={movie.title}
/>
</CardActionArea>
</Card>
</NavLink>
</Grid>
))
:
searchedMovie ?
<div className="">Try a different phrase...</div>
:
<CardMedia
component="img"
image={loadingSpinner}
className="loading-spinner"
/>
}
</Grid>
<div ref={loadingRef}>...</div>
</div>
);
}
export default CatalogCards;
fetchMovies() method fetches info about movies from an API.
useIntersectionObserver is a custom hook that helps to check if ref={loadingRef} appears in the screen and more movies should be fetched.
How can I solve it?
Thanks!

Unable to find an element by: [data-testid=“movieList”]

I need to test results in my ul from the movielistcomponent but i keep getting unbale to find element [data-testid=movieList]
My movielistComponent
</section>
{isLoaded ? (
items.length === 0 ? (
<div>No Results Found</div>
) : (
<ul className="mt-50 styled" data-testid='movieList'>
{items.map((item) => (
<li className="slide-up-fade-in py-10" key={item.id}>
{item.Title}
</li>
))}
</ul>
)
) : (
<div className="mt-50 slide-up-fade-in" data-testid="no-result"></div>
)}
</div>
My test
import React from 'react';
import App from './App';
import { render, fireEvent, cleanup, waitFor , screen} from '#testing-library/react';
import fetchMock from 'fetch-mock';
import '#testing-library/jest-dom/extend-expect';
const renderApp = () => render(<App/>);
afterEach(() => {
fetchMock.restore();
cleanup()
});
test('initial UI is rendered as expected', () => {
let { getByTestId, queryByTestId } = renderApp();
expect(getByTestId('app-input')).toHaveValue(null);
expect(getByTestId('submit-button')).toHaveTextContent("Search");
expect(screen.getByTestId('movieList').childNodes).toHaveLength(0);
expect(queryByTestId('no-result')).toBe(null);
});

How to show informations with triggering the show/close button in React?

I am in the progress of learning React. I want to show countries' information via the toggleable button. But I have some problem with that.
There is an input box that is triggered by entering letters. I send HTTP Get Request depends on this input and the response is being filtered. The value which is filtered appears on the screen.
Ass you see, I just want the country name and button to appear. After that, when I press the button, only information about that country should come.
My code:
App.js
import React from 'react'
import Countries from './components/Countries'
const App = () => {
return (
<div>
<Countries />
</div>
)
}
export default App
Countries.js
import React, { useState,useEffect} from 'react'
import ShowSection from './ShowSection'
import axios from 'axios'
const Countries = (props) => {
const [search,setSearch] = useState('')
const [countries,setCountries] = useState([])
useEffect(()=> {
axios
.get('https://restcountries.eu/rest/v2/all')
.then((response) => {
console.log("Burda")
const responseCountries = response.data
const filter = responseCountries.filter(el =>
el.name.toLowerCase()
.indexOf(search.toLocaleLowerCase()) > -1)
setCountries(filter)
})
},[search])
const handleInput = (event) => {
setSearch(event.target.value)
console.log(countries)
}
return(
<div>
find countries <input onChange={handleInput}/>
<div>
<ShowSection list={countries}/>
</div>
</div>
)
}
export default Countries
ShowSection.js
import React from 'react'
import InfoSection from './InfoSection'
const ShowSection = (props) => {
const {list} = props
var id = 0;
if(list.length === 1){
return(
<div>
{
list.map((item,index) =>
<div>
<h2>{item.name}</h2>
<p>capital {item.capital}</p>
<p>population {item.population}</p>
<h3>languages</h3>
<ul>
{item.languages.map(m =>
<li key={index.toString()}>{m.name}</li>)}
</ul>
<img alt="Flag" src={item.flag} width="150px" height="150px"/>
</div>
)
}
</div>
)
}
else if(list.length <= 10){
return(
list.map((item,i) =>
<div>
<InfoSection key={item.id} item={item} num={++id}/>
</div>
)
)
}
else{
return(
<div>Nothing to rendered</div>
)
}
}
export default ShowSection
InfoSection.js
import React,{useState} from 'react'
const InfoSection = (props) => {
const {item} = props
const [toggle,setToggle] = useState(false)
return(
<div>
{item.name}
<button onClick={() =>setToggle(!toggle)}>
{toggle ? 'Cancel' : 'Show'}
</button>
<p>capital {item.capital}</p>
<p>population {item.population}</p>
<h3>languages</h3>
<ul>
{item.languages.map(m =>
<li key={item.callingCodes}>{m.name}</li>)}
</ul>
<img alt="Flag" src={item.flag} width="150px" height="150px"/>
</div>
)
}
export default InfoSection
Like #GG mentioned in the comments, you can use conditional rendering to display the details of the country when toggle is true/false.
Like this
return(
<div>
{item.name}
<button onClick={() =>setToggle(!toggle)}>
{toggle ? 'Cancel' : 'Show'}
</button>
{toggle &&
<>
<p>capital {item.capital}</p>
<p>population {item.population}</p>
<h3>languages</h3>
<ul>
{item.languages.map(m =>
<li key={item.callingCodes}>{m.name}</li>)}
</ul>
<img alt="Flag" src={item.flag} width="150px" height="150px"/>
</>
}
</div>
)

Categories

Resources