How to save state of toggled nested menu items after page refresh? - javascript

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);
}

Related

I want to disable the options for each question saperately after clicking the option for that question

setIsStarted(true)
setCurrentScore(0)
setResultCard(false)
setCurrentQuestion(0)
}
const checkOption = (isCorrect) =>{
setDisability(true)
if(isCorrect){
setCurrentScore(currentScore+1);
}
setCurrentQuestion(currentQuestion+1);
if(currentQuestion==questions.length-1){
setShowResult(true);
}
}
function showResultButton(showResult){
if (showResult) {
return <button className='show-result' onClick={showResultCard}>Show results</button>
}
}
const showResultCard = () =>{
setShowResult(false)
setResultCard(true);
setIsStarted(false)
}
function displayResultCard(resultCard){
if(resultCard){
return <div className='score-card'>You have answerd {currentScore}/5 correctly</div>
}
}
const updateDisability = () =>{
setDisability(false)
}
function DisplayQuesion(){
const quesList = questions.map((element) => {
return(
<div className='question-card'>
<h2>{element.text}</h2>
<ul>
{element.options.map((option) => {
return <button key={option.id} onClick={() => checkOption(option.isCorrect)} disabled={updateDisability}>{option.text}</button>
})}
</ul>
</div>
)
})
return (<div>{quesList}</div>)
}
In this code my all options are getting disabled even after clicking one option.When an option for a question is clicked, all other buttons for the question are disabled. when all questions are answered, the show results button gets shown.
This happens because you're not splitting down the list of questions in child components and so on.
You need to create three components to achieve this:
Questionary
Question
Answer
By doing so, you'll be able to define a separate state for each question/answer so they don't get mixed up.
Within your code, notice that if you click on an item it will update the state of the entire component which consequentially will update also all the other answers because they shared the same state.
You can check this link to have a better understanding:
https://www.pluralsight.com/guides/passing-state-of-parent-to-child-component-as-props

Unable to Deselect the Previous item, when clicked on another Item using Reactjs

I have list of items and I am changing the background color when I clicked on that item...
I need to change only the current Selected Item only , Previous selected Item should deselect... Now here, all items are getting changed when I clicked on each item... How can I solve this?? Can anyone help me in this ??
Here is my code
const Time = () => {
const [selectedItem, setSelectedItem] = React.useState();
const handleSelect = (event,selectedItem) => {
setSelectedItem(selectedItem);
event.target.style.backgroundColor = 'black';
event.target.style.color = 'white';
};
const dataTime = useMemo(() => TimeData, []);
const chunks = useMemo(() => {
const _chunks = [];
const tmp = [...dataTime];
while (tmp.length) {
_chunks.push(tmp.splice(0, 3));
}
//console.log(_chunks);
return _chunks;
}, [dataTime]);
return (
<div className="Main-Section">
<div className="First-Section">Time</div>
<div className="Second-Section">
<div className="date_title">Wednesday , June 13</div>
<div className="time_Slots">
{chunks.map((timeslot) => {
return (
<div key={timeslot} className="inline">
{timeslot.map((time) => {
return <p className='timeslots target' key={time}
onClick={(event) => handleSelect(event,time)}>{time}</p>;
})}
</div>
);
})}
</div>
<div>{selectedItem}</div>
<div className='new_Date'>PICK A NEW DATE</div>
</div>
</div>
);
};
U can struct Ur time data to an object like this...
{text:"",selected:false}
and use selected property flag in data then assign a class conditionally into items based on that flag. then in clicking iterate Ur list and make all the flags false except the selected one.
function selectFn(selectedItem) {
data.forEach((item) => {
item.selected = false;
});
selectedItem.selected = true;
}
and UR template be some
...
{chunks.map((timeslot) => {
return (
<div key={timeslot} className="inline">
{timeslot.map((time) => {
return <p className={`timeslots target ${time.selected ? "selectedClass" : ""}`} key={time.text}
onClick={(event) => selectFn(time)}>{time.text}</p>;
})}
</div>
);
})}
...
the css stuff..
.selectedClass{
background:black;
color:white;
}
you have to first change color for all the element to default color. Then you have to change the current element color to desired color.

Javascript: how to display certain amount of items from an array and display others on button click?

