[JavaScript] Web Componentsで明暗モード切替ボタンを作成する

概要

この記事について

かんたんな概要と結論

Web Componentsの利点と、利点を表現した簡単なデモを作成した。
Componentのカプセル化を用いることで、外部に影響されないスタイル、動作を設計することができる。

JavaScriptの標準APIであるWeb Componentsを学習している際に、
一度自分でもわかりやすいようにデモを作成してみようと思った。

今回は、画面あるいは指定ボックス内の明るいモード、暗いモードを切り替えるトグルボタンを、
Web Componentsを用いて作成した。

こちらからダウンロードできます(GitHub)。

Web Componentsとは?

ABOUT

Web Components は、再利用可能なカスタム要素を作成し、ウェブアプリの中で利用するための、一連のテクノロジーです。コードの他の部分から独立した、カプセル化された機能を使って実現します。 https://developer.mozilla.org/ja/docs/Web/Web_Components

Reactのような外部ライブラリを用いなくても、
標準APIのみで、DOMの一部とデザイン、イベントなどを一つのコンポーネントにまとめて使い回すことができる技術。

サンプル

下記「サンプルを作成する」で作成したサンプル(説明は後述)。
'🌞'のマークのトグルボタンをクリックすると、画面やボックスの色を切り替える。

PROS

  • コンポーネントの使い回しを可能とする
    DRYなコーディングを助ける。
  • styleをカプセル化することができる
    これは、あるセレクタに該当する要素に対する外部で指定されたstyleが、
    コンポーネント内(正確にはコンポーネント内のShadow DOM)に影響を与えず、
    また、コンポーネント内で記述されたstyleが外部の要素に影響を与えることはないということを意味している。
  • 動作のカプセル化をすることができる
    styleと同様に、clickイベントなども独自の挙動を定義することが可能。

CONS

  • コンポーネントの外部と内部とのデータのやり取りが苦手
    Shadow DOMを用いてカプセル化する場合は特に制限が強く、
    Reactのように自由気ままにイベント移譲をしたりstate管理したりすることは困難。
  • サポートしているブラウザに制限がある
    今後の開発に期待。

サンプルを作成する(明暗モード切替ボタン)

ABOUT

画面、あるいは指定ボックス内の明るいモード、暗いモードを切り替えるトグルボタンを作成したい。

要件

  • スタイルやクリック時のイベントを内部で設定したい
  • スタイルの一部(ボタンの大きさ)は外部で定義した値を使えるようにする
  • 切り替え対象の要素も、外部で定義した値を使えるようにする

コード

toggle-color-button-component.ts

 1const style = `
 2:host {
 3    --toggle-color-button-criteria-len:var(--toggle-color-button-len,75px);
 4    width: var(--toggle-color-button-criteria-len);
 5    height: calc(var(--toggle-color-button-criteria-len) / 1.78);
 6}
 7div{
 8    position: relative;
 9    width: var(--toggle-color-button-criteria-len);
10    height: calc(var(--toggle-color-button-criteria-len) / 1.78);
11    margin: auto;
12    display: inline-block;
13}
14input {
15    position: absolute;
16    left: 0;
17    top: 0;
18    width: 100%;
19    height: 100%;
20    margin:0;
21    z-index: 5;
22    opacity: 0;
23    cursor: pointer;
24}
25label {
26    width: 100%;
27    height: 100%;
28    background: #ffeb3b;
29    position: relative;
30    display: inline-block;
31    border-radius: calc(var(--toggle-color-button-criteria-len)/1.63);
32    transition: 0.4s;
33    box-sizing: border-box;
34}
35label:after {
36    content: '🌞';
37    position: absolute;
38    left: 0;
39    top: 0;
40    z-index: 2;
41    transition: 0.4s;
42    font-size: calc(var(--toggle-color-button-criteria-len)/2.5);
43    line-height: calc(var(--toggle-color-button-criteria-len)/1.70);
44}
45input:checked + label {
46    background-color: #3d00a9;
47}
48input:checked + label:after {
49    content: '🌙';
50    left: calc(var(--toggle-color-button-criteria-len) / 2.14);
51}
52`;
53
54const template = `
55<div>
56    <input id="toggle" type='checkbox' />
57    <label for="toggle" />
58</div>
59`;
60
61const tmpl = document.createElement("template");
62tmpl.innerHTML = `<style>${style}</style>${template}`;
63
64customElements.define("toggle-color-button", class extends HTMLElement {
65    connectedCallback() {
66        // shadowDOMの設定
67        const shadowRoot = this.attachShadow({ mode: "closed" });
68        shadowRoot.append(tmpl.content.cloneNode(true));
69        // トグルする対象の要素
70        const toggledElem = document.querySelector<HTMLElement>(this.dataset.toggled || "html");
71        if (!toggledElem) return;
72        // ボタンクリック時にトグル
73        shadowRoot.querySelector("input")?.addEventListener("click", () => {
74            toggledElem.dataset.mode =
75                toggledElem.dataset.mode !== 'dark' ?
76                    'dark'
77                    : 'light';
78        })
79    }
80});

