How to manage state for navbar with multiple dropdown items? - javascript

i have navbar with 2 dropdowns that are managed with state. My issue is that when i click on one because both dropdowns open and close at the same time.
How can i make sure only the dropdown clicked on is open ?
const [show, setShow] = useState(false);
const [show1, setShow1] = useState(false);
menuItems.map((item) => {
console.log(item.href, item.submenuItems)
return (
item.submenu === true ?
<li key= {item.id }className="nav-item dropdown"
ref={ref}>
<Link
key={item.id}
href={item.href}
passHref
spy={true}
smooth={true}
offset={-70}
duration={500}
//className="nav-item"
>
<a
onClick={() => {
setShow(!show)
}}
className="nav-link rounded dropdown-toggle
dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded={show ? "true" : "false"}
id="navbarDropdown"
role="button"
>
{item.title}
</a>
</Link>

As mentioned earlier, You would need to create a separate file to track the individual mapped item as currently, you are mapping all the items to the same useState hook. So, it will toggle for all since its being used by each item. As you can see from the above example code you provided inside the <a> tag
onClick={() => setShow(!show)}
Just create a new file such as MenuItem.js
MenuItem.js
In this file you would need to add over the HTML code you are generating for the mapped data and also add the useState Hook to this file
import { useState } from "react";
export default HeaderItem = ({ item }) => {
const [show, setShow] = useState(false);
return (
<li key={item.id} className="nav-item dropdown">
<a
onClick={() => {
setShow(!show);
}}
className="nav-link rounded dropdown-toggle
dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded={show ? "true" : "false"}
id="navbarDropdown"
role="button"
>
{item.title} - {`${show}`}
</a>
{show && item.submenu
? item.submenuItems.map((subitem) => (
<ul>
<li>{subitem.id}</li>
</ul>
))
: ""}
</li>
);
};
File where you are mapping the data probably
After that you would need to make your changes in your current file where you are mapping the data. I took the liberty to copy the data off of the sandbox link you shared and used it to go over items and when clicked it should display the inside submenu contents for that item.
// import "./styles.css";
import HeaderItem from "./HeaderItem";
export default function App() {
const menuItems = [
{
id: "home",
title: "home",
href: "/",
submenu: false,
submenuItems: []
},
{
id: "services",
title: "services",
href: "/#services",
submenu: true,
submenuItems: [
{
id: "conciergerie",
title: "conciergerie",
href: "/ourServices/conciergerie"
},
{
id: "property management",
title: "property management",
href: "/ourServices/conciergerie"
}
]
},
{
id: "properties",
title: "properties",
href: "/#properties",
submenu: true,
submenuItems: [
{
id: "buy",
title: "buy",
href: "https://www.idealista.com"
},
{
id: "rent",
title: "rent",
href: "https://www.idealista.com"
}
]
},
{
id: "about-us",
title: "about us",
href: "/#about-us",
submenu: false,
submenuItems: []
},
{
id: "team",
title: "team",
href: "/#team",
submenu: false,
submenuItems: []
},
{
id: "contact",
title: "contact",
href: "/#contact",
submenu: false,
submenuItems: []
}
];
const headerItems = menuItems.map((item) => {
return item.submenu === true ? <HeaderItem item={item} /> : null;
});
return <div className="App">{headerItems}</div>;
}

Add your state inside your map so that map has its own state. Make sure you delete it from the main component ;)
menuItems.map((item) => {
console.log(item.href, item.submenuItems)
const [show, setShow] = useState(false);
return (
...

You don't need to (and should not) set a state inside any loop.
You could just give one state to one dropdown and one state to the other.
const [dropdownOpen1, setdropdownOpen1] = useState(false)
const [dropdownOpen2, setdropdownOpen2] = useState(false)
As mentioned above, you create two separate dropdown components, with each dropdown having its own state per component.
This way each dropdown has its own state, and you can't reuse that state for another dropdown.

Related

How can I do dynamic badge on React CoreUI?

I have a problem with a badge on Core UI. I have a Sidebar and one of the elements is Chat. When a new message comes, the badge must show new message. But the old developer have written different ways and I can not change it. I cannot find anything for this.
My codes
Sidebar elements
const _nav = [
{
_tag: "CSidebarNavItem",
name: "Chat",
to: "/chat",
filter: "feedbacks",
icon: "cil-speech",
badge: {
color: "info",
text: "NEW MESSAGE",
},
},
]
My React component
import navigation from "./_nav";
const [filteredNav, setFilteredNav] = useState([]);
const [chatBadge, setChatBadge] = useState(false);
const handleChatBadge = () => {
setChatBadge(true)
}
// it is a sidebar element for user-role
useLayoutEffect(() => {
allwedMenu().then((res) => {
const arr = [navigation[0]];
res.data.items.forEach((element) => {
arr.push(element.name);
});
setFilteredNav(navigation.filter((item) => arr.includes(item.filter)));
});
}, []);
<CSidebarNav>
<CCreateElement
items={filteredNav}
components={{
CSidebarNavDivider,
CSidebarNavDropdown,
CSidebarNavItem,
CSidebarNavTitle,
}}
/>
</CSidebarNav>
I need the badge to show when chatBadge is true. But I don't know how can I write this.
You can only add a condition to show the badge when chatBadge is true.
Based on the Value of ChatBadge, you can use the property of the Component CSideBarNavItem to display the badge and pass the colour and text properties.
Here's the updated code:
<CSidebarNav>
<CCreateElement
items={filteredNav}
components={{
CSidebarNavDivider,
CSidebarNavDropdown,
CSidebarNavItem,
CSidebarNavTitle,
}}
/>
{filteredNav.map((item, index) => (
<CSidebarNavItem
key={index}
name={item.name}
to={item.to}
icon={item.icon}
badge={
item.name === "Chat" && chatBadge
? { color: "info", text: "NEW MESSAGE" }
: null
}
/>
))}
</CSidebarNav>
Hope it helps.

The color of the navlinks doesn't change even though the state changes to dark mode

I am using react for the first time, and I am facing this error. (Sorry for this mess of a code)
App.js
const btnMode = {
lightBtn: <LightModeIcon style={{color: "white", marginRight: "1.25rem",}} />,
darkBtn: <NightsStayIcon style={{color: "#395B64", marginRight: "1.25rem",}} />,
}
const [linkStyle, setLinkStyle] = useState({
color: "black",
});
const [btnStyle, setBtnStyle] = useState(btnMode.darkBtn);
const themeHandler = () => {
if(btnStyle.type.type.render.displayName === "NightsStayIcon") //to change from light to dark mode
{
setBtnStyle(btnMode.lightBtn);
setLinkStyle({
color: "white !important",
});
}
else{
setBtnStyle(btnMode.darkBtn);
setLinkStyle({
color: "black",
});
}
}
return(
<div className="App">
<Header style={linkStyle}/>
<div onClick={themeHandler}>
{ btnStyle }
</div>
</div>
);
I sent the state of the linkStyle to Header component which should change accordingly to the state of the btnStyle.
Header.js
function Header(props) {
console.log(props.style) // {color: "white !important"} when state changes accordingly
const navLinks = [
{ id: 0, body: "Home", link: "/" },
{ id: 1, body: "About", link: "/about" },
{ id: 2, body: "Articles", link: "/articles" },
{ id: 3, body: "Social", link: "/social" },
{ id: 4, body: "Contact", link: "/contact" },
];
return (
<div className="navContent">
{navLinks.map((item) => (
<a href={item.link} key={item.id} style={props.style}>
{item.body}
</a>
))}
</div>
)
In a tag, I cant get the style required even though I can see that in the console, the property does reflect the state change. What am I doing wrong here?
you are not calling the function themeHandler() when you want to change the theme.
ideally you should call this function as well whenever you want to switch to the theme.

Getting content of currently active Text component wrapped inside popover of antd

I am using antd components for my react app. I have a Text component wrapped inside of Popover component. Now in my case this Popover is applied to one particular column of table, i.e. every row-element in that column has a Popover component rendered for it upon mouse hovering.
title: "Name",
dataIndex: "name",
key: "name-key",
sortType: "string",
sortDirections: ["descend", "ascend"],
sorter: (a, b) => a.name.length - b.name.length,
render: (text, record) => (
<Popover>
<Text onMouseOver={handleOnMouseOverCommitId}> {name} </Text>
</Popover>
)
I want to get hold of the row-element's value, the one contained by the above Text component whenever I hover over it. In this case the value denoted by {name} above.
I tried getting it with e.target.value via onMouseOver event, but it returned undefined.
I think I get the reason behind it, because the event.target returns an html node of type <span>.
With a normal div element e.target.value has worked in the past for me. But doing the same thing with a predefined component like antd's Text seems a bit trickier.
Just to elaborate, the Popover has two buttons and based on which button user clicks, I need to render some other components, something like an overlay component.
But in order to do that I would also need to get hold of the text value which originally triggered the Popover.
Below is the code(most of the things removed for preciseness).
record.name is what I ultimately need to capture.
<Popover
content={
<>
<Space>
<Button onClick={showSomeOverlayPaneForName}>
{"View Details for record.name"}
</Button>
<Button href={"https://abc.xyz.com/" + record.role}>
{"View Role Details"}
</Button>
</Space>
</>
}
trigger={"hover"}
>
<Text style={{"color": blue.primary}} copyable={true} onMouseOver={handleOnMouseOverName}>{record.name}</Text>
</Popover>
The handleOnMouseOverName function(which doesn't work anyway) :
const handleOnMouseOverName = (e) => {
//console.log("e.target.value :--- ", e.target.value);
setCurrentActiveName(e.target.value)
}
And once my currentActiveName variable is set(via useState), I use that value inside my function showSomeOverlayPaneForName
const showSomeOverlayPaneForName = (e) => {
axios
.get(
`some-url`,
{
params: {name: currentActiveName}
}
)
.then((response) => {
setData(response.data);
}).catch(reason => {
//xyz
});
}
You need to pass on the record of the enclosing render function to the handleOnMouseOverName function.
Check the following example
import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { Space, Table, Button, Popover } from 'antd';
const App = () => {
const data = [
{
key: '1',
name: 'John Brown',
address: 'New York No. 1 Lake Park',
role: 'admin',
},
{
key: '2',
name: 'Jim Green',
address: 'London No. 1 Lake Park',
role: 'user',
},
{
key: '3',
name: 'Joe Black',
address: 'Sidney No. 1 Lake Park',
role: 'manager',
},
];
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
const content = (
<>
<Space>
<Button
onClick={() => {
viewDetail(record);
}}
>
{'View Details for ' + record.name}
</Button>
<Button href={'https://abc.xyz.com/' + record.role}>
{'View Role Details'}
</Button>
</Space>
</>
);
return (
<>
<Popover content={content} title="Details">
<div
onMouseOver={() => {
handleOnMouseOverName(record);
}}
>
{name}
</div>
</Popover>
</>
);
},
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
];
const handleOnMouseOverName = (record) => {
console.log(record);
};
const viewDetail = (record) => {
console.log(record);
};
return <Table columns={columns} dataSource={data} />;
};
export default App;
Output:
I hope this helps.
From antd docs: https://ant.design/components/popover/#header
Apparently you're supposed to render the <Popover/> with a content={content}-prop
For example
const content = <div>Content to render under title</div>
const App = () => {
const value = "Text to hover";
return (
<Popover content={content} title="Title">
<Text>{value}</Text>
</Popover>
)
}

Why I can't scroll a div into view?

So I have a sidebar, and when I click on one of the items, it should scroll to div with a certain name. It worked before, but I decided to add 'active' class to the item clicked to change its color, and it stopped scrolling to divs.
Sidebar.jsx file:
import React, { useState} from 'react';
import './Sidebar.scss';
import { SlidebarData } from "./SlidebarData";
import spike from '../../assets/spike_pic.jpg';
const Sidebar = () => {
const [sideBar, setSidebar] = useState(false);
const [selectedLink, setSelectedLink] = useState("");
return (
<div className="sidebar">
<span className="btn" onClick={() => setSidebar(!sideBar)}>Menu</span>
<div className="profile">
<img src={spike}/>
<span>Alim Budaev</span>
<span>Available for work</span>
</div>
<ul className="sidebarlist" id={sideBar ? "hidden" : ""}>
{SlidebarData.map((val,key) =>{
return (
<li
className={`row ${selectedLink === val.link ? "active" : ""}`}
id={val.link}
key={key}
onClick={()=> {
setSelectedLink(val.link);
document.getElementById(val.link).scrollIntoView();
}}>
{""}
<div>
{val.title}
</div>
</li>
);
})}
</ul>
</div>
);
}
export default Sidebar;
As you see, I have a setState hook for selectedlink, which adds active class, but scrollIntoView doesn't work because of it, and I can't figure out why.
SidebarData.jsx, where I get links from:
export const SlidebarData = [
{
title: "Home",
link: "home"
},
{
title: "About",
link: "about"
},
{
title: "Services",
link: "services"
},
{
title: "Contact",
link: "contact"
}
]

Map function is not working properly when I hit onClick

So I am making this project in ReactJs, which has a sidebar, where I am trying to implement dropdown menu.
Required Behavior
If I click in any of the option of the sidebar, if it has a submenu, it will show. And close upon again clicking.
Current Behavior
If I click any of the options, all the submenus are showing at once.
For example if I click publications option, it shows me all the options, such as featured publications, journal publications.
How do I fix that?
My sidebarItems array
const sidebarItems = [
{
title: "Publications",
url: "#",
subMenu: [
{
title: "Journal Publications",
url: "#",
},
{
title: "Featured Publications",
url: "#",
},
],
},
{
title: "Team Members",
url: "#",
subMenu: [
{
title: "Current Members",
url: "#",
},
{
title: "Lab Alumni",
url: "#",
},
],
},
{
title: "Projects",
url: "#",
subMenu: [
{
title: "Project 1",
url: "#",
},
{
title: "Project 2",
url: "#",
},
{
title: "Project 3",
url: "#",
},
],
},
{
title: "News",
url: "#",
},
{
title: "Contact Us",
url: "#",
},
];
export default sidebarItems;
The Sidebar Component
import { useState } from "react";
import { Box, Text } from "#chakra-ui/react";
import sidebarItems from "./sidebarItems";
export default function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Box>
{sidebarItems.map((items) => {
return (
<Box
width='200px'
height='40px'
textAlign='center'
cursor='pointer'
onClick={() => {
setIsOpen(!isOpen);
}}
>
<Text>{items.title}</Text>
{isOpen
? items.subMenu?.map((item) => {
return <Text>{item.title}</Text>;
})
: ""}
</Box>
);
})}
</Box>
</div>
);
}
You have to use an array state variable. Your single state variable isOpen is dictating all the subMenus here:
{isOpen
? items.subMenu?.map((item) => {
return <Text>{item.title}</Text>;
})
: ""}
You need to have an array state variable here, so each sidebar item has a corresponding boolean to dictate opening/closing of value.
const [isOpen, setIsOpen] = useState(Array(sidebarItems.length).fill(false));
Now you have to ensure that you are setting it correctly and manipulating the right array element.
onClick={() => {
let newIsOpen = [...isOpen];
newIsOpen[index] = !isOpen[index];
setIsOpen(newIsOpen);
}}
I hope this helps you reach your solution
This is happening because you are using wrong logic and you don't specify which submenu should be shown.
First, delete the current state and dont use it.
Then, you should define another state like:
const [selectedMenu, setSelectedMenu] = useState("");
then, define this function:
const handleClick = (title) => {
setSelectedMenu(title);
}
after that, once you click on Box, you should invoke function like this:
onClick={() => handleClick(item.title)}
consequently, you should write your logic like this:
<Text>{items.title}</Text>
{item.title === selectedMenu
? items.subMenu?.map((item) => {
return <Text>{item.title}</Text>;
})
: ""}
I think the problem is occurring because you have only 1 state variable set for every sidebar option. Every sidebar option should have its own variable keeping track of whether its submenu should open or not.
In your code, when the isOpen state variable is set to true then when the function maps over all the options the variable's value will always be true.
Try setting a variable for each of the menu options which contains whether the submenu should open or not.

Categories

Resources