Project: Details Tag Opener

Overview

  • This class stores the open/closed state of <details> on the page.

  • Every time a <details>is toggled the state gets stored in localStorage.

  • When the page is reloaded/refreshed the state of each <details>element is restored.

Example

Open and close theses <details> tags then refresh the page. They'll be restored to where you left them.

alfa

the quick

red fox

bravo

jumps over

the lazy

charlie

brown dog

delta

brown dog

Source Code

export class DetailsOpener {
  #state;

  bittyReady() {
    this.loadState();
    this.setTagState();
    this.watchTags();
  }

  numberOfTags() {
    return document.querySelectorAll("details").length;
  }

  setTagState() {
    document.querySelectorAll("details").forEach((tag, tagIndex) => {
      if (
        this.#state.openTags.has(tagIndex)
      ) {
        console.log(tag);
        tag.open = true;
      } else {
        tag.open = false;
      }
    });
  }

  loadState() {
    const storage = localStorage.getItem(this.storageName());
    this.#state = {
      numberOfTags: this.numberOfTags(),
      openTags: new Set(),
    };
    document.querySelectorAll("details").forEach((tag, tagIndex) => {
      if (tag.open === true) {
        this.#state.openTags.add(tagIndex);
      }
    });
    if (storage !== null) {
      const data = JSON.parse(storage).state;
      if (this.numberOfTags() === data.numberOfTags) {
        this.#state = {
          numberOfTags: data.numberOfTags,
          openTags: new Set(data.openTags),
        };
      }
    }
  }

  saveState() {
    localStorage.setItem(
      this.storageName(),
      JSON.stringify({
        "state": {
          numberOfTags: this.#state.numberOfTags,
          openTags: [...this.#state.openTags],
        },
      }),
    );
  }

  storageName() {
    return `details-state-${window.location.pathname}`;
  }

  watchTags() {
    document.querySelectorAll("details").forEach((tag, tagIndex) => {
      tag.addEventListener("toggle", (event) => {
        if (tag.open) {
          this.#state.openTags.add(tagIndex);
        } else {
          this.#state.openTags.delete(tagIndex);
        }
        this.saveState();
      });
    });
  }
}

Notes

  • This script isn't really bitty specific. All the methods are generic with no requirement for adding data-* attributes to any elements. bitty is merely used to wrap the class and load it.

  • Adding open attributes to <details> elements directly in the HTML opens them by default. If an element with an open attribute is closed it stays closed on the next refresh regardless of if the open attribute exists or not.

    That provides a way for doing things like opening an introduction section on a page by default and then preventing it from re-opening if the user decides to close it.

  • Nested <details>tags maintain their state regardless of their parent. For example, if you do the following:

    • open a parent tag
    • open a child tag
    • close the parent tag
    • re-open the parent tag

    The child tag will be open when the parent is re-opened.

  • If the number of details elements on the page changes when it's refreshed/reloaded everything goes back to its default state (i.e. any tags with open attributes are opened and all the other ones are closed).

  • Data is stored with a key that uses the window.location.pathname. If a single URL is used to host multiple content pages (e.g. in a single page app) things will get weird. The code would need to be updated to accommodate that.