こんにちは!ちゅらデータぬおーすてぃんやいびーん!
概要
Googleが開発しているLit-htmlを使って、前回作ったフォーム部品のDOM変更をより簡単に実行する方法を紹介します。
前回の記事はこちら:
背景
これ前のWeb Componentの記事では、100% Vanilla(つまり、npmの第三者パッケージを使わない)Web Componentを作ってきました。
しかし、このようなやり方では、EventListenerを追加する、DOMの変更を行うなど、コードが冗長になりがちなのです。
そこで、lit-htmlというパッケージを使うと、ほぼほぼVanillaのまま、HTMLを書きながら、値を代入したり、EventListenerをつけたりすることが簡単にできます。
しかも、DOMの変更も、lit-htmlに行なってもらえるのです。
実は、lit-htmlはlitの一部の機能を切り離したもので、Web Componentsとの相性も抜群です。
また、VS Codeを使っている人なら、以下のLit-html用のプラグインをインストールすれば、VS CodeのIntellisenseがHTML・CSSに対して使えるようになります。
lit-htmlを使うことで、DOMをいじるわずらしさがなくなりつつ、VanillaのWeb Componentのメリットも満喫できます。
プロジェクトへの追加
以下のコマンドを実行して追加します。
yarn add lit-html
そしたら、package.jsonが以下のようになります。
{
"name": "webpack-wc",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"html-loader": "^3.1.0",
"html-webpack-plugin": "^5.5.0",
"to-string-loader": "^1.2.0",
"typescript": "^4.7.3",
"webpack": "^5.73.0",
"webpack-cli": "^4.9.2"
},
"dependencies": {
"lit-html": "^2.2.5"
}
}
これまでのwebpack設定を変えずにビルドできます!
前回のラジオ入力をリファクタリングしてみる
前回で作ったラジオ入力のWeb Componentでは、index.ts、template.html、styles.cssに分かれていました。
今回、template.htmlは無くなります。
import styles from "./styles.css";
import { html, render } from "lit-html";
export default class WcTrueFalseRadio extends HTMLElement {
#value = true;
#name = "wc-radio";
#labelText = "";
#trueRadio: HTMLInputElement;
#falseRadio: HTMLInputElement;
#hiddenInput = document.createElement("input");
constructor() {
super();
this.attachShadow({ mode: "open" });
if (!this.shadowRoot) throw Error("Shadow root not supported.");
const styleElement = document.createElement("style");
styleElement.innerHTML = styles;
this.shadowRoot.append(styleElement);
}
get value(): boolean {
return this.#value;
}
set value(newValue: any) {
this.#value = Boolean(newValue); // ラジオ入力の値に変化が生じたら、DOMにもその結果を自動的に反映させる
this.#render();
}
connectedCallback() {
const labelText = this.getAttribute("label"); // index.htmlで指定したlabelの値を取得する
if (!labelText) throw TypeError("label attribute must be defined.");
this.#labelText = labelText;
const inputName = this.getAttribute("name"); // Light DOMのフォームに入れる\<input name="">で使います。
if (!inputName) throw TypeError("name attribute must be defined.");
this.#name = inputName;
this.#render();
this.#trueRadio = this.shadowRoot!.querySelector<HTMLInputElement>("input#true")!;
this.#falseRadio = this.shadowRoot!.querySelector<HTMLInputElement>("input#false")!;
this.#hiddenInput = this.querySelector("input")!;
}
disconnectedCallback() {
this.#falseRadio.removeEventListener("click", this.#handleRadioInput); // EventListenerを外すことは基本的に不要ですが、メモリリークを意識することがGood Practiceです。
this.#trueRadio.removeEventListener("click", this.#handleRadioInput);
}
#handleRadioInput: EventListener = (event) => {
const target = event.currentTarget;
if (!(target instanceof HTMLInputElement)) throw Error("This listener must be used with an Input Element.");
this.value = Boolean(Number(target.value));
};
#render() {
if (!this.shadowRoot) throw Error("Cannot render Shadow DOM if not attached.");
const shadowDOMTemplate = html`
<span id="label">${this.#labelText}</span>
<div id="input-group">
<label for="true">はい</label>
<input
@click=${this.#handleRadioInput}
type="radio"
name="radio"
id="true"
checked
.checked=${this.value}
value="1"
aria-describedby="label"
/>
<label for="false">いいえ</label>
<input
@click=${this.#handleRadioInput}
type="radio"
name="radio"
id="false"
value="0"
.checked=${!this.value}
aria-describedby="label"
/>
<slot name="hidden-input"></slot>
</div>
`;
render(shadowDOMTemplate, this.shadowRoot); // なければ、Shadow DOMに追加、あれば、変わった箇所を変更する
const lightDOMTemplate = html`
<input type="hidden" slot="hidden-input" name=${this.#name} .value=${String(Number(this.value))} />
`;
render(lightDOMTemplate, this); // Light DOMに入ります。
}
}
このようになります。lit-htmlのテンプレート記号の解説は以下の通り
記号 | 意味 |
---|---|
@click | Vanilla JavaScriptのonclickに値します。@〜でありとあらゆるEventListenerを追加することができます。 |
?checked | checkedというAttributeをBooleanとして追加するかどうかを決めるもの。${}の中の変数がtrueだったら、<input checked>になります。falseだったら、<input>に。 checkedじゃなくても、任意のAttributeでも使えます。 |
.value | <input>のValueを設定するものですが、attributeとしてのValueではありません。DOMで見えるattributeとして指定したい時は、.valueではなく、valueを使用します。 |
参考のために元々あったtemplate.htmlを載せておきましょうね。これが不要になったのです。
<span id="label"></span>
<div id="input-group">
<label for="true">はい</label>
<input type="radio" name="radio" id="true" checked value="1" aria-describedby="label">
<label for="false">いいえ</label>
<input type="radio" name="radio" id="false" value="0" aria-describedby="label">
<slot name="hidden-input"></slot>
</div>
一度、renderが実行されれば、次から実行される時は違ったところのみを変えるようにします。ReactのようなVirtual DOMではありませんし、同じタグ名のものがあってもReactのようにkeyで特定して変更するような仕組みではないのでご注意ください。
まとめ
以上、lit-htmlを単独でWeb Componentで使う方法をご紹介しました。これはあくまでも提案であり、他の使い方ももちろんあるかと思います。
筆者は、このlit-htmlはとても便利なツールだと思いますので、使って便利そうな部品では導入すればいいかと思います。
今度、litを使ったWeb Componentをご紹介したいと思います!