Wesley Schwengle IncludeIf: A digital space written in pencil Articles Series Answerrit /dev/null About ArticlesCut the noise: Declarative web components with HTMLElementSugar

TL;DR

HTMLElementSugar provides declarative attribute definitions, template handling, component aliases, and configuration inheritance — all while staying close to vanilla web components. In this blog I’ll explain the why, the how, and the because. You can find it on npm .

Sugar for my honey, sugar for my web components.

Webcomponents solve a really fun problem, they only introduce a new one: BOILERPLATE much?

When I wrote a HTML mixtape I didn’t really care about the boilerplate. But afterwards I realized I could actually write a toolset that implements most of the components I used to create a mixtape library with predefined web components. My initial reaction was to utilize UltiSnips but I soon realized that this was not really the thing I was needing.
For observedAttributes you need a list of attributes. For attributeMap, you need to define optional parsers, and for defaultConfig, you need to define if you need default values and what they are. This can probably be done with a good snippet, but it needs too much coding on the wrong side of my editor.

So I decided to tackle the problem on the javascript side. I started writing HTMLElementSugar. It had a different name when I started, but it is now called HTMLElementSugar. It’s important to note that all my web components operate at the light DOM. I don’t operate in the shadow DOM, as I don’t have a compelling reason to do so at this moment. Besides this reason, it is also very SEO and crawler-friendly. Well… in theory at least, search engines and SEO tools have a lot of issues with pages that are written with webcomponents.

Cutting the boilerplate, declaring the attribute

So what does HTMLElementSugar do? Well, a lot. It removes the “need” to write observedAttributes, attributeMap, and defaultConfig. You only need to declare attribute definitions: attributeDefs. This is essentially a mapping that transforms the data into observedAttributes, attributeMap, and defaultConfig. It is a small hash/map/dictionary/associative array — every language I know has a different term for what I, a perl dev, name a hash, so pick one you like ;)) — where you define the name and a specification. By default, attributes are observed, unless you add observed in a falsey manner. You can define a default and a parser, all are optional:

class MyNewElement extends HTMLElementSugar { static tag = 'my-new-element'; static attributeDefs = { foo: { default: 'bar' }, 'bar': { parser: parseBoolean }, 'baz': null, 'qux': {}, 'toto': { observed: "" }, // not observed 'titi': { observed: 0 }, // not observed 'tata': { observed: false }, // not observed 'tete': { observed: null }, // not observed 'yes': { observed: true }, 'yez': { observed: 1 }, 'yep': { observed: "here be dragons" }, }; }

Compare that to this:

export class TrackItem extends HTMLElement { static tag = 'track-item'; static observedAttributes = ['name', 'tagline', 'audioBase', 'videoBase', 'artBase']; static attributeMap = { name: true, tagline: true, hidden: parseBoolean, playHidden: parseBoolean, }; static defaultConfig = { name: null, tagline: null, audioBase: './audio/', videoBase: './video/', artBase: './art/', }; }

We simplified everything by declaring it once, and we then populated observedAttributes, attributeMap, and defaultConfig. Let me show you by using our test file:

t.test('test sunny day for attributeDefs', async t => { MyNewElement.init(); t.same(MyNewElement.tag, 'my-new-element', 'tag set'); t.same( MyNewElement.observedAttributes, ['foo', 'bar', 'baz', 'qux', 'yes', 'yez', 'yep'], 'observedAttributes set' ); t.same(MyNewElement.attributeMap, { bar: parseBoolean }, 'Parser on bar'); t.same(MyNewElement.defaultConfig, { foo: 'bar' }, 'default applied'); });

Simple, effective, and no repetition.

Pre-v0.1.0 we allowed authors to write their own observedAttributes, attributeMap, and defaultConfig and opt out of using attributeDefs. This is no longer the case due to inheritance problems.

So now our mixtape component will look like this:

export class TrackItem extends HTMLElementSugar { static tag = 'track-item'; static attributeDefs = { name: { default: null }, tagline: { default: null }, audioBase: { default: './audio' }, videoBase: { default: './video' }, artBase: { default: './art' }, hidden: { observed: 0, parser: parseBoolean }, playHidden: { observed: 0, parser: parseBoolean }, } }

Now you can register the component:

TrackItem.register()

This will run some sanity checks, e.g., does your tag exist, and transforms the data. It will also fail fast once an error is detected. And to make your development experience nicer, you don’t need to register() your component in a test suite, you can call init() and all the checks are done for you.

But this is not all…

Writing templates

Web components also use templates. If you have written web components before, you know that you can write them in HTML directly, or you can include them in your web component. Because HTMLElementSugar operates in the light DOM its template handling is flexible. HTMLElementSugar supports four, or five, use cases, depending on how you look at them. You can use standard HTML template references, javascript template references, a regular JS function or via a tuple. Or you write none. The tuple is the one I use most often as it allows overrides that one may want to have in a project. It allows you to add some hooks for CSS targetting.

class TypeComponent extends HTMLElementSugar { static tag = 'tuple-based'; static HtmlTemplate = [ 'some-id-in-html', () => { const t = document.createElement('template'); t.innerHTML = `<div class="track-row"><div class="track-info">from fn</div></div>`; return t; } ]; }

Other features of HTMLElementSugar

Besides the sugar for attribute definitions and templates, several other features are available: aliases and configuration.

Calling it the beauty or the beast: aliases

The beautiful thing about web components (I’m being sarcastic here) is that they require you to define them as foo-bar and you cannot have foobar; it needs a hyphen. This ensures you’ll never clash with newly introduced or future HTML elements. Understandable, but it limits some expressive ways to write your web components. So… this is actually one of the reasons I added alias support. I wanted proper web components — spec-compliant, hyphenated, well-behaved — but I also wanted the freedom to write more semantic, human-friendly HTML.
Under the hood, everything stays compliant. We register a canonical element like track-item. But at authoring time, we can allow expressive tags like <song> or <book>. A small rewrite layer transforms those into their hyphenated counterparts before the browser even complains.

You get the discipline of the spec. But you also get to write HTML that reads like English.

