簡単なカスタムタグ

ごく簡単なカスタムタグを作成するサンプルです。
次のような構成のカスタムタグを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div>
    <div>
        <slot></slot>
    </div>
    <div>
        <div class="inline-div">
            <slot name="inner1"></slot>
        </div>
        <div class="inline-div">
            <slot name="inner2"></slot>
        </div>
    </div>
</div>

ソースコード

とりあえずソースコードは次のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebContent Sample</title>
    <script>
    class TwoSlot extends HTMLElement {
        static get template() {
            return `
            <div>
                <div>
                    <slot></slot>
                </div>
                <div>
                    <div class="inline-div">
                        <slot name="inner1"></slot>
                    </div>
                    <div class="inline-div">
                        <slot name="inner2"></slot>
                    </div>
                </div>
            </div>
            <style>
            .inline-div {
                display: inline-block;
            }
            </style>
            `;
        }
 
        constructor(){
            super();
 
//          // こっちに書いても<slot>の変更は効く
//          // 多分、JSでやっている更新処理が効かない
//          let sr = this.attachShadow({mode: "open"});
//          sr.innerHTML = TwoSlot.template;
        }
 
        connectedCallback(){
            let sr = this.attachShadow({mode: "open"});
            sr.innerHTML = TwoSlot.template;
        }
    }
 
    customElements.define('two-slot', TwoSlot);
    </script>
</head>
<body>
    <two-slot id="TwoSlotSample">
        <p slot="inner1" class="left">hoge</p>
        <p slot="inner2" class="right">fuga</p>
        <div class="main">
            <h1>タイトル</h1>
            <div>
                これはメインのコンテンツです。
            </div>
        </div>
    </two-slot>
 
    <script>
    let elm = document.getElementById("TwoSlotSample");
    elm.querySelector(".left").innerHTML = `
        <span style="color:red; border:solid red 1px;">FOO</span>
    `;
    </script>
</body>
</html>

説明

TwoSlotクラス

このカスタムタグを作るためのクラスです。
<slot>は3個あるのにTwoSlotなのは色々試している間にスロットが1つ増えてしまったせいです。気にしないでください。

まず、このクラスはHTMLElementを継承しています。
カスタムタグはHTMLElementクラスを継承しなければならないので、こういうものだと思っておきます。

クラスには、
  • template()
  • constructor()
  • connectedCallback()
の3つのメソッドを実装しています。
次にこれらについて説明します。

template()

この静的なgetterメソッドはTwoSlotカスタムタグのシャドウルート以下のHTMLを返します。本来なら<template>タグを使うのがより良い方法なのでしょうが、こうしておけばJSだけに閉じられるので今回は単なる文字列で実装しました。
ちなみに`から`で囲まれた部分(テンプレート文字列と言うらしい)はヒアドキュメントの様に複数行の文字列を記述できる上、${変数}で変数展開もしてくれると言う非常に便利なヤツです。これを使えばHTMLをそのまま書いておけば良いので非常に楽チンです。
このHTMLを返すのはインスタンスメソッドである必要はないので静的メソッドで実装します。

constructor()

コンストラクタです。
実際にはsuper()でHTMLElementのコンストラクタを呼んでいるだけです。今回のサンプルはメンバ変数を持たないのでこれだけです。

コメントアウトしてありますが、次に説明するconnectedCallback()の処理をこちらに書いてもこのサンプルの内容だと同じ様に動作します。
これは、このカスタムタグが生成された後はHTMLの機能だけしか使わないのでconstructorに書いても同じことになるからですが、どこかのWebページにconnectedCallback()に書くべきとか書いてあったのでこちらをコメントアウトしてあります。
でもMDNによると、
constructor():要素の作成またはアップグレード時に呼び出されます
connectedCallback():要素がドキュメントに挿入されたときに呼び出される、シャドウツリーへの呼び出し
とあるので、constructor()でも良い様な気がします?

connectedCallback()

4つあるライフサイクルコールバックの一つで、先にも書いた通り、
「要素がドキュメントに挿入されたときに呼び出される、シャドウツリーへの呼び出し」
のコールバックメソッドです。
このTwoSlotタグがドキュメントに挿入された時に呼び出されてシャドウルート以下にtemplate()のHTMLを作るためにここで実装しています。

this.attachShadow()でシャドウルートをこのエレメント(TwoSlotタグ)にアタッチしてその参照を得ます。
それからシャドウルートのinnerHTMLにHTML文字列を突っ込むとシャドウルート以下に目的のHTMLが構成されます。
これでカスタムタグの出来上がりです。

customElements.define()

これでJSで作ったカスタムタグをHTMLに登録?します。
カスタムタグは名前に「-」を含まなければならないので「two-slot」と言う名前にしています。

HTML部分

50〜59行目で実際にタグを使っています。
実際の表示は次の様になります。
現在ではChrome以外では機能しませんが次の様になります(ページの表示上の問題でサンプルコード中の<h1>を<strong>に変えています)。

hoge

fuga

タイトル
これはメインのコンテンツです。

HTMLのソースでは「タイトル」と「これはメインのコンテンツです。」が「hoge」、「fuga」の下に書かれているのに、カスタムタグに適用されたことで「タイトル」と「これはメインのコンテンツです。」が上に、「hoge」と「fuga」は下に書かれ、「hoge」と「fuga」は横に並んで表示されるのでカスタムタグが機能していることがわかります。

<slot>

下の表示はサンプルコードの最下部にある<script>内の処理を適用した場合です。「hoge」の部分を書き換えるとそれが反映されます。

FOO

fuga

タイトル
これはメインのコンテンツです。

これは<slot>タグの機能です。
<slot>タグはカスタムタグの子要素をカスタムタグの内部に反映するためのタグで、名前付きと名前なしのものがあります。
名前付きのスロットに対応する要素は「<p slot="inner1" class="left">hoge</p>」の様にslot属性で名前を指定します。
名前を指定しなかった子要素は全部名前なしの<slot>タグに反映される様です。

この<slot>は非常に便利です。
<slot>は対応するカスタムタグの子要素の参照になっています。
シャドウルート以下の要素は外部から隔離されていて触れないのですが、カスタムタグの子要素は触ることができます。
なので、カスタムタグの子要素を変更することでカスタム要素内のタグの構成やスタイルを変更できる上、特になんの処理を実装しなくてもその変更を即座に反映することができます。
そして単なる参照なので要素の追加削除などは行われず、反映に必要な負荷は再レンダリングだけで済むと思われます。

まとめ

ごく簡単なカスタムタグを実装してみました。
JSが必要になりますが、かなり簡単にカスタムタグを実装できます。
これは非常に便利だと思います。例えば商品カタログの一覧ページの様な物を考えた場合、「商品名」、「画像」、「商品概要」、「値段」をひとかたまりにするためいくつかの<div>で商品1つ分のレイアウトを作成し、それをいくつも書くことになりますが、それがカスタムタグでシンプルに表現できます。と言うか、文章は<p>、画像は<img>で単にカスタムタグ内のドキュメントとして記述すればよく、レイアウトのための<div>を本体のHTML内に書かなくてよくなるため非常に見通しの良いHTMLを書けます。
その上、<slot>が非常に便利で、対応する要素の変更が非常に低負荷で行えます。
WebComponentsが革命だと言われるのがよく分かります。メジャーなブラウザが一通りこの機能を実装したらあっという間にカスタムタグライブラリの様なものができてきて、HTML作成を一変させることになると思います。