![今のところinput[type=”number”]を使わない方が良い理由と代替案 アイキャッチ](https://www.fourier.jp/storage/blog/post-outline/8vnrBTPg2GKmF2G4dn5VjZAcdMOa1pzh.jpg)
はじめに
Reactに慣れてきた頃、WebComponentsを使う機会がありました。
Reactだと書けるが、WebComponentsでは書けないことがあり、ハマったので記事にしてみます。
ハマったところ
select
をコンポーネントにしてみます。
Reactの場合
MUIを参考にしてみます。
<Select>
 <MenuItem value={10}>Ten</MenuItem>
 <MenuItem value={20}>Twenty</MenuItem>
 <MenuItem value={30}>Thirty</MenuItem>
</Select>
select
のコンポーネントと言えば、こんな感じですよね。
WebComponentsで実装した場合
MUIのように理想としては以下のように作りたいところです。
<x-select>
	<x-option value="10">Ten</x-option>
	<x-option value="20">Twenty</x-option>
	<x-option value="30">Thirty</x-option>
</x-select>
では作成してみましょう。
まずはWebComponentsのおさらいですが、以下のようにすれば div
で囲って slot
が入るだけのコンポーネントを作れます。
customElements.define('x-div', class extends HTMLElement {
 constructor() {
 super();
 this._root = this.attachShadow({mode: 'closed'});
 }

 render() {
 this._root.innerHTML = `<div><slot/></div>`;
 }

 connectedCallback() {
 this.render();
 }
});
<x-div>SLOTの内容<x-div>

上に従って x-select
と x-option
を作ってみましょう。
customElements.define('x-select', class extends HTMLElement {
 constructor() {
 super();
 this._root = this.attachShadow({mode: 'closed'});
 }
 render() {
 this._root.innerHTML = `<select><slot/></select>`;
 }
 connectedCallback() {
 this.render();
 }
});

customElements.define('x-option', class extends HTMLElement {
 constructor() {
 super();
 this._root = this.attachShadow({mode: 'closed'});
 }
 render() {
 this._root.innerHTML = `<option><slot/></option>`;
 }
 connectedCallback() {
 this.render();
 }
});
<x-select>
 <x-option value="10">Ten</x-option>
 <x-option value="20">Twenty</x-option>
 <x-option value="30">Thirty</x-option>
</x-select>
実際に動かしてみます。

何故 slot
の中に、何も入らなかったのでしょうか?
一応 x-option
を使わずに実行してみます。
<x-select>
		<option value="10">Ten</option>
		<option value="20">Twenty</option>
		<option value="30">Thirty</option>
</x-select>

これも slot
の部分に、何も入っていないことが分かります。何故でしょうか?
これはWebComponentsの仕様が関係しています。
子に <slot/>
を持てる要素は attachShadow()
が使える要素に限るのです。
使用できる要素の一覧は以下で確認できます。ここに select
が含まれていないことが分かります。
Customized built-in elementの場合
先ほどの例は、自立カスタム要素(Autonomous custom element)で実装したものでした。
では、カスタマイズされた組み込み要素(Customized built-in element)ではどうでしょうか?
コードを以下のように変えてみます。
customElements.define('x-select', class extends HTMLSelectElement {
 constructor() {
 super();
 this._root = this.attachShadow({mode: 'closed'});
 }
 render() {
 this._root.innerHTML = `<select><slot/></select>`;
 }
 connectedCallback() {
 this.render();
 }
}, { extends: 'select' });

customElements.define('x-option', class extends HTMLOptionElement {
 constructor() {
 super();
 this._root = this.attachShadow({mode: 'closed'});
 }
 render() {
 this._root.innerHTML = `<option><slot/></option>`;
 }
 connectedCallback() {
 this.render();
 }
}, { extends: 'option' });
<select is="x-select">
		<option value="10" is="x-option">Ten</option>
		<option value="20" is="x-option">Twenty</option>
		<option value="30" is="x-option">Thirty</option>
</select>
実際に動かしてみると、JavaScriptのエラーが発生しました。
Uncaught DOMException: Failed to execute 'attachShadow' on 'Element': This element does not support attachShadow at new customElements.define.extends
この原因も先ほどと同じです。
 select
や option
は attachShadow()
が出来ないので、このような実装をする事が出来ないのです。
Declarative Shadow DOMの場合
最近モダンブラウザで使えるようになった、 Declarative Shadow DOM もせっかくなので試してみましょう。
<x-select>
 <template shadowrootmode="close">
 <select>
 <slot></slot>
 </select>
 </template>
</x-select>

<x-option>
 <template shadowrootmode="close">
 <option>
 <slot></slot>
 </option>
 </template>
</x-option>

<x-select>
 <option value="10">Ten</option>
 <option value="20">Twenty</option>
 <option value="30">Thirty</option>
</x-select>

<x-select>
 <x-option value="10">Ten</x-option>
 <x-option value="20">Twenty</x-option>
 <x-option value="30">Thirty</x-option>
</x-select>

がっ……駄目っ……! 理由はやはり同じです。
結論
WebComponentsでは、React(MUI)のような書き方が出来ないことが分かりました。
ではどうするのか
Vue.jsを思い出してみてください。Vuetifyの Select
では以下のような書き方をします。
<v-select
 label="Select"
 :items="['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']"
></v-select>
このように属性(Props)を使うような書き方をすれば、WebComponentsでもそれらしい事が出来ます。
JavaScriptで属性を確認して、 option
をShadowDOM内に生成するようにします。
JSON.parse(this.getAttribute('items')).forEach(item => {
 const option = document.createElement('option');
 option.textContent = item;
 option.value = item;
 this._root.querySelector('select').appendChild(option);
});
こちらでも、課題はいくつかあります。
課題1 何を入れたら良いのか直感的に分かりづらい
今回はitemsの中身に配列(JSON)を入れていますが、これは実装者によるため予想不能です。
属性名がvalueかもしれませんし、 options
かもしれません。
配列じゃなくて、CSV形式( California, Colorado, ...
)かもしれません。
そのため、チームで統一するようにするか、コンポーネントを使う度にドキュメントを見直す事になります。
課題2 破壊的変更になりがち
select
には option
以外にも、 optgroup
を使う事が出来ます。
それを運用中に実装するとなったら困ることになります。
例えばitemsを配列で実装していた場合は、ここで詰むことになります。
破壊的変更をするか、新しく optgroup
に対応した select
コンポーネントを作るかという二択になってしまいます。
JSONで option
か optgroup
が選択出来るように実装していた場合はラッキーですが、課題1の問題点明らかに出ていると思います。
<x-select items="[
 {'type': 'optgroup', 'label': 'グループ', 'options': {{'label': '公開', 'value': '1'}, {'label': '非公開', 'value': '0'}}},
]"/>
課題3 読みづらい
これは個人的な感想ですが、Reactなどの書き方と比べると、分かりづらい書き方を強要されます。
Litなどのフレームワークを使えば多少マシになりますが、ReactやVue.jsで書きたくなること間違いなしです。
最後に
Reactと比較したWebComponentsの実装を本記事で書きました。
この問題はWebComponentsがあまり使われていない理由の一つになっていると思います。
attachShadow()
を使う事が出来る要素が増えれば解決するのですが、セキュリティ上の都合による理由など簡単には行かないようです。
それでも、WebComponentsのポテンシャルは無視できません。今後のアップデートに期待したいところです。