  1. You can test your code with jsdom , and see if everything works as intended (jsdom breaks when you register a web component without hyphens).
  2. You can bend the spec because you added an alias and HTMLElementSugar rewrites the DOM for you.
  3. You can still reach the element via its official name, e.g., track-item.
  4. You can alias valid use-cases with optional default attributes

To see this in action:

TrackItem.alias('song', { type: 'song' }); YourBook.alias('book');

This adds the alias and sets some attributes on the track-item, although I realize that while writing, type doesn’t exist in the track-item example. But you can see where I’m going with this. Instead of your-book, you could use book.

So this allows you to write the following HTML:

<song></song> <!-- song here is sugar for --> <track-item type="song"></track-item> <!-- or in case of book --> <book attributes="here"></book> <!-- is alternative syntax for --> <your-book attributes="here"></your-book>

Aliases enable you to be expressive while remaining spec-compliant. And allows you to pass in often-used attributes or bend the spec when you absolutely want to, because you feel the semantics vibe better with mixtape instead of some-mixtape.

You do need to add one small utility helper in your HTML:

@import '@skirbi/sugar/aliases-register';

Otherwise it won’t work.

Configuration (without inheritance)

In my mixtape project, I found that I needed configuration from a parent component. So I decided to create a way to be able to select configuration details from a parent, but it expanded into something more. You can actually target specific elements. We do this with getNearestConfig().

You can define how configuration is found: via a CSS selector or a more complex XPath expression. It uses the first node that is found and checks if the element supports a getConfig() call. It will then return that configuration.

With a CSS pattern, it looks like this:

class TrackItem extends HTMLElementSugar { static getConfigSelector = 'track-config'; }

Which you can optionally override in HTML:

<track-item get-config-selector="track-config"></track-item>

This means your components don’t need to duplicate configuration. You only need to define the configuration once, thus making it leaner.

You could also opt for a more fine-grained search by using xpath:

<track-item get-config-xpath="//config-provider[@role='primary']"></track-item>

Sample the mixtape, remix your work

I was writing a mixtape. And once I got tired of having to type a lot of the same boilerplate, I wrote HTMLElementSugar, and it wasn’t trying to solve a complex problem. It tried to reduce noise and repetition. If you also want to avoid repetition and focus on writing just enough code to make it happen in a meaningful way: Sample it. Use it. Remix it.

You can install it using:

npm install @skirbi/sugar

Or check it out on npm: @skirbi/sugar

The hidden track

When I wrote this blogpost, sugar was alone. Since then it has grown, significantly. It now works under Livewire without having to use wire:ignore, it is resistant against morphing. And is now part of something bigger I call skirbi . The full ecosystem will most likely be featured in future blogposts.

Sugar, via skirbi, powers this blog via a hugo theme called @tatuahe/hugo . And the same suite powers the customer facing side of the Aruba Beach Tennis Open Championships management system.

You might also like