Help making an Accordion Menu save the open/close status of its panels

As a new coder, I have been having a lot of problem trying to figure out what I need to do, to make this work. Javascript and its syntax is a little too far advanced for me to understand at this point in my learning. I am using code from w3schools and am trying to tailor it to my purposes.

I have an accordion menu that works fine functionally, but I want the open/close status of panels to save when I refresh the page. I do not want to use cookies to store this, and I would prefer storing the status via session storage rather than with local storage.

Since I don’t understand how the javascript that I am using works, I have no idea how to add in the session saving. I have looked at other examples but they are very different from the code that I have and I have no idea how to mesh the 2 together (despite days of trying to do just that).

If have 3 different menu sections (panels) so I think that I have to save the status of each panel. However the examples I see, do not do this. Maybe they are saving it in an array, but I really don’t know.

I know people don’t like writing code for others, but generally reading working code is how I learn. (Although in this case, I just can’t figure out all the Javascript syntax).

I would like to ask for 2 really big favors here:

  1. Can someone please complete the Javascript code that I would need to achieve this. Here is my codepen: https://codepen.io/helpneeded22/pen/NWxrmMJ
  2. Can someone walk me through what is happening on the following lines of code. I just guessing at what is happening:
var coll = document.getElementsByClassName("collapsible");

This retrieves all the style elements of the class “collapsible” from my css and stores it into an array called “coll”?

