Decorator Pattern Plus
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.
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.
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.