home ~ projects ~ socials

Keep details Tags Open on Refresh

Intro

I'm working on a new site1. It wraps content in <details> elements to keep pages short. Great reading experience. Crappy editing experience. Changes collapse the tags. You have to re-open them each time to see what you're working on.

This is how I'm saving open/closed states across page loads to fix that.

The Story

Open The Story

Where Did I Put That Devil

I've switched to using <details> elements to wrap content sections on the bitty.js2 home page and hide them be default. I really like the approach. It lets me put everything on the page without making it incredibly long.

Stay Where I Put You

The approach is great for reading. It makes editing a pain, though. The page reloading that accompanies each change causes any open <details> elements to close. Reopening them to see each edit is tedious.

I could add open tags to the sections I'm actively working on. But, I know myself. My propensity to forget and leave them open when publishing is not to be underestimated.

Enter JavaScript

The code below is designed to fix that. It watches all the <details> tags on a page. When one changes from opened to closed (or vice verse) it saves the state into localStorage. The stored values are pulled back out when the page is reloaded and each <details> section is put back to the way it was.

Bring It Home

Seeing the functionality in play got me thinking about this site. Specifically, using it to hide the preamble in code posts. I just set that up. This is the first page that uses it. I couldn't be happier with the result.

-a

The Code

JavaScript

class DetailsWatcherTest {
  #storageName = "openDetailsDataTest";
  #data;

  initTags() {
    this.details().forEach((tag, tagIndex) => {
      if (this.#data.has(tagIndex)) {
        tag.open = true;
      }
      tag.addEventListener("toggle", (event) => {
        if (event.newState === "open") {
          this.#data.add(tagIndex);
        } else {
          this.#data.delete(tagIndex);
        }
        this.saveState();
      });
    })
  }

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

  loadState() {
    const storage = localStorage.getItem(this.#storageName);
    if (storage === null) {
      this.#data = new Set();
    } else {
      this.#data = new Set(JSON.parse(storage).data);
    }
  }

  run() {
    this.loadState();
    this.initTags();
  }

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

const dw = new DetailsWatcherTest();    
dw.run();

The Non-Surprising Length

This code's a little longer than I would have initially guessed. That's no surprise though. Most code projects work out that way when you get into the details. I could clean it up a little by using a constructor instead of having to call .run() if I really wanted to. But, eh, it's fine.

I'll probably turn it into a web component at some point. That's for later. For now, it's time to get back to making bitty.js.

-a

-- end of line --

Footnotes

A vanilla js web component that adds reactive abilities to pages.

This is the same as the first footnote. There would only be one if my setup could have two footnotes in the content that linked to the same one down here. That's not possible at the moment. So, you get a bonus link.