1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Lit-htmlでWeb ComponentのDOM変更を簡単に行う方法

Last updated at Posted at 2022-06-14

こんにちは!ちゅらデータぬおーすてぃんやいびーん!

概要

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が以下のようになります。

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は無くなります。

wc-true-false-radio/index.ts
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をご紹介したいと思います!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?