Implementation:
I have an HTML page with items container:
<section class="products">
<div class="container">
<h2 class="products-title">Some title</h2>
<div class="products-items"></div>
<button class="products-btn">Show more</button>
</div>
</section>
I have a data.js file with an array of items (16 items), here is an example:
export const products = [
{
id: 0,
name: 'Product 1',
price: 23,
category: 'Category 1',
imgSrc: './images/product-photo.jpg',
},
];
I parsed data.js file using .map() to populate 'products-items' div:
const displayProducts = products => {
const productsContainer = document.querySelector('.products-items');
const newProducts = products.map(product => {
const {
id,
name,
price,
category,
imgSrc,
} = product;
return `
<div class="product-item" data-id="${id}">
<p>${name}</p>
<p>${price}</p>
<p>${category}</p>
<img src="${imgSrc}" alt="product photo">
</div>
`;
}).join('');
productsContainer.innerHTML = newProducts;
};
export default displayProducts;
In app.js I've imported products variable and displayProducts function.
If I pass products variable to display products like this displayProducts(products), it will show all 16 items.
Desired result:
What I need is to show only first 4 items and load 4 new items each time user clicks 'Show more' button. In the end all 16 items should be displayed and 'Show more' button should be hidden.
When I had all 16 items as static data in HTML. I used CSS to hide items by default: .product-item {display: none}
Then I added class 'product-item--active' to first 4 items to display them by default: .product-item--active {display: block} and used this functionality to add active class for remaining items on button click:
const showMoreBtn = document.querySelector('.products-btn');
let currentItems = 4;
showMoreBtn.addEventListener('click', e => {
const elementList = [
...document.querySelectorAll('.products-items .product-item'),
];
for (let i = currentItems; i < currentItems + 4; i++) {
if (elementList[i]) {
elementList[i].classList.add('product-item--active');
}
}
currentItems += 4;
// Hide load more button after all items were loaded
if (currentItems >= elementList.length) {
e.target.style.display = 'none';
}
});
Issue:
But now, when items load dynamically, this functionality does not work.
I figured out how to display first 4 items using .slice():
let firstItems = products.slice(0, 4);
displayProducts(firstItems);
But, I can't figure out how to load new items on button click and hide it once all item displayed.
Update (Solved): I appreciate provided answers with good explanation and examples. Thank you.
After examining them I have the following solution (in case if someone may find it useful). Also, I removed redundant
'product-item--active' class from CSS, now there is no need to hide items by default:
import { products } from './data.js';
import displayProducts from './components/displayProducts.js';
const showMoreBtn = document.querySelector('.products-btn');
let currentItems = 0;
const displayNextFour = () => {
displayProducts(products.slice(currentItems, currentItems + 4));
// Display next 4 items until their amount exceeds
// the array length
if (!(currentItems + 4 > products.length)) {
currentItems += 4;
}
// Remove event listener from 'Show more' button and
// hide it after all items from the array are displayed
if (currentItems === products.length) {
showMoreBtn.removeEventListener('click', displayNextFour);
showMoreBtn.style.display = 'none';
}
};
displayNextFour();
showMoreBtn.addEventListener('click', displayNextFour);
Feel free to play with this minimal reproducable example using an array of 16 random strings for the content to display. It uses insertAdjacentHTML to append a block of 4 items on button click. That prevents overwriting all html on every click. The click handler is assigned using Event Delegation. If the number of items shown equals the available items, the button is disabled.
A second idea may be to hide all but [pageSize] items and on button click unhide the next [pageSize] items. See this stackblitz snippet. That snippet is more generic: it enables a variable amount of items and setting a page size (number of items to show subsequently). It requires no extra variables for tracking.
initRandomStrExt();
document.addEventListener(`click`, handle);
const fakeArray = [...Array(16)].map(v => String.getRandom(32));
// show first 4 items on page load
addFourItems(fakeArray);
function handle(evt) {
if (evt.target.classList.contains(`products-btn`)) {
return addFourItems(fakeArray);
}
}
function addFourItems(fromArr) {
// determine n of currently visible items
const start = document.querySelectorAll(`.product-item`).length;
// disable the button if all items will be visible after running this
if (start === fromArr.length - 4) {
document.querySelector(`.products-btn`).setAttribute(`disabled`, true);
}
// append 4 items of the array to the .product-items container
document.querySelector(`.products-items`)
.insertAdjacentHTML(`beforeend`,
`<div>${fromArr.slice(start, start + 4)
// ^ slice the next 4 items from the array
.map((item, i) => `<div class="product-item">${i + 1 + start} - ${
item}</div>`).join(``)}</div>`);
}
// for demo, random string helper
function initRandomStrExt() {
if (String.getRandom) {
return;
}
const characters = [...Array(26)]
.map((x, i) => String.fromCharCode(i + 65))
.concat([...Array(26)].map((x, i) => String.fromCharCode(i + 97)))
.concat([...Array(10)].map((x, i) => `${i}`));
const getCharacters = excludes =>
excludes && characters.filter(c => !~excludes.indexOf(c)) || characters;
String.getRandom = (len = 12, excludes = []) => {
const chars = getCharacters(excludes);
return [...Array(len)]
.map(() => chars[Math.floor(Math.random() * chars.length)])
.join("");
};
};
<section class="products">
<div class="container">
<h2 class="products-title">Some title</h2>
<div class="products-items"></div>
<p><button class="products-btn">Show more</button></p>
</div>
You can create a global variable to keep track of which items should be dispalyed e.g. current_index. Then create a function e.g. displayNextFour() to display four items each time and update current_index accordingly. Then on button click call displayNextFour() function.
let current_index = 0
const displayNextFour = () => {
displayProducts(products.slice(current_index, current_index+4));
//console.log(products.slice(current_index, current_index+4))
if(current_index + 4 <= products.length)
current_index+=4
}
(In displayProducts create elements with product-item--active class)

Dynamically set useState for multiple sub menus

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!

Automatically scroll down to next listgroup item and display the content in center in mobile view (React)

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!

Categories