カスタム要素を作ってみる
それでは今回の締めくくりとして、ここまで例示してきた <my-great-beautiful-button> の内部実装を解説しましょう。main.jsというファイル名で、リスト2の内容を実装します。
// (1) カスタム要素の振る舞いはクラス構文で実装する
class MyGreatBeautifulButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" }); // (3) カスタム要素に空のシャドウDOMを紐付ける
// HTML側で指定されたbackgroundColor属性やcolor属性の値を参照する
const backgroundColorAttr = this.getAttribute('backgroundColor') || '#eee';
const colorAttr = this.getAttribute('color') || '#000';
// (4) シャドウDOMの文書構造を定義する
this.shadowRoot.innerHTML = `
<style>
button {
border: none;
border-radius: 8px;
padding: 10px 20px;
background-color: ${backgroundColorAttr};
color: ${colorAttr};
}
</style>
<button type="submit">
<slot name="label" /><!-- (5) 利用側のパラメータを差し込む -->
</button>
`;
}
}
// (2) カスタム要素を登録する
customElements.define("my-great-beautiful-button", MyGreatBeautifulButton);
カスタム要素の定義は、
- (1)の要素本体を表すクラスの定義
- (2)(1)のクラスを登録する処理
から構成されます。これによって <my-great-beautiful-button> をHTMLとして読み込まれた際に(2)で登録した(1)のクラスが実行されます。
ただし、それだけでは空の要素ができるだけなので、(3)で作成したシャドウDOMツリーに(4)で文書構造を与えています。
ボタンのテキストは利用する側から差し込みたいので、(5)に <slot> 要素を定義しました。 name="label" という属性で名前を定義しておいたのが、後ほどHTML側から利用する際に役立ちます。
次は、登録したカスタム要素をHTMLの中で使ってみましょう(リスト3)。
<!DOCTYPE html>
<html lang="ja">
<body>
<div>
<my-great-beautiful-button><!-- (2) -->
<span slot="label">submit now!</span><!-- (3) -->
</my-great-beautiful-button>
</div>
<div style="margin-top: 8px;">
<my-great-beautiful-button backgroundColor="#008CCF" color="#FFFFFF" >
<span slot="label">submit now!</span>
</my-great-beautiful-button>
</div>
<script src="./main.js"></script><!-- (1) -->
</body>
</html>
リスト2で作成したJavaScriptファイルは、HTML内のカスタム要素を利用している箇所よりも下、body要素の下部(1)で読み込んでおきましょう。
カスタム要素は通常のHTMLと同じように(2)のような使い方をします。少し見慣れない点としては、(3)のslot属性でしょうか。(3)でslotを定義した場合、リスト2の<slot>要素が(今回の例では<span>要素に置き換わります。これはHTMLテンプレートの機能の一部です。
HTMLテンプレートで文書構造を再利用する
リスト2の例ではカスタム要素の初期化処理(constructor())に文書構造を直接埋め込んでいましたが、HTMLファイルではなくJavaScriptの文字列として定義していたため、エディタで書いている際にも補完などの入力支援を受けられず、心細い開発体験になってしまったことと思います。やはりHTMLは .html 拡張子のHTMLファイルに書いたほうが気分がいいかと思いますので、次の例では、文書構造を再利用可能なHTMLテンプレートとして記述してみましょう。
新たにHTMLファイルを作り、 <template>要素の中にカスタム要素の文書構造となるHTMLを記述します(リスト4)。
<!DOCTYPE html>
<html lang="ja">
<body>
<!-- HTMLテンプレートを定義する -->
<template id="my-great-beautiful-button">
<style>
button {
border: none;
border-radius: 8px;
padding: 10px 20px;
background-color: brown;
color: yellow;
}
</style>
<button type="submit">
<slot name="label"></slot>
</button>
</template>
</body>
</html>
HTMLテンプレートは単独では表示の対象にならないため、このまま表示しても真っ白な画面が出るだけです。次は、<template>要素に付与したidを使用して、カスタム要素を定義してみましょう(リスト5)。
class MyGreatBeautifulButtonWithTemplate extends HTMLElement {
constructor() {
super();
// (1) HTMLテンプレートを呼び出す
const template = document.getElementById('my-great-beautiful-button');
// (2) テンプレート内の構造をコピーする(操作しても元のテンプレートを壊さないようにする)
const templateContent = template.content.cloneNode(true);
// (3) attachした戻り値でシャドウDOMのルート要素を入手する
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(templateContent); // (4) テンプレートをシャドウDOMのツリーに追加する
}
}
customElements.define(
"my-great-beautiful-button-with-template",
MyGreatBeautifulButtonWithTemplate
);
リスト2でinnerHTMLに文字列を与えていた処理の代わりに、リスト4のHTMLテンプレートを当てはめる処理を行っています。
まずは(1)でidを利用して<template>要素を呼び出します。<template>要素の中身は contentというプロパティに格納されていますが、これをそのまま使ってしまうと元のテンプレートを壊してしまって他の再利用先に迷惑がかかるので、(2)のようにcloneNodeメソッドを使ってコピーしたものを利用します。引数にtrueを与えることでcontent以下の全要素をコピーします。(3)はリスト2と同じようにattachShadowメソッドを呼び出してカスタム要素にシャドウDOMを紐づけていますが、この戻り値でシャドウDOMのルート要素が得られるためshadowRoot変数に保持しておきます。最後に、(4)でシャドウDOMに(2)でテンプレートから作成した要素を追加することで、このカスタム要素にHTMLテンプレート由来の文書構造が登録されます。
それでは、リスト5のmain2.jsで作成した<my-great-beautiful-button-with-template>要素を、index2.htmlから呼び出しましょう。カスタム要素を追加したり、テンプレートの閉じタグ(</template>)の下に<script>要素を配置します(リスト6)。
<!DOCTYPE html>
<html lang="ja">
<body>
<!-- 追加:ここから -->
<div>
<my-great-beautiful-button-with-template>
<span slot="label">submit now! (with template)</span>
</my-great-beautiful-button-with-template>
</div>
<!-- 追加:ここまで -->
<template id="my-great-beautiful-button">
<!-- 省略 -->
</template>
<script src="./main2.js"></script><!-- 追加 -->
</body>
</html>
この状態でブラウザに表示すると、図5のようになります。
図5:HTMLテンプレートを利用した例
テンプレートの文書構造を利用したカスタム要素を作ることができました。
このままだと背景色や文字色がカスタマイズできないので、もう少し手を加えてみましょう。といっても、リスト2のように文字列として色指定を埋め込むことはできないので、CSSカスタムプロパティの仕組みを利用して、パラメータを外側から与えることにします。カプセル化が崩れたように感じられるかもしれませんが、外部から操作できる限られた方法を提供するのはカプセル化の要素の一つなので、問題ありません。
JavaScriptには手を加える必要がないので、HTMLのみを編集します(リスト7)。
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- (2) 追加:ここから -->
<style>
my-great-beautiful-button-with-template.bright {
--background-color: yellow;
--color: gray;
}
</style>
<!-- (2) 追加:ここまで -->
</head>
<body>
<div>
<my-great-beautiful-button-with-template>
<span slot="label">submit now! (with template)</span>
</my-great-beautiful-button-with-template>
</div>
<!-- (3) 追加:ここから -->
<div style="margin-top: 8px;">
<my-great-beautiful-button-with-template class="bright">
<span slot="label">submit now! (with template)</span>
</my-great-beautiful-button-with-template>
</div>
<!-- (3) 追加:ここまで -->
<template id="my-great-beautiful-button">
<style>
button {
border: none;
border-radius: 8px;
padding: 10px 20px;
background-color: var(--background-color, brown); /* (1) */
color: var(--color, azure); /* (1) */
}
</style>
<button type="submit">
<slot name="label"></slot>
</button>
</template>
<script src="./main2.js"></script>
</body>
</html>
まずは<template>要素内でCSSカスタムプロパティを受け取れるようにします。(1)でCSSの var()関数を利用して--background-colorと--colorを受け取れるようにし、未指定の場合のデフォルト値として第二引数に従来の値を入れておきました。次に、(2)でCSSとしてカスタムプロパティを定義しています。
<my-great-beautiful-button-with-template>要素に特定のクラスを付けた場合のみ適用されるスタイルなので、おそらく問題は起こりづらいはずです。最後に、(3)で<my-great-beautiful-button-with-template>要素に(2)で定義したbrightクラスを付与して、スタイルが当たるようにしました。
リスト7を実行すると、図6のようになります。
図6:テンプレート由来のカスタム要素に外部から色指定できた
カスタマイズできました。スタイル定義の大半はHTMLテンプレートやカスタム要素の中に隠蔽しつつ、カスタマイズしたい小数の項目だけを通常のCSSに定義すればよいので、従来の方法よりもよいアプローチになっています。
まとめ
Node.jsの力を借りず、ブラウザの力だけでUIの部品化をできる技術として、Web Componentsを解説していきます。今回はバックグラウンドの思想と簡単な使い方を解説しました。次回は利用できるAPIを細かく解説していきます。
