この記事は、連載「基礎からわかる Web Components 徹底解説 〜仕様から実装まで理解する〜」の第2回目になります。
カルーセルギャラリーをコンポーネント化する
Web Componentsによってスコープが実現したものの、コンポーネント化する難しさは変わりません。分割しすぎればHTML ImportsによるHTTPリクエストが必然的に増加し、パフォーマンスへ懸念が残ります。コンポーネント化した要素のどの部分を変更可能にするかなどの、汎用性についても悩ましいところです。
複数の写真をコンパクトなスペースで表示するUIとして、カルーセルギャラリーはよく見かけるUIです。これを実装するには、例えばカルーセルを構築するためのJavaSciriptの処理、及びそのCSSを既存のHTMLに追加する必要があります。
カルーセルギャラリーを構成するためのjQueryのプラグインなどは多数配布されていますが、サードパーティのリソースを導入するリスクについては前回の記事で触れたとおりです。今回は実践編として、これをコンポーネント化し、タグのみでカルーセルを実現する<carousel-panel>という要素を実際に作っていきます。
キャプチャだけではわかりませんが、画像をマウスで左右にスワイプ可能になっています。
仕様の決定と利用のイメージ
コンポーネントを作る前に、どのような機能でどのように表示されるかといった仕様を決めなければなりません。これはWeb Componentsに限らない話ですが、機能として断片化するにあたり重要なステップです。今回は次のような形で進めます。
- <carousel-panel>をカスタム要素とする
- カルーセルに表示する画像は、内部の<img>に指定
- <img>は複数配置可能だが、画像サイズは同一とする
- スワイプアクションで画像を送れるようにする
以下は配置するHTMLのイメージです。
<carousel-panel> <img src="image.png" width="320" height="320"> <img src="image.jpg" width="320" height="320"> <img src="image.gif" width="320" height="320"> </carousel-panel>
このステップでは、HTMLタグとしてのインターフェースや、どんなUIを備えているのかといった実装の部分はもちろんのこと、切り出す機能の範囲やそもそもの利用頻度といった保守性についても考慮したほうがよいでしょう。
カルーセルのベースとなる要素を作成する(Web Componentsの基本手順のおさらい)
では、前回の記事で解説した内容を踏まえて、次のような流れで作成していきます。
- カスタム要素の雛形となるHTMLを<template>に定義する
- 挙動を決定するプロトタイプオブジェクトを作成し、ライフサイクルコールバックを指定する
- カスタム要素にShadow DOMを作成し、スタイルを閉じ込める
- プロトタイプオブジェクトを
document.registerElement()
に指定し、カスタム要素を定義する
<template id='template-carousel-panel'></template><script> // 現在実行中のスクリプト要素を取得する var currentScript = document.currentScript; var ownerDocument = currentScript.ownerDocument; var CarouselPanelPrototype = Object.create(HTMLElement.prototype);
// <carousel-panel>要素が生成された時のコールバック CarouselPanelPrototype.createdCallback = function () { var template = currentScript.ownerDocument.querySelector('#template-carousel-panel'); var clone = document.importNode(template.content, true);
this.shadowRoot = this.createShadowRoot(); this.shadowRoot.appendChild(clone);
};
// <carousel-panel>要素がHTMLに追加された時のコールバック CarouselPanelPrototype.attachedCallback = function () {};
// <carousel-panel>要素の定義 window.CarouselPanel = document.registerElement('carousel-panel', { prototype: CarouselPanelPrototype }); </script>
これで<carousel-panel>要素の定義と、処理を追加していく雛形の作成が完了しました。
カルーセル要素へ挿入したコンテンツを利用する(Shadow DOMへのコンテンツ挿入)
<carousel-panel>内に<img>を配置するという仕様にしました。しかし、Shadow Rootが生成されると、ブラウザに表示されるのはShadow Rootに追加された内容になります。
このようにカスタム要素で挟まれた要素(この場合は<img>)をShadow DOM内で参照するには<content>を使います。<content>を使ってShadow DOM内に挿入ポイントを設けることで、Shadow Hostのコンテンツを参照することが可能になります。
Shadow Rootに追加するHTMLの雛形となるテンプレートを以下のように変更します。
<template id='template-carousel-panel'> <div id='container' class='carousel'> <div id='wrap' class='carousel-wrap'> <content></content> </div> </div> </template>
すると以下のように、HTMLのツリー構造に変化はありませんが、Shadow Root配下にある<content>に<img>があるように振る舞います。
<content>による挿入ポイントを複数定義し、参照をコントロールする手段も存在します。これらのShadow DOMのさらなるコンセプトについては、HTML5RocksのShadow DOM 301 – 上級者向けコンセプトと DOM APIという記事を参照してください。
カルーセル要素のスタイリング(Shadow DOMへ追加したコンテンツのスタイリング)
次に、カルーセルギャラリーを実現するために<carousel-panel>に配置される<img>や、内部でラッパーの役割を果たしている<div id=’container’ class=’carousel’>や<div id=’wrap’ class=’carousel-wrap’>にCSSを適用します。
Shadow DOM内のHTMLを装飾する場合、Shadow DOMに<style>要素を追加することで、CSSの影響範囲はShadow Root配下のHTMLに限定されます。
しかし、カスタム要素の外からShadow DOMを装飾したいケースも往々にしてあることでしょう。そのために、カスタム要素の外からカスタム要素内のHTML(つまりShadow DOM)にアクセスしたり、前述の<content>によって参照されるHTMLにアクセスするためのCSSセレクタがあります。
Shadow DOMに関連するCSSセレクタ
今回はカスタム要素である<carousel-panel>と、<content>によって参照する要素の選択するCSSセレクタを使います。
:host
Shadow Treeをホストしている要素(つまり、カスタム要素)を指すセレクタ::content
<content>による挿入ポイントをShadow DOM内部から参照するセレクタ
:host {background: red;}
というルールをShadow DOM内の<style>に追加すると、<carousel-panel>の背景色が赤になります。また、:host()
のようにホスト要素に特定のクラスが付与されているケースだけに限定することも可能です。:host(.red) {background: red;}
というルールを定義すると、<carousel-panel class=’red’>の場合のみ、背景色が赤になります。
また、<content>によって参照する要素は、Shadow Root配下に存在しません。そのため、Shadow DOM内部から参照するために::content
という擬似要素が用意されています。
今回はこの:host
で<carousel-panel>そのものと配下の.carousel
と.carousel-wrap
を、::content
で<carousel-panel>のコンテンツとなる<img>を、それぞれスタイリングしています。
<template id='template-carousel-panel'> <style> :host { display: inline-block; } :host .carousel { overflow: hidden; visibility: hidden; position: relative; } :host .carousel-wrap { overflow: hidden; position: relative; } ::content img { float: left; position: relative; } </style> <div id='container' class='carousel'> <div id='wrap' class='carousel-wrap'> <content></content> </div> </div> </template>
他にも、今回は使いませんが、Shadow DOMに関連するCSSセレクタは次のものがあります。
:host-context()
ホスト要素の祖先にあたる要素を選択する::shadow
Shadow DOMの外部からShadow DOMを参照する擬似要素/deep/
Shadow DOMが形成するスコープを貫通するコンビネータ
これらのShadow DOMに関するCSSセレクタについての詳細は、HTML5RocksのShadow DOM 201 – CSS とスタイリングが参考になるでしょう。
Shadow DOM配下の<link>要素は無効になる
Shadow DOM配下のHTMLにCSSを適用するために<style>を記述してきましたが、以下のように<link rel=’stylesheet’>ではダメなのかという疑問を抱いた人もいるかと思います。
<template> <link rel='stylesheet' href='carousel-panel.css'> <div id='container' class='carousel'> <div id='wrap' class='carousel-wrap'> <content></content> </div> </div> </template>
結論から言えば、Shadow DOM配下の<link>要素は無効です。<link>の他にも、<base>要素がShadow DOM配下では不活性でなければならないと、仕様で定められています。これについての詳細は7.1 Inert HTML Elements – Shadow DOMを見てください。
そのため、Shadow DOM配下のHTMLをスタイリングするためには、Shadow DOMに<style>要素を記述するか、Shadow DOMの外から前述の::shadow
等のセレクタを使う必要があります。
カルーセル要素のJavaScriptによる挙動の追加(ライフサイクルコールバックの利用)
attachedCallback
カルーセルを実装するには、touchstart
・touchmove
・touchend
を使ったスワイプアクションのハンドリングや、スワイプ領域を確保するための画像サイズを計算、現在何枚目の画像を表示しているか等を記憶する必要があります。
このように、カスタム要素へのイベントを定義することの負荷であったり、このケースの画像のサイズの計算のようにHTMLに挿入されるまで不確定な情報の取得、タイマーの実行のようなグローバルのリソースを必要とする処理等は、createdCallback
中で行うのではなく、HTMLに追加されたタイミングで実行されるattachedCallback
で行う方が適切と言えます。
今回のカルーセルの実装では、画像のスワイプ時の実装等をこのattachedCallback
内で実装しました。Web Components特有の実装というものはなく、DOMのAPIを使った実装が続くので、記事での解説は割愛します。なお、最後にも紹介しますが今回の<carousel-panel autoslide>はGitHubにて全てのソースコードが公開されています。attachedCallbackの実装はこちらです。
detachedCallback
window
やdocument
のようなグローバルオブジェクトのイベントを監視したり、setInterval
のようなタイマーを実行するような場合は、detachedCallback
でそれらを解放するのが望ましいでしょう。必要なくなりカスタム要素が削除されても、イベントハンドラが残り続け、ブラウザに負荷を及ぼしてしまうといったような可能性があります。
attributeChangedCallback
<script>要素のsrc
属性のように、値が更新されると要素の振る舞いは即座に変わります。カスタム要素の属性値の変更を検知して、振る舞いをコントロールしたい場合は、attributeChangedCallback
を利用します。
今回の<carousel-panel>には利用していませんが、例えば<carousel-panel autoslide>のように、「autoslide
がある場合は自動で画像送りをする」という仕様だとすると、autoslide
が追加された場合は自動で画像送りを開始し、削除された場合はそれを止めるという処理を実装しなければなりません。
attributeChangedCallback
に指定するコールバック関数はattributeName
(変更された属性), oldValue
(変更前の値), newValue
(変更後の値)を引数に取るので、様々な利用ケースに対応することができるでしょう。
カルーセル要素の完成(デモとソースコード)
ここまでの解説を踏まえて、後は実際にJavaScriptでスワイプ時の動作などをコールバックを利用して定義するだけです。実際に作成した<carousel-panel>はこちらです。JavaScriptのコードは少し長いので、GitHubのリポジトリから確認していただきたいと思います。
本記事では実装していなかったスワイプ時の処理等が、attachedCallback
内で追加されています。
また以下のサイトでは、リッチなUIをひとつのタグで利用できるようにしたものから、ブラウザAPIをHTMLから宣言的に利用可能にするものまで、実に様々なアイデアがWeb Componentsとして公開されています。コンポーネント化のアイデアに詰まったときには眺めてみるのもよいでしょう。
- Custom Elements – a web components gallery for modern web apps
- Component Kitchen – The best ingredients for your web apps
まとめ
今回はひとつの機能を実際にWeb Components化する例を解説しました。Web Componentsによってコンポーネント化をする手段は提供されましたが、コンポーネント化のアイデアや方法論については、引き続き開発者の間で議論がされていくことと思います。
次回は、Web Componentsをより柔軟に、そして強力に利用出来るようにするPolymerというライブラリについて紹介します。