template要素にコンポーネントのDOM構造を記述する。
内部で使われるstyleも文字列として記述。

なお、ここではstyleを文字列としてベタ書きしたけれども、
Sass用のライブラリやCSS-in-JSのライブラリを用いてうまいことトランスパイルしてあげれば
保守性を高めることができるだろう(Web Componetns内部に記述するstyleなどそこまで多くはないので、だいたいは通常のCSSで事足りるだろうけど)

customElements.defineメソッド実行によって
スクリプトを読み込んだHTML側で<toggle-color-button></toggle-color-button>の形でカスタムタグを使用することができるようになる。

index.html

 1<!DOCTYPE html>
 2<html lang="en">
 3
 4<head>
 5    <meta charset="UTF-8">
 6    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 7    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 8    <title>Document</title>
 9    <script src="./dist/toggle-color-button-component.js"></script>
10    <style type="text/css">
11        #box-one {
12            --toggle-color-button-len: 50px;
13            margin: 60px auto;
14            max-width: 400px;
15            border: solid 2px;
16        }
17
18        #box-one[data-mode="dark"] {
19            background-color: darkblue;
20            color: lightblue;
21        }
22
23        #box-two {
24            --toggle-color-button-len: 40px;
25            margin: 60px auto;
26            max-width: 600px;
27            border: solid 2px;
28            text-align: center;
29        }
30
31        #box-two input {
32            display: none;
33        }
34
35        #box-two label:before {
36            font-family: FontAwesome;
37            display: inline-block;
38            content: "□";
39            color: blue;
40            letter-spacing: 10px;
41
42        }
43
44        #box-two input:checked+label:before {
45            content: "✔";
46        }
47
48        html[data-mode="dark"] * {
49            background-color: darkslategrey;
50            color: rgb(255, 230, 0);
51
52        }
53
54        label,
55        input {
56            cursor: pointer;
57        }
58    </style>
59</head>
60
61<body>
62    <div id="box-one">
63        <p>ボックス内のダークモード切り替えボタン:<toggle-color-button data-toggled="#box-one"></toggle-color-button>
64        </p>
65
66    </div>
67    <div id="box-two">
68        <div>
69            画面全体のダークモード切り替えボタン:<toggle-color-button></toggle-color-button>
70        </div>
71        <p><input id="switchA" type="checkbox" /><label for="switchA">スイッチA</label></p>
72        <p><input id="switchB" type="checkbox" /><label for="switchB">スイッチB</label></p>
73        <p><input id="switchC" type="checkbox" /><label for="switchC">スイッチC</label></p>
74        <p><input id="switchD" type="checkbox" /><label for="switchD">スイッチD</label></p>
75        <hr>
76        <div>
77            <p>スイッチ稼働状態</p>
78            <p>スイッチA:<span id="result-switchA">OFF</span></p>
79            <p>スイッチB:<span id="result-switchB">OFF</span></p>
80            <p>スイッチC:<span id="result-switchC">OFF</span></p>
81            <p>スイッチD:<span id="result-switchD">OFF</span></p>
82        </div>
83
84    </div>
85    <script>
86        for (const input of document.querySelectorAll("#box-two input")) {
87            input.addEventListener("click", (event) => {
88                const state = event.currentTarget.checked;
89                document.querySelector("#result-" + event.target.id).textContent = state ? "ON" : "OFF";
90            })
91        }
92
93    </script>
94</body>
95
96</html>
切り替え対象の要素の指定

box-one要素では、
data-toggled属性として#box-oneを指定することで、
明暗モードを切り替える対象をbox-one内に限定している。

これを指定しない場合、画面全体(ルート要素=html要素)が切り替えられることになる

styleの独立

box-two要素内で、
チェックボックスやlabelなどに色や大きさなどのstyleを指定しているが、
box-two内部にあるはずのWeb Componentsには影響しない。

また、Web Componentsでグローバルに宣言したチェックボックスなどのstyleも、
box-two内のほかのチェックボックスに影響しない。

styleの一部指定

ボタンの大きさは、
--toggle-color-button-lenというCSS変数で指定可能になっている。

上記の例では、box-onebox-twoで別々の大きさのトグルボタンを実装している。

Shadow Domのスタイルの値を指定するためには、
このようにCSS変数の形で外部から注入する方法が許されている。

動作の独立

box-two内のチェックボックスは、
それぞれ対応する稼働状態表示欄にON/OFFを報告するイベントを設定している。

もしShadow DOMによるカプセル化が行われていなければ、
Web Components内部のチェックボックスもまた同様のイベントが設定され、対応する表示欄がないためにエラーでスクリプトが落ちる。

しかし、Shadow DOMのおかげで、ここではコンポーネントはそのイベントが設定されず、エラーが起こらない。

デモ

CodeSandBox

関連記事

comments powered by Disqus