min read

HTML as data

One of the things that makes front-end development difficult is the duplication of our data structures. The mainstream thing to do today is to put your data into JavaScript structures (Objects and Arrays) and then project those back onto the DOM.

Doing so has a considerable downside; how do we get the data into these JavaScript structures? Some solutions are:

A use-case

One thing I'm attempting to do this year is to approach problems from an HTML-first perspective. I have a small site of recipes that I use when cooking. Currently I've put very little thought into its design; it's just a list sorted chronologically by when I added each recipe.

One thing I've been thinking about doing is adding some metadata, such as tags, so that I can do interesting things like group recipes by category.

I questioned how I could do this, after all I don't want this metadata to be viewable, so where do I put it? After some thought I realized I was not viewing the document as a datasource. I was viewing it as a display.

Anything we put into the page can be hidden, so it serves as a good place for metadata. A recipe might look like this:

<article>
  <a href="./potato-soup.html">Potato Soup</a>
  <ul class="tags">
    <li>soup</li>
  </ul>
</article>

To which I can then hide this metadata easily with some CSS:

.tags {
  display: none;
}

Now that we have the metadata we can use this as our datasource in JavaScript. Conventional wisdom says that manual DOM manipulation leads to spaghetti code, but I think modern JavaScript helps to avoid this.

For example we can create some classes to represent our recipe:

class Tag {
  constructor(el) {
    this.el = el;
  }

  get name() {
    return this.el.textContent;
  }
}
  
class Recipe {
  constructor(el) {
    this.el = el;
  }

  get title() {
    return this.el.querySelector('a').textContent;
  }

  *tags() {
    let tags = this.el.querySelectorAll('.tags li');
    for(let el of tags) {
      yield new Tag(el);
    }
  }
}

let soupElement = document.querySelector('#potato-soup');
let recipe = new Recipe(soupElement);
for(let tag of recipe.tags()) {
  console.log(tag); 
}

Now we get the best of both worlds; we have nice JavaScript data-structures to use, but the DOM is our datasource. If another script adds a tag to this recipe, it's ok to us because the next time recipe.tags() is called that new tag will be included.