Decorator Pattern Plus

Decorator Pattern + LitElement & lit-html = 😍

westbrook
6 min readDec 4, 2018

このテキストを日本語で読む。

Photo by Rene Böhmer on Unsplash

Recently I’ve been experimenting with an interesting way to use shadow DOM. Is it silly? Is it useful? I’m not sure, but I’ve been enjoying it, so I’m going to teach it to you. Right now I’m using this technique with form elements that I’ve created as custom elements, but it’s possible that this would be useful in other contexts. But first, what type of problem am I solving?

In the following example outlines the basics of how to make to make an input element with LitElement, custom elements and shadow DOM.

<sugoi-input></sugoi-input>...customElements.define('sugoi-input', class extends LitElement {
render() {
return html`<input />`;
}
});

From this baseline, you can easily keep styles, functionality, and child DOM encapsulated within sugoi-input. In fact, this is one of the most common used of custom elements and shadow DOM. However, if you use an element built in this way inside of a form you will soon run into issues. With the input sitting inside of the elements shadow DOM, standard serialization of a form would not capture values inputed therein. Because of this, even were you to add name='sugoi’ to the input in the shadow DOM, you still wouldn’t be able to use the following code.

<form>
<sugoi-input></sugoi-input>
<input name="futsuu" value="普通" />
</form>
...customElements.define('sugoi-input', class extends LitElement {
render() {
return html`<input name="sugoi" />`;
}
});
...let form = document.querySelector('form');
let formData = new FormData(form);
console.log(formData.get('futsuu')); // 普通 <= This value is read.
console.log(formData.get('sugoi')); // null <= This value is not.
Photo by Thought Catalog on Unsplash

But, please don’t be sad. We can easily solve this problem with the decorator pattern. If you decorate a standard input with sugoi-input we will once again be able to serialize it into a form.

<form>
<sugoi-input>
<input name="sugoi" value="凄い" />
</sugoi-input>
<input name="futsuu" value="普通" />
</form>
...let form = document.querySelector('form');
let formData = new FormData(form);
console.log(formData.get('futsuu')); // 普通 <= This value is read.
console.log(formData.get('sugoi')); // 凄い <= This one too. Yay!

How do we do it?

When using shadow DOM, a <slot/> element will allow elements in the light DOM to be projected into the shadow DOM. Read this article for other useful features of the <slot/> element. In our LitElement we just have to change the input into a slot.

<sugoi-input>
<input name="sugoi" value="凄い" />
</sugoi-input>
...customElements.define('sugoi-input', class extends LitElement {
render() {
return html`<slot></slot>`;
}
});

After this change, if you want to use custom styles you would need to change you input {} based selectors to ::slotted(input) {} inside of your shadow DOM.

This approach can really make the work of creating a form troublesome. Every time you use a <sugoi-input></sugoi-input> you will have to put a <input/> element into it. This is easy enough at the top level of your HTML, and using the decorator pattern there does mean that your users will have access to this form element even when they have javascript off. However, you have to manage the insertion of an <input/> into every <sugoi-input></sugoi-input> across your entire application. Let’s go a step further and prepare sugoi-input so that it takes care of this and we don’t have to remember.

LitElement and lit-html

Normally, when using LitElement the only part of lit-html that you have to worry about is the syntax. Once you write your render() method using lit-html syntax, LitElement takes care of the rest of the interaction between the two. LitElement is really useful, but if you use it import {render} from ‘lit-html/lib/shady-render.js’ you unlock the ability to use decorator pattern plus. But, what is decorator pattern plus?

When you only use shadow DOM, your input has to come from there. With the standard decorator pattern, your input has to come from the light DOM. When you use decorator pattern plus it will also come from the light DOM, but if there isn’t an input in your light DOM, your custom element will put one there for you. So, how are we able to do that?

<form>
<sugoi-input>
<input name="sugoi" value="凄い" />
</sugoi-input>
<sugoi-input
name="sugoi"
value="凄い"
></sugoi-input>
</form>
...import {render} from ‘lit-html/lib/shady-render.js’customElements.define('sugoi-input', class extends LitElement {
static get properties() {
return {
name: { type: String },
value: { type: String }
};
}
connectedCallback() {
this.buildLightDOMInput();
}
render() {
return html`<slot></slot>`;
}
updated() {
this.updateLightDOMInput();
}
buildLightDOMInput() {
if (this.$element || this.querySelector('input')) return;
this.$element = this.initGeneratedInput;
}
updateLightDOMInput() {
if (this.$element) this.renderInput();
}
initGeneratedInput() {
this.renderInput();
return this.querySelector('input');
}
renderInput() {
render(this.inputTemplate(), this, this.localName);
}
inputTemplate() {
return html`
<input
name="${this.name}"
.value="${this.value || ''}"
/>
`;
}
});

In this example, when the parent form is serialized both ways of writing the sugoi-input element would be visible. This way using sugoi-input is even easier, both html at the top level and within your application code.

To use decorator pattern plus, put a <slot/> into the render() method of LitElement so that the input we see comes from the light DOM. The connectedCallback() checks whether or not there is an input in the light DOM and build a new input when one is missing. If one is created, it’s easy to keep it up-to-date with the help of lit-html. In every call to the updated() method, when a built input is present, it is given the current component properties. This way, both the standard template for the LitElement and the template for the decorator pattern plus’ input are managed appropriately.

Other benefits

This technique is certainly useful when serializing a form, however there are other benefits to its use. Because the parent form can access the sugoi-input, that input can also be auto-completed. If you want to make an easy to use application, auto-complete is important. It allows your users to fill out forms more quickly.

It’s good to allow your users to use their browser’s auto-complete.

Another benefit of using decorator pattern plus is that password managers will also be able to recognize your input. Whether it’s LastPass or keeper or Sticky Password or something else, this also makes your site easier for your visitors to use.

What’s next?

I think that decorator pattern plus is a good technique, and the way of using it in this article is a new way of thinking. Let’s research together if there are even more useful ways of using it. There are a number of interesting experiments to be had. It would be good to think about other attributes of the input element. This article doesn’t get into them, but there are many more attributes like placeholder, autocomplete, pattern and more that are useful. Should they be put into the input similar to how name and value are being done already?

<sugoi-input
name="sugoi"
value="凄い"
placeholder="何ですか?"
autocomplete="name"
pattern="[a-z]{4,8}"
><sugoi-input>
<sugoi-input>
<input
name="sugoi"
value="凄い"
placeholder="何ですか?"
autocomplete="name"
pattern="[a-z]{4,8}"
/>
<sugoi-input>

We also need to think about the way we want to apply a label to this input. Should this come from the light DOM, the shadow DOM, or the custom element? If we use a <slot/>, is it good to put all of the html be direct to a single one? Because accessibility is so important, we need to find a good answer for this.

<sugoi-input
name="sugoi"
value="凄い"
label="今日は?”
><sugoi-input>
<sugoi-input>
<label for="sugoi">今日は?</label>
<input
id="sugoi"
name="sugoi"
value="凄い"
/>
<sugoi-input>

After the standard abilities of an input, what other functionality should be added this way? Error messaging, search ahead, prefix/suffix application, all of these things and more are very interesting. When JavaScript is turned off, what features are good to make available?

These are some of my questions so far. If you also have questions, please share them in a comment below.

As translated, by myself, from the Japanese, also by myself, of the original publication of this article. All questionable original content and possible more questionable translation content is my own.

Special thanks to my Lingo Live coach Asami Nishikaku and my Hills Learning teacher Komi Kaoru for their support in crafting the Japanese version of this post.

--

--