for (i = 0; i < coll.length; i++) {

Creating a loop, however what is the value of “coll”'s length? Is it the number of times the “collapsible” class is used on current page?

coll[i].addEventListener("click", function() {

For each instance of “coll”, wait to see if the user clicks on the instance? (Which in my case there would be coll[0] and coll[1]? If I understand how this loop is incrementing correctly I think the first instance is “coll[0]” and not “coll[1]”?

this.classList.toggle("active");

This will switch elements in the “collapsible” class to those elements declared in the “active” class? It apparently doesn’t replace the class completely, but the elements it contains (such as background color) will take precedence over those used in the other class?

var content = this.nextElementSibling;

I’m really not certain about this. W3schools describes this syntax as “The nextElementSibling property returns the element immediately following the specified element, in the same tree level.”. I may not be completely clear what an “element” is. What would be the “elements” and “sibling” in my example?

if (content.style.display === "block") {
   content.style.display = "none";

The above condition, does make me think that the elements, are the style elements in the class, and this code is used to replace those style elements?

Check this link which may be useful because session storage has a limited lifetime whereas the following information is still available the next day even after your computer has been switched off:

This code is just for a menu, so long term storage is not required. However, I would like some clarity on something mentioned there: " data stored in sessionStorage gets cleared when the page session ends"

What exactly is the end of a page session. I read this as when the user closes the browser. However my page is in php so there are a lot of occasions where the user is redirected to the same page to run different actions depending on posted values. Would this cause any issues with sessionStorage? (Would the redirection to the page be considered as “leaving the page” which would clear out the session variable?

So many questions :slight_smile:)

I think Sessions are stored on the server and for example the lifetime could be set to one hour. If a user is on the site for more than one hour then all the session values are lost. Same for closing the site in the browser, all session values are lost. While the user remains on the site then session variables are available to other pages on the same site. Ideal for retaining users credentials across pages, no need to repeatedly login for different pages?

Cookies are also for one site and remain for a long time, not sure how long. Opening a site checks to see if there are Cookies available and if so load the cookie values. Ideal for saving layouts and colours?

Local storage’s values, once set remain forever until they are deleted. The table of local storage values are related to the browser and the site. Other browsers do not have access to the local storage values. Ideal for a current App because no need to retain user information in a database.

“Horse for courses” springs to mind. Decide exactly what is required and select the correct tool.

Edits finished

1 Like

Hi @orclord1, to answer your questions regarding the codepen:

Kind of, getElementsByClassName() returns a HTMLCollection; however you can access its elements like array elements, and it also has a length property giving you the number of contained elements so that you can loop over them.

It’s the number of elements having that class.

Yes exactly!

It won’t replace the current classes but just toggle the particular active class, leaving its other classes unaffected (as oppesed to setting the className property, which would indeed overwrite all other classes).

The precedence of the styles is just a matter of CSS specificity though and is not affected by when a class has been added by your JS.

The entire document is represented as a tree of elements, which is called the DOM. Your buttons would be elements, as would be the divs; you can inspect the tree structure by right-clicking anywhere on the page and select “inspect element”.

It actually sets the style property of those elements… why they are using that here in favour of classes as before IDK though. ^^

1 Like

Here is the current code:

var coll = document.getElementsByClassName("collapsible");
var i;

for (i = 0; i < coll.length; i++) {
   coll[i].addEventListener("click", function() {
      this.classList.toggle("active");
      var content = this.nextElementSibling;
      if (content.style.display === "block") {
         content.style.display = "none";
      } else {
         content.style.display = "block";
      }
   });
}

I have some potential improvements.

Use CSS for updating the display

The display block/none code can be removed, and entirely replaced with some simple CSS code instead.

JavaScript:

      // var content = this.nextElementSibling;
      // if (content.style.display === "block") {
      //    content.style.display = "none";
      // } else {
      //    content.style.display = "block";
      // }

CSS:

.collapsible.active+.content{
  display: block;
}

Which means - when the collapsible is active, the content is shown.

Separate the event handler function from the elements that handler is assigned to.

Further simplification can be achieved by moving the function out of the addEventHandler code:

function panelClickHandler() {
  this.classList.toggle("active");
}
...
   coll[i].addEventListener("click", panelClickHandler);

That lets us separately update the function and the loop.

Update the loop to use the forEach method

With the loop, we can replace getElementsByClassName with querySelectorAll, which gives us easy access to the forEach method.

// var coll = document.getElementsByClassName("collapsible");
var coll = document.querySelectorAll(".collapsible");

// var i;
// for (i = 0; i < coll.length; i++) {
//    coll[i].addEventListener("click", panelClickHandler);
// }
coll.forEach(function addPanelHandler(panel) {
  panel.addEventListener("click", panelClickHandler);
});

Replace this keyword with a more meaningful panel variable

And with the panelClickHandler function, we can get the panel from the event object. letting us replace the this keyword with panel to make it easier to understand what is happening there.

// function panelClickHandler() {
function panelClickHandler(evt) {
  const panel = evt.target;
  // this.classList.toggle("active");
  panel.classList.toggle("active");
}

In summary

That leaves us with code that’s easier to understand than before:

function panelClickHandler(evt) {
  const panel = evt.target;
  panel.classList.toggle("active");
}

var coll = document.querySelectorAll(".collapsible");
coll.forEach(function addPanelHandler(panel) {
   panel.addEventListener("click", panelClickHandler);
});

Which says: When a panel is clicked we toggle the active class on it, and that happens to all collapsible elements.

The updated code is found at https://codepen.io/pmw57/pen/xxZEWGy

1 Like

Wow Paul. Thanks so much for the code revamp. Of all the different “accordion menu” examples I have reviewed on a browser search, none are any where near as elegant as this. It seems like so little code that does so much. I’ll never figure out how you can remember all those different bits of syntax/code to make it all work. I really appreciate all the time you spend detailing that out for me.

The final stage now to get this all working is to incorporate saving the state now to sessionStorage.

The problem now for me, is that I don’t see variables that can be saved in sessionStorage. With the loop it generated variables (coll[0], coll[1], etc). How would this work with the forEach?

2 Likes

Thanks. I find that some of the benefits of keeping it simple is that it ends up being easy to understand and is hard to break.

Adding session storage

With the session storage, the panelClickHandler function can pass the panel to a separate function, called savePanelState()

function panelClickHandler(evt) {
  const panel = evt.target;
  panel.classList.toggle("active");
  savePanelState(panel);
}

Saving the panel state

Details about using localStorage are found at https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage

Each panel has a unique identifier, which can be the key we use. The value can be whether the panel is active or not.

As a test, I’ll try outputting that information to the console.

function savePanelState(panel) {
  console.log({
    key: panel.id,
    value: panel.classList.contains("active")
  });
}

Result: {key: "header1", value: true}
Looking good.

We can move those values out to separate named variables:

function savePanelState(panel) {
  const key = panel.id;
  const value = panel.classList.contains("active");
  console.log({key, value});
}

We can now save key and value to local storage, and can also use console.log to confirm the localStorage values can be retrieved.

function savePanelState(panel) {
  const key = panel.id;
  const value = panel.classList.contains("active");
  localStorage.setItem(key, value);
  // console.log(localStorage.getItem(key));
}

Loading the panel state

The only challenge now is getting those values on pageload. We can update the addPanelHandler function to achieve that with loadPanelState, which is a nice match for the savePanelState that we used earlier.

coll.forEach(function addPanelHandler(panel) {
  panel.addEventListener("click", panelClickHandler);
  loadPanelState(panel);
});

Yes, it is another function call, which is a simple one called loadPanelState that is a nice match for the savePanelState that we used earlier.

The loadPanelState function just needs to get the id of the panel, and use the localStorage value to set whether the panel is active or not.

The classList.toggle method accepts a second parameter where you can specify whether the panel should be toggled to be open or closed.

function loadPanelState(panel) {
  const state = localStorage.getItem(panel.id);
  panel.classList.toggle("active", state === "true");
}

What happens on the first time a page is loaded and there is no localStorage values? It gives null, which results in the panel starting off closed.

Summary

The full scripting code is:

function loadPanelState(panel) {
  const state = localStorage.getItem(panel.id);
  panel.classList.toggle("active", state === "true");
}

function savePanelState(panel) {
  const key = panel.id;
  const value = panel.classList.contains("active");
  localStorage.setItem(key, value);
}
function panelClickHandler(evt) {
  const panel = evt.target;
  panel.classList.toggle("active");
  savePanelState(panel);
}

var coll = document.querySelectorAll(".collapsible");
coll.forEach(function addPanelHandler(panel) {
  panel.addEventListener("click", panelClickHandler);
  loadPanelState(panel);
});

and the updated working example is found at https://codepen.io/pmw57/pen/ExPgErz

Edit: Updated to include the state=“true” issue.

2 Likes

Excuse me for jumping in but I liked the look of that code and tried to test it out.:slight_smile:

It seems to me that there is something amiss though as you can’t seem to have the menu appear as closed after you have opened it and then closed it and then refreshed the page. It always appears opened.

It seems to be this line:

panel.classList.toggle("active", state);

If I use:

panel.classList.toggle("active", state ==='true');

It seems to work as I expect but is probably an incorrect assumption on my part :slight_smile:

2 Likes

Good spotting there.

I got caught by an unfounded assumption. When True or False is saved to localServer, they end up being text values of “true” or “false”. The trouble is that “false” as a string is considered to be true, because it contains text and is an empty string.

As a result the codePen code is updated with the fix, and the above post is updated to resolve the issue.

Thanks for testing things out.

3 Likes

Thank you so much Paul_Wilkins for all your time in writing the code and the explanations – it is so greatly appreciated. I have been struggling with this for over 1 week. While my slow 50-year old brain has been able to work out the nuances of HTML, CSS, PHP and MySQL, for some reason Javascript just seems to be very taxing for me.

1 Like

Thank you m3g4p0p for your time in replying to my post and helping with my learning. This does help me understand the flow of Javascript a little bit better.

1 Like