I'm working on creating a multi level menu and I have sucessfully created a toggle which opens the sub menu on click. The issue I am having however is on click, all of the sub menus are opening. Here is my code so far:
Function
const [isSubOpen, setIsSubOpen] = useState(false)
const toggleSubMenu = (index, e) => {
e.preventDefault()
console.log(index.key)
let test = e.currentTarget.nextElementSibling.id
console.log(test)
if (test == index.key) {
setIsSubOpen(!isSubOpen)
}
}
Menu
<ul>
<li>
<a href={item.url}
onClick={toggleSubMenu.bind(this, { key })}
>
</a>
</li>
</ul>
Sub menu
<div id={key} css={isSubOpen ? tw`block` : tw`hidden`}></div>
Using a single boolean for all of them will cause them all to open and close whenever the state changes. If you want to keep it all in one state, you can use an array or an object to manage each sub-menu. An array would be easiest, so I'll show an example of how that would work.
Your state would be an array consisting of booleans. Each index would represent a sub-menu, false would be closed and true would be open. So if you click to open the first sub-menu at index 0, you would set the array to [true, false].
// Initialize the state with `false` for each sub-menu
const [subMenuState, setSubMenuState] = useState([false, false])
const toggleSubMenu = (e, i) => {
e.preventDefault()
// Clone the array
const newState = subMenuState.slice(0)
// Toggle the state of the clicked sub-menu
newState[i] = !newState[i]
// Set the new state
setSubMenuState(newState)
}
Whenever you call toggleSubMenu, you would pass the index as the second parameter like so:
<ul>
<li>
<a href="#" onClick={e => toggleSubMenu(e, 0)}>
Link 1
</a>
</li>
<li>
<a href="#" onClick={e => toggleSubMenu(e, 1)}>
Link 2
</a>
</li>
</ul>
Then reference that index in the sub-menu to see whether or not it's open:
<div css={subMenuState[0] ? tw`block` : tw`hidden`}>Sub-menu 1</div>
<div css={subMenuState[1] ? tw`block` : tw`hidden`}>Sub-menu 2</div>
I'm not sure what the use case is here, but with most menus you want to close the other active sub-menus. For example, if sub-menu 1 is open and you click to open sub-menu 2, you want sub-menu 1 to close and sub-menu 2 to open. Here's how you would achieve that effect:
const [subMenuState, setSubMenuState] = useState([false, false])
const toggleSubMenu = (e, i) => {
e.preventDefault()
// Clone the array
const clone = subMenuState.slice(0)
// Reset all sub-menus except for the one that clicked
const newState = clone.map((val, index) => {
if(index === i) {
return val
}
return false
})
newState[i] = !newState[i]
setSubMenuState(newState)
}
Let me know if you have any questions. Hope this was helpful!
Related
I’m just starting to learn javascript so please bear with me if this is a silly question
I have a sidebar menu with layers of nested hidden child submenu items that can be toggled to be visible or not
It works, but their state reverts once the page reloads.
Is there a relatively simple way to save their state in localstorage so that it stays the same when the page refreshes?
What might that look like?
const todos = document.querySelectorAll(".todo");
const togglers = document.querySelectorAll(".toggler");
todos.forEach((todo) => {
todo.addEventListener("click", () => {
todo.classList.toggle("active");
});
});
togglers.forEach((toggler) => {
toggler.addEventListener("click", () => {
toggler.classList.toggle("active");
toggler.nextElementSibling.classList.toggle("active");
});
});
<div class="sidebar">
<ul class="todos" id="todos">
<div class="toggler">Resolve Exterior Herbs</div>
<ul class="toggler-target">
<li class="todo"> Introduction</li>
<div class="toggler">Dispel Wind Cold Herbs</div>
<ul class="toggler-target">
<li class="todo"> Introduction</li>
Thank you so much
Pretty primitive solution that will help until you modify sidebar list items. It relies on order of items so if you check items 1 and 3 and add 1 more item in between of them - item1 will be toggled but item3 is now item4 and it will not be toggled, the new item3 will be. I guess you got what I mean.
const togglersKey = "sidebar_togglers";
const checkedTogglers = loadTogglersState();
const todos = document.querySelectorAll(".todo");
const togglers = document.querySelectorAll(".toggler");
todos.forEach((todo, i) => {
todo.addEventListener("click", () => {
todo.classList.toggle("active");
});
});
togglers.forEach((toggler, i) => {
if (checkedTogglers[i]) {
toggler.classList.add("active");
toggler.nextElementSibling.classList.add("active");
}
toggler.addEventListener("click", () => {
checkedTogglers[i] = toggler.classList.toggle("active");
toggler.nextElementSibling.classList.toggle("active");
saveTogglersState();
});
});
function saveTogglersState() {
localStorage.setItem(togglersKey, JSON.stringify(checkedTogglers));
}
function loadTogglersState() {
const json = localStorage.getItem(togglersKey);
if (!json) return [];
return JSON.parse(json);
}
I used ul li to implement search result:
<ul className="search-result">
<li tabindex="1">title here...</li>
<li tabindex="2">title here...</li>
<li tabindex="3">title here...</li>
.
.
.
</ul>
and this is the style for each item:
.search-result{
li:active, li:focuse {
font-weight: bold;
background: #f0f0f0;
}
}
but I'm trying to add a feature which client navigate between result items by using key-down or key-up buttons on the keyboard. but how can I access to current active tabindex in document to increase or decrease that by JavaScript and not tab button?
Do you want to tab to each <li> (search result) and use the up/down arrow keys to navigate through the list?
You have to be careful when using positive values for tabindex. It should rarely be used because it changes the default browser focus order which most users are used to. The default focus order is DOM order. In your case, if you want the user to tab to each <li> in order, you don't need a positive value for tabindex because your <li> elements are already in the order you want them tabbed to. Just set them all to 0. A value of 0 means that the DOM element should be inserted into the tab order in the normal DOM order.
<ul className="search-result">
<li tabindex="0">title here...</li>
<li tabindex="0">title here...</li>
<li tabindex="0">title here...</li>
.
.
.
</ul>
Now, having said that, elements should only have tabindex="0" if they are interactive elements. An <li> is not normally an interactive element so it'll be confusing to tab to it. What can the user do once they tab to the <li>? Can they press enter or space to select it? If the user can't interact with that element, then it should not be a tab stop.
Typically, the <li> contains things that are interactive, such as links, buttons, checkboxes, etc. Those elements are already tab stops by default and don't need a tabindex.
As far as using up/down arrow keys for navigation, again, you should only be able to arrow to elements that are interactive. I normally have an onkeydown handler on the <ul> and it listens for the arrow keys and adjusts the tabindex for each item. But I only do this when I want my entire list to be one tab stop and the user must arrow up/down to go between each list item. In your case, it sounds like you want both behaviors which is why I asked the question at the beginning of my answer.
If the list is treated as one tab stop, then all <li> elements will have a tabindex="-1" except for the <li> that has focus. It will have tabindex="0". That way the user can tab to the list as a whole and the focus will go to the list item that last had focus. The user can then up/down through the list.
When the user presses up/down, all you have to do is change tabindex from 0 to -1 for the currently focused list item and change tabindex from -1 to 0 for the list item you're moving focus to, and then call focus() on the newly focused element.
You need store current tab index in state and update it in keydown eventListener
import "./styles.css";
import { useState, useEffect } from "react";
import users from "./data";
export default function App() {
const [searchValue, setSearchValue] = useState("");
const [currentTab, setCurrentTab] = useState(0);
const searchResults = users.filter(
(user) => searchValue.trim() && user.includes(searchValue.trim())
);
useEffect(() => {
const handleKeyDown = (e) => {
const keyCode = e.keyCode;
if (keyCode == 38 && searchResults.length - 1 > currentTab)
setCurrentTab(currentTab + 1);
if (keyCode == 40 && currentTab >= 1) setCurrentTab(currentTab - 1);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [searchResults, currentTab]);
return (
<div>
<input
type="text"
value={searchValue}
onChange={(e) => {
setSearchValue(e.target.value);
setCurrentTab(0);
}}
/>
<ul className="search-result">
{searchResults.map((result, i) => (
<li className={currentTab === i ? "active" : ""}>{result}</li>
))}
</ul>
</div>
);
}
I created a sandbox, you can check it
I want to create a button that will hide each ticket and one general button that will restore them all.
this is the Code:
return (
<ul className="tickets">
{filteredTickets.map((ticket) => (
<li key={ticket.id} className="ticket">
<h5 className="headline">{ticket.headline}</h5>
<p className="text">{ticket.text}</p>
<footer>
<div className="data">
By {ticket.address} | {new Date(ticket.time).toLocaleString()}
</div>
</footer>
</li>
))}
</ul>
);
here is an example of what you want!
you have to replace myFunction() for your button and myDIV into your element that you want to hide it!
<button onclick="myFunction()">Click Me</button>
function myFunction() {
var x = document.getElementById("myDIV");
if (x.style.display === "none") {
x.style.display = "block";
} else {
x.style.display = "none";
}
}
for react =
const [visible, setVisible] = useState(true)
here is for button
<button onlick={() =>setVisible(!visible)}>hide/show
here is a demo in JS, modify to what you want exactly
<ul class="ticket">
<li>
<p>hey, I'm a P</p>
<div class="data">I'm a Div</div>
</li>
</ul>
.hide {display:none}
const generalBtn = document.getElementById(`btn`);
const divContainer = document.querySelector(`.ticket`);
const eachDiv = divContainer.getElementsByClassName(`data`);
generalBtn.addEventListener(`click`, () => {
[...eachDiv].forEach((div) => {
div.classList.toggle(`hide`);
});
});
There is a good solution in your case but as mentioned in the comments, it needs to manipulate the filteredTickets array.
You need to add a property/value to each item of filteredTickets to track or change their state. For example, it can be isVisible property which is a boolean with false or true value.
Now, isVisible value will determine the behavior. let's modify the ticket:
const handleHideTicket = (id) => {
// find selected ticket and change its visibility
const updatedFilterdTickets = filteredTikcets.map(ticket => (ticket.id === id ? {...ticket, isVisible: false} : ticket))
// now the updatedFilterdTickets need to be set in your state or general state like redux or you need to send it to the server throw a API calling.
}
return (
<ul className="tickets">
{filteredTickets.filter(ticket => ticket.isVisible).map((ticket) => (
<li key={ticket.id} className="ticket">
<h5 className="headline">{ticket.headline}</h5>
<p className="text">{ticket.text}</p>
<footer>
<div className="data">
By {ticket.address} | {new Date(ticket.time).toLocaleString()}
</div>
// add a button to control visibility of each ticket
<button onClick={() => handleHideTicket (ticket.id)}> click to hid / show </button>
</footer>
</li>
))}
</ul>
);
Explanation:
a new button added to each ticket and pass the handleHideTicket handler to it. If the user clicks on this button, the handler finds that ticket and sets the isVisible property to the false.
On the other hand, we can remove the hidden tickets by applying a simple filter method before map method. so only visible tickets will be displayed.
Now, create a general button to show all the tickets. In this case, you need a handler function that sets all ticket's isVisible value to true
const handleShowAllTickets = () => {
const updatedFilteredTickets = filteredTickets.map(ticket => ({...ticket, isVisible: true}))
// now put the updatedFilteredTickets in your general store/post an API call/ update state
}
Note: as I mentioned in the code's comments, you need to update your filteredTickets array after changing via handlers to reflect the changes in your elements.
i want to parse data from child to parent i have try solution from question
How to parse data from child to parent using reactjs?
I print that state and what appears is the state of the previous action, not the state of the last action
I tried to implement this to bring up content based on the menu that was clicked
example:
i have 3 menu
- A
- B
- C
when i click first time at the menu, for example A. the state in console is '', Then Second time i click B, the state in console is A
this is my code
PARENT
changeMenu= (menu) =>{
this.setState({
menu: menu
});
console.log('menu',menu); // Show State
}
render(){
return (
<LeftMenuMycommission active="0" menu = {(value) => this.changeMenu(value)}/>
CHILD
menuClick = (menu_name, active) =>{
this.setState({
menu: menu_name,
})
this.props.menu(this.state.menu);
}
render (){
render (
<ul>
<li ><a onClick={this.menuClick.bind(this, "A")}><i className={"fa fa-circle"}></i> A</a></li>
<li ><a onClick={this.menuClick.bind(this, "B")}><i className={"fa fa-circle"}></i> B</a></li>
<li ><a onClick={this.menuClick.bind(this, "C")}><i className={"fa fa-circle"}></i> C</a></li>
</ul>
Can anyone help me to find the problem?
Any help would be appreciated thank you :)
It's not guaranteed that state is updated immediately. You would need to use callback function and then call your parent method to pass the child component state to the parent correctly:
menuClick = (menu_name, active) =>{
this.setState({
menu: menu_name,
}, () => {
this.props.menu(this.state.menu);
})
}
Or, componentDidUpdate will do the same job:
componentDidUpdate() {
this.props.menu(this.state.menu) // only called after component is updated
}
1) I am trying to Auto scroll to the next item in listgroup. For example if user answer the first question it should auto scroll to the second question. (React) and onSubmit it should scroll to the first not answered question
2) When user view this list in mobile view the YES or NO Radio button should display in center and also SUBMIT AND CLEAR BUTTON (BOOTSTRAP)
3) How to know which item is selected from the drop down and display it in console.
Code
There are a number of ways this can be achieved. One way would be to add a method that scrolls to an item in your form, via "vanilla js", and then use that in both your onInputChanged on onSubmut methods.
You could defined this function in your component as:
// Scrolls the list to a list item by list item index
scrollToItemByIndex = (index) => {
// Find the list item element (by index), and scroll wrapper element
const scrollItem = document.querySelector(`[scrollIndex="${ (index) }"]`)
const scrollWrapper = document.querySelector(`[scrollWrapper="scrollWrapper"]`)
if(scrollItem && scrollWrapper) {
// If list item found in DOM, get the top offset
const itemRect = scrollItem.offsetTop // [UPDATED]
const wrapperRect = scrollWrapper.offsetTop // [UPDATED]
// Scroll the wrapper to the offset of the list item we're scrolling to
scrollWrapper.scrollTo(0, itemRect - wrapperRect)
}
}
You onInputChange function could then be updated as follows:
onInputChange = ({ target }) => {
const { cards } = this.state;
const { options } = this.state;
const nexState = cards.map((card, index) => {
if (card.cardName !== target.name) return card;
const options = card.options.map(opt => {
const checked = opt.radioName === target.value;
return {
...opt,
selected: checked
}
})
// [ADD] When input changes (ie something is set), scroll to next item
this.scrollToItemByIndex( index + 1 )
const style = options.every(option => !option.selected) ? 'danger' : 'info'
return {
...card,
style,
options
}
});
this.setState({ cards: nexState })
}
Also, your onSubmit would be updated to scroll to any form items that are not valid:
onSubmit = () => {
this.state.cards.forEach((card, index) => {
var invalid = card.options.every(option => !option.selected)
if (invalid) {
card.style = 'danger'
// [ADD] this item has invalid input, so scroll to it
this.scrollToItemByIndex(index)
}
else {
card.style = 'info'
}
});
...
}
Finally, you'd need to update your component's render method with the following, to ensure that the query selectors above function correctly:
<ul class="nav nav-pills nav-stacked anyClass" scrollWrapper="scrollWrapper">
and:
{cards.map((card, idx) => (<ListGroup bsStyle="custom" scrollIndex={idx}>
...
</ ListGroup >)}
[UPDATED] A full working sample can be found here:
https://stackblitz.com/edit/react-z7nhgd?file=index.js
Hope this helps!