I am somewhat new to react and I am building a navigation menu where I target the ul and li components using the map() function with index to avoid duplicating code. Whenever I click to open one component all components open instead of an individual one. I know the issue is probably in me not targeting the components correctly so any help will be appreciated.
Here is the code:
...react icon and state imports
const Navbar = () => {
const [subMenuOpen, setSubmenuOpen] = useState(false);
const menu = [
{
title: "Management",
submenu: true,
icon: <FaUserTie/>,
submenuItems: [
{ title: "MGMT Cockpit" },
{ title: "P&L by Month" },
{ title: "B/O Report" },
{ title: "User List" },
],
},
{
title: "Tools",
submenu: true,
icon: <BsTools/>,
submenuItems: [
{ title: "Inventory" },
{ title: "Damages" },
{ title: "MGMGT Tools" },
{ title: "Plan Tools" },
{ title: "Sales Tools" },
{ title: "Planning Tools" },
]
},
];
return (
<div className="flex">
<div className="bg-primary h-screen w-64 p-3 relative overflow-y-scroll">
{menu.map((item, index) => <sidebarItem key={index} sidebarItem = {index}/>)}
<ul className="pt-2">
{menu.map((menu, index) => (
<>
<li className="
text-gray-300 text-lg flex item-center gap-x-4 cursor-pointer p-2 my-4
hover:bg-slate-50 hover:text-primary hover:rounded-lg duration-500"
onClick={() => setSubmenuOpen(!subMenuOpen)}> //Fix this???
<span className="text-2xl block float-left">
{menu.icon ? menu.icon : <RiDashboardFill/>}
</span>
<span className="text-base font-medium flex-1">{menu.title}</span>
{menu.submenu && (
<BsChevronDown className={`${subMenuOpen && "rotate-180"} duration-300`}/>
)}
</li>
{menu.submenu && subMenuOpen && (
<ul>
{menu.submenuItems.map((submenuItem, index) => (
<li key={index} className="text-gray-300 text-md gap-x-4 px-5 my-3">
{submenuItem.title}
</li>
))}
</ul>
)}
</>
))}
</ul>
</div>
</div>
)
}
export default Navbar
App.js just imports this Navbar so I didn't include the code for that.
You have different possibilities of open submenus (since you have multiple menu types), so you should have state that reflects that. A plain true/false state won't be able to store enough information. One approach would be an array of submenu indices that are open.
const [submenuIndicesOpen, setSubmenuIndicesOpen] = useState([]);
Then examine the indices when iterating to determine what should be shown and how to call setSubmenuItemsOpen when toggling.
<ul className="pt-2">
{menu.map((menuItem, index) => ( /* don't shadow the menu variable here */
<>
<li className="
text-gray-300 text-lg flex item-center gap-x-4 cursor-pointer p-2 my-4
hover:bg-slate-50 hover:text-primary hover:rounded-lg duration-500"
onClick={() => setSubmenuOpen(
submenuIndicesOpen.includes(index)
? submenuIndicesOpen.filter(i => i !== index)
: [...submenuIndicesOpen, index]
)}>
{menuItem.submenu && submenuIndicesOpen.includes(index) && (
If only one submenu can be open at a time, you could instead have a state that's just a single number, the index of the open submenu.
your subMenuOpen state should be an array with index and in onClick handler you should pass index of li that you have in map , to control which menu should be open
or
you can set your menu as a state with a property of open on each submenu to control open of it
Related
The toggle works only if clicking on a button, ignores the div, seems button triggers some sort of state change, how do I get this to work when clicking anywhere ?
ToolBar.ts
export default class ToolBar {
options:Array<ToolBarOptions>;
constructor() {
this.options = [
new ToolBarOptions(ToolButton.sideMenu,SideMenuIcon,false,true, []),
new ToolBarOptions(ToolButton.mainMenu,MainMenuIcon,false,true, [ new ToolBarOptions(ToolButton.export,exportIcon,true,true,[])]),
new ToolBarOptions(ToolButton.entities,EntityIcon,false,true,[]),
new ToolBarOptions(ToolButton.setting,settingsIcon,false,true,[]),
];
}
}
class ToolBarOptions {
disabled:boolean;
name:ToolButton;
icon:string;
show:boolean;
options:Array<ToolBarOptions>;
constructor(name: ToolButton,icon:string,disabled:boolean,show:boolean, options:Array<ToolBarOptions>) {
this.name = name;
this.disabled = disabled;
this.icon = icon;
this.show=show;
this.options=options;
}
}
export const enum ToolButton{
mainMenu="mainMenu",
export="export",
entities="entities",
sideMenu="sideMenu",
setting="setting",
}
App.svelte
let toolbarOptions = new ToolBar();
function handleClickOutSide() {
console.log(toolbarOptions.options)
toolbarOptions.options.forEach((o) => {
o.show=!o.show;
});
console.log(toolbarOptions.options)
<div on:click={handleClickOutSide } class="toolbar">
<ul class="">
{#each toolbarOptions.options as {name, icon,options, show }, i}
<li>
<button on:click={()=>show=!show} name={name} class="flex items-center justify-center relative {toolbarOptions.options.length-1 === i ? "h-10":""}">
{#if toolbarOptions.options.length-1 ===i}
<div>100%</div>
{/if}
<icon> {#html icon}</icon>
<span>
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</span>
{#if options.length >0 }
<div class="absolute top-10 w-32 bg-black h-10 cursor-pointer {show ? "hidden":""}">
<ul class="w-full flex">
{#each options as {name, icon,show }}
<li class="min-w-full flex items-center h-10 px-2">
<span class=""> {#html icon} </span>
<span class="left-4 w-1/2"> {name}</span>
</li>
{/each}
</ul>
</div>
{/if}
</button>
</li>
{/each}
</ul>
</div>
When interacting with each item in a list there are several options that take reactivity into account:
Use item index access
Map all items
Use a dummy assignment
Examples for each:
let items = [
{ name: 'Item 1', checked: false },
{ name: 'Item 2', checked: false },
{ name: 'Item 3', checked: false },
];
const toggleViaIndex = () =>
items.forEach((e, i) => items[i].checked = !items[i].checked);
const toggleViaMap = () =>
items = items.map(item => ({ ...item, checked: !item.checked }));
const toggleViaDummyAssignment = () => {
items.forEach(item => item.checked = !item.checked);
items = items;
}
REPL
Personally, I am not a fan of the dummy assignment, because it looks like a useless statement. You can of course add comments to make it clearer why the statement exists.
I would not recommend using classes unless necessary, by the way. It breaks things like the map approach, if the class defines any functions or the prototype matters, because those aspects get lost in the spread.
Also: click events on things that are not button elements should always have a keyboard equivalent for accessibility; in this case probably an escape press.
You also probably should just set show to false rather than invert it on click outside.
By the way, ToolBarOptions could be shortened significantly via TypeScript:
class ToolBarOptions {
constructor(
public name: ToolButton,
public icon: string,
public disabled: boolean,
public show: boolean,
public options: Array<ToolBarOptions>,
) { }
}
I am trying to create a custom menu select for my Algolia search based on the documentation here with a barebones example. I am using Tailwind and Headless UI for my component styling.
More specifically, I am trying to make this work with the ListBox component by Headless UI, which is documented here. My code is below:
import { connectMenu } from "react-instantsearch-dom";
import React, { useState, Fragment, useEffect } from "react";
import { Listbox, Transition } from "#headlessui/react";
import { CheckIcon, SelectorIcon } from "#heroicons/react/solid";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
const MobileDropdownS = ({ label, currentRefinement, refine, hide }) => {
const [selected, setSelected] = useState(true);
const items= [
{ value: 'category', label: 'Category' },
{ value: 'brand', label: 'Brand' },
{ value: 'color', label: 'Color' },
{ value: 'size', label: 'Size' },
]
if (hide) return null;
return (
<Listbox
onChange={(event) => {
refine(event.target.value);
setSelected(event.target.value);
}}
value={currentRefinement || ""}
>
{({ open }) => (
<div>
<Listbox.Label>{label}</Listbox.Label>
<div>
<Listbox.Button>
<span>{label}</span>
</Listbox.Button>
<Listbox.Options>
{items.map((item) => (
<Listbox.Option
key={item.label}
value={item.isRefined ? currentRefinement : item.value}
key={item.label}
className={({ active }) =>
classNames(
active ? "text-white bg-indigo-600" : "text-gray-900",
"cursor-default select-none relative py-2 pl-3 pr-9"
)
}
>
{({ selected, active }) => (
<>
<div className="flex items-center">
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"ml-3 block truncate"
)}
>
{item.label}
</span>
</div>
{selected ? (
<span
className={classNames(
active ? "text-white" : "text-indigo-600",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</div>
)}
</Listbox>
);
};
const MobileDropdownSelect = connectMenu(MobileDropdownS);
export default MobileDropdownSelect;
The way I understand it, ListBox becomes the <select> tag from Algolia's documentation, where I should add this:
refine(event.target.value);
setSelected(event.target.value);
However, this gives me the following error:
×
TypeError: Cannot read properties of undefined (reading 'value')
onChange
D:/Gatsby/XTheme/src/components/search/MobileDropdown.jsx:22
19 | <Listbox
20 | onChange={
21 | event => {
> 22 | refine(event.target.value)
23 | setSelected(event.target.value)
24 | }
25 | }
Is this not correct? How do I fix this error and port over Algolia's example with the <select> input to the <ListBox> component such that when I select an item from the Listbox, it refines the search based on the item?
Tru using event.currentTarget.value in place of event.target.value . You can find difference between both in this link
I am trying to make a webapplication with Treeviz dependency. The goal is to place a popover button to each node of the tree and if user clicks to the button he/she can see the description of the node,and after it should be editable. I tried in many ways but for me popover does not work in React.
There is an example for what I would like to do. You can see I have to insert React component to HTML therefor I am using renderToString. All you have to look is the renderNode property of the tree. I am referencing to React component in renderNode like: ${tooltip} ${popover}.
import React from "react";
import { TreevizReact } from "treeviz-react";
import { renderToString } from "react-dom/server";
const data_1 = [
{
id: 1,
text_1: "Chaos",
description: "Void",
father: null,
color: "#FF5722"
},
{
id: 2,
text_1: "Tartarus",
description: "Abyss",
father: 1,
color: "#FFC107"
},
{ id: 3, text_1: "Gaia", description: "Earth", father: 1, color: "#8BC34A" },
{ id: 4, text_1: "Eros", description: "Desire", father: 1, color: "#00BCD4" }
];
export const App = () => {
return (
<div style={{ marginLeft: 10 }}>
<div style={{ display: "flex" }}>
<TreevizReact
data={data_1}
relationnalField={"father"}
nodeWidth={120}
nodeHeight={80}
areaHeight={500}
areaWidth={1000}
mainAxisNodeSpacing={2}
secondaryAxisNodeSpacing={2}
linkShape={"quadraticBeziers"}
renderNode={(data) => {
const nodeData = data.data;
const settings = data.settings;
let result = "";
const tooltip = renderToString(
<strong
data-toggle="tooltip"
data-placement="top"
title={nodeData.description}
>
{nodeData.text_1}
</strong>
);
const popover = renderToString(
<button
type="button"
className="btn btn-secondary"
data-container="body"
data-toggle="popover"
data-placement="top"
data-content={nodeData.description}
>
Popover on top
</button>
);
if (data.depth !== 2) {
result = `<div className="box"
style='cursor:pointer;height:${settings.nodeHeight}px; width:${settings.nodeWidth}px;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:${nodeData.color};border-radius:5px;'>
<div>
${tooltip}
${popover}
</div></div>`;
} else {
result = `<div className="box" style='cursor:pointer;height:${
settings.nodeHeight
}px; width:${
settings.nodeHeight
}px;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:${
nodeData.color
};border-radius:${settings.nodeHeight / 2}px;'><div><strong>
${nodeData.text_1}
</strong></div></div>`;
}
return result;
}}
duration={600}
isHorizontal
linkWidth={(node) => 10}
/>
</div>
</div>
);
};
export default App;
Tooltip is working but popover does not show up.
You can try it: https://codesandbox.io/s/zealous-orla-4bq5f?file=/src/App.js
Also tried
const popover = renderToString(
<Popup
trigger={<button> Trigger</button>}
position="right center"
>
<form onSubmit={saveHandler}>
<ContentEditable
html={text.current}
onChange={handleChange}
/>
<button type="submit">Save</button>
<button>Cancel</button>
</form>
</Popup>
const popoverContent = (
<Popover id="popover-basic">
<Popover.Header as="h3">Popover right</Popover.Header>
<Popover.Body>
And here's some <strong>amazing</strong> content. It's very
engaging. right?
</Popover.Body>
</Popover>
);
const popover = renderToString(
<OverlayTrigger
trigger="click"
placement="right"
overlay={popoverContent}
>
<Button variant="success">Click me to see</Button>
</OverlayTrigger>
);
None of them worked for me.
Probably your approach doesn't work because the dom elements in the tree are created dynamically, and bootstrap doesn't set them up.
A more react-ish way to do it would be using react-bootstrap lib and managing every UI aspect in states. To implement the tooltip, the Overlay component actually as a prop called target that allows you to change over what element the tooltip is shown.
<Overlay target={tooltipTarget} show={showTooltip} placement="right">
{(props) => (
<Tooltip id="overlay-example" {...props}>
{tooltipNode?.data.text_1}
</Tooltip>
)}
</Overlay>
Then you only need to manage all these states in the onNodeMouseEnter and onNodeMouseLeave handlers in the TreevizReact component.
onNodeMouseEnter={(_event, node) => {
const t = document.querySelector(`#node-${node.id}`);
setTooltipTarget(t);
setShowTooltip(true);
setTooltipNode(node);
}}
onNodeMouseLeave={(_event, node) => {
setTooltipTarget(null);
setShowTooltip(false);
setTooltipNode(null);
}}
The popup follows the same logic with another set of states.
<div ref={ref}>
<Overlay
show={!!selectedNode.current}
target={target}
placement="bottom"
container={ref.current}
containerPadding={20}
>
<Popover id="popover-contained">
{/* hack to avoid weird blinking (due mutable state) */}
{!!selectedNode.current && (
<>
some form based on node data
{JSON.stringify(selectedNode.current?.data)}
</>
)}
</Popover>
</Overlay>
</div>
and in onNodeClick handler
onNodeClick={(_event, node) => {
const t = document.querySelector(`#node-${node.id}`);
// unselect if already selected
if (selectedNode.current?.id === node.id) {
selectedNode.current = null;
setTarget(null);
} else {
selectedNode.current = node;
setTarget(t);
}
}}
You might notice this time I used a mutable variable via a ref (selectedNode.current), for some reason using a state caused some issues, maybe due to D3 and react having different rendering cycles, reason why you'll probably encounter some other glitches in this implementation.
https://codesandbox.io/s/quizzical-buck-slofh?file=/src/App.js
I've been trying to add a menu with an active styling by using useState in React js but for some reasons it only stays in active state, does not go back to inactive when you click on the other menu item. On the other hand, it works perfectly when I pass down my active state as props from Sidebar.js
React version: "react": "^17.0.2"
Node version: v16.0.0
Here is my code:
SideLink.js
import React, {useState} from "react";
const SideLink = ({ name, Icon}) => {
const [active, setactive] = useState("Home")
return (
<li className="group" onClick={() => setactive(name)}>
<a href={name.toLowerCase()} className="cursor-pointer block mb-2 pointer-events-none">
<div className="inline-block">
<div
className={`
flex items-center
group-hover:bg-blue-50
group-hover:text-red-500
rounded-full pl-2 py-3 pr-6
${active === name ? "text-red-400" : ""}
`}
>
<Icon />
<span className="ml-2 font-bold text-xl">{name}</span>
</div>
</div>
</a>
</li>
);
};
export default SideLink;
Sidebar.js
import React, {useState} from "react";
import SideLink from "../../assets/components/SideLink";
import {
BookmarksIcon,
ExploreIcon,
HomeIcon,
ListsIcon,
MessagesIcon,
MoreIcon,
NotificationsIcon,
ProfileIcon,
} from "../../assets/icons/Icon";
const sideLinks = [
{
name: "Home",
icon: HomeIcon,
},
{
name: "Explore",
icon: ExploreIcon,
},
{
name: "Notifications",
icon: NotificationsIcon,
},
{
name: "Messages",
icon: MessagesIcon,
},
{
name: "Bookmarks",
icon: BookmarksIcon,
},
{
name: "Lists",
icon: ListsIcon,
},
{
name: "Profile",
icon: ProfileIcon,
},
{
name: "More",
icon: MoreIcon,
},
];
const Sidebar = () => {
return (
<div className="w-72 flex flex-col justify-between px-2">
<div>
<nav>
<ul>
{sideLinks.map(({ name, icon }) => (
<SideLink key={name} name={name} Icon={icon}/>
))}
</ul>
</nav>
</div>
<div>bottom</div>
</div>
);
};
export default Sidebar;
Thank you!
Keep in mind that in your example each SideLink maintain their own independent state. When one of those call its own setactive, it doesn't have any effect on any other.
What I think you really want is one piece of state that live in a central location, the parent component, Sidebar, and forward to SideLink that value and the means to change that it.
I am new to Javascript/ReactJs world. I am building a side nav. Below are some part of my code -
import React, {Component} from "react";
import Link from "./Link/Link";
class Sidebar extends Component {
state = {
dashboard: {
name: "Dashboard",
href: "#",
active: true,
svgPath: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
children: []
},
team: {
name: "Team",
href: "#",
active: false,
svgPath: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
children: [
{
name: "Overflow",
href: "#",
},
{
name: "Members",
href: "#",
},
{
name: "Calender",
href: "#",
},
{
name: "Settings",
href: "#",
}
]
}
}
navClickHandler = (e, key) => {
console.log(e, key)
}
render() {
let navLinks = [];
for (const item in this.state) {
navLinks.push(<Link key={item} navClicked={(e, item) => this.navClickHandler(e, item)} config={this.state[item]} />)
}
return (
<div className="hidden bg-indigo-700 md:flex md:flex-shrink-0">
<div className="flex flex-col w-64 border-r border-gray-200 pt-5 pb-4 bg-white overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<img className="h-8 w-auto"
src="https://tailwindui.com/img/logos/workflow-logo-indigo-600-mark-gray-800-text.svg"
alt="Workflow" />
</div>
<div className="mt-5 flex-grow flex flex-col">
<nav className="flex-1 px-2 space-y-1 bg-white" aria-label="Sidebar">
{navLinks}
</nav>
</div>
</div>
</div>
);
}
}
export default Sidebar;
How can I get the item value inside the navClickedHandler function -
let navLinks = [];
for (const item in this.state) {
navLinks.push(<Link key={item} navClicked={(e, item) => this.navClickHandler(e, item)} config={this.state[item]} />)
}
Actually, I want to use that key(item) to check which of the side-nav is active. It will change the state object based on the click.
console.log in nacClickHandler is giving undefined.
You are not passing item to Link, so you cannot get it as an argument back from the click event.
So change this:
navClicked={(e, item) => this.navClickHandler(e, item)}
to this:
navClicked={e => this.navClickHandler(e, item)}
Or, you can change your <Link/> component to something like this:
const Link = ({navClicked, item}) => (
<div onClick={e => navClicked(e, item)}></div>
);
And pass item as a prop:
<Link key={item} item={item} navClicked={this.navClickHandler} config={this.state[item]} />
The second option is preferable since it allows you to pass the same function reference always, thus avoiding redundant renders (provided that Link is a PureComponent, or a functional component wrapped in memo)
Hello other problems I see is that key={item} will be translated to key=[object Object] therefore your behaviour will fail due to the check key1 !== key2 because the actual value will be [object Object] !=== [object Object] which fails.
An alternative I would suggest to either use some unique data item.id for instance or generate some using a basic package like uuid
Back to your issue, in order to check which item is active I would suggest to you to do this:
<Link key={item.id} .... active={state.selected.id === item.id}/>