Web Componentsでお気に入りボタンを作ってみましょう!
はじめに
最近、Onsen UIでWeb Components(ウェブ・コンポーネント)を使い始めました。このAPIで、ウェブ開発者は新しいHTMLのタグを楽に作れるようになります。Web Componentsを学びながら、下のようなシンプルな「お気に入りボタン」を 作ってみましょう。
今回作成する「お気に入りボタン」は、カスタムエレメントなので作成後は簡単に利用できます。
<favorite-star></favorite-star>
クリックしたら
Onsen UIを作り始めた当時は、Web Componentsというものがないため、Onsen UIのカスタムエレメントはAngularのディレクティブで実装していましたが、今後はOnsen UIのコンポーネントをAngularJSだけではなく、どんなフレームワークでも利用可能にし、jQueryやReact.jsの開発者でも、Onsen UIでハイブリッドアプリを楽しく作れるようしたいと考えています。そのため、Onsen UIの中心部をWeb Componentsで実装することにしました。また、AngularJSのサポートもAngularJSのラッパーで継続して対応します。(Web Componentsは各ブラウザにはまだ実装されていないため、SafariやIEなどで対応するには、ポリフィルを使わないといけませんが、近い将来には必要なくなると思います。)
このチュートリアルのコードはここでダウンロードできます。
それでは始めましょう!
まずは、新しいタグを登録しないといけません。カスタムエレメントを登録するには新しくできたdocument.registerElement()
という関数を使います。引数としては、「タグの名前」と「オプションオブジェクト」を入れます。
window.FavoriteStarElement = document.registerElement('favorite-star', {
prototype: proto
});
prototype
というオプションでは、エレメントの動作を定義するのでとても大事です。
HTMLのタグを作っているので、プロトタイプはHTMLElement.prototype
ををコピーして継承しましょう。
var proto = Object.create(HTMLElement.prototype);
<template>タグ
<template>タグの中には、カスタムエレメントで表示するお気に入りボタン(ここではUTF-8の★が入っている<span>タグ)とそれに適用するスタイルシートを入れます。
<template>
<style>
...
</style>
<span class="favorite-star-character"> ★</span>
</template>
この記事ではスタイルシートについては触れませんが、興味がある方はこちらを参照してください。
シャドウDOMとは?
Web Componentsのスペックの一番紛らわしい部分は、多分シャドウDOMだと思います。シャドウDOMは標準DOMに付いている特別なサブツリーです。シャドウDOMに入っているスタイルシートはそのツリーのエレメントのみに影響を与えます。
これはカスタムエレメントを作る時には非常に便利です。 エレメントの中に定義されているスタイルシートは副作用があるかどうか心配する必要がなくなりますね。
新しいシャドウDOMのサブツリーを作るにはdocument.createShadowRoot()
を使います。
ChromeはもうポリフィルがなくてもシャドウDOMを対応していますので、DevToolsのインスペクターでシャドウDOMはこのように見えます。
Web Componentsのライフサイクルコールバック
カスタムエレメントのライフライクルには、特別なコールバックが四つあります。これらのコールバックで、エレメントの動作を制御することができます。コールバックはエレメントのプロトタイプオブジェクトに付けます。
一番大事なのはcreatedCallback
です。エレメントが生まれるときに起動される関数です。これを使うだけで、かなり複雑なカスタムエレメントがつくれます。
proto.createdCallback = function() {
console.log('ハロー・ワールド');
};
これに加えて、attachedCallback
とdetachedCallback
という関数もあります。attachedCallback
はエレメントがDOMに付くときに呼び出されます。detachedCallback
は逆にDOMから外されたときに呼びさだれます。
この二つの関数には、メモリーリークを起こさないようにイベントリスナーを登録したり、イベントリスナーを削除したりするととても便利です。
attributeChangedCallback
は、エレメント属性は変わった時に呼び出されます。
ここでは、まずお気に入りボタンのcreatedCallback
を作りましょう。
// documentオブジェクトを取ります。
var currentScript = document._currentScript || document.currentScript,
doc = currentScript.ownerDocument,
// プロトタイプオブジェクトを作成します。
var proto = Object.create(HTMLElement.prototype);
// この関数はエレメントが生まれたときに呼び出されます。
proto.createdCallback = function() {
// <template>タグのコンテンツを取ります。
var template = doc.querySelector('template'),
clone = document.importNode(template.content, true);
// シャドウDOMのサブツリーを作成して、コンテンツを
// 付けます。
this.shadowRoot = this.createShadowRoot();
this.shadowRoot.appendChild(clone);
// 星の<span>タグを探します。
this.element = this.shadowRoot.querySelector('.favorite-star-character');
// イベントリスナー
this.boundOnClick = this.onClick.bind(this);
this.boundOnMouseover = this.onMouseover.bind(this);
this.boundOnMouseout = this.onMouseout.bind(this);
// 生まれたときにactiveの属性が付いてたら
// <span>タグにactive属性を付けます。
if (this.hasAttribute('active')) {
this.element.setAttribute('active', '');
}
};
ユーザーがクリックした場合とエレメントの上にホバーした場合の動作もつけないといけないので、
attachedCallback
でイベントリスナーを登録します。
proto.attachedCallback = function() {
var el = this.element;
// イベントリスナーを登録します。
el.addEventListener('click', this.boundOnClick);
el.addEventListener('mouseout', this.boundOnMouseout);
el.addEventListener('mouseover', this.boundOnMouseover);
}
proto.detachedCallback = function() {
var el = this.element;
// イベントリスナーを削除します。
el.removeEventListener('click', this.boundOnClick);
el.removeEventListener('mouseout', this.boundOnMouseout);
el.removeEventListener('mouseover', this.boundOnMouseover);
}
proto.toggle = function() {
if (this.hasAttribute('active')) {
this.removeAttribute('active');
}
else {
this.setAttribute('active', '');
}
}
proto.onClick = function() {
this.toggle();
}
// この場合は:hoverクラスを使えないので、hoverというクラスを
// 作成します。:hoverを使ったら、金色の星をクリックしたら動作は
// 間違っています(灰色になりません)。
proto.onMouseover = function() {
var el = this.element;
if (!el.hasAttribute('active')) {
el.setAttribute('hover', '');
}
}
proto.onMouseout = function() {
var el = this.element;
el.removeAttribute('hover');
}
開発者の中には、element.setAttribute
やelement.removeAttribute
で直接制御する可能性もありますので、それに対応するattributeChangedCallback
も作ります。
proto.attributeChangedCallback = function(attr) {
if (attr === 'active') {
var el = this.element;
if (this.hasAttribute('active')) {
el.setAttribute('active', '');
}
else {
el.removeAttribute('active');
el.removeAttribute('hover');
}
}
}
出来上がりです!
おわりに
まだこのAPIを対応していないブラウザはあるけど、ポリフィルを入れることで全てのブラウザに対応できるため、Web Componentsは既に実践に使える技術になったと感じますね。
私はちょっとしか触ってないけど、こんな風に新しいタグを作るのはとても便利なことだと思います。中身はどんなに複雑でも、外から見ればとても使いやすいHTMLタグにしか見えないです。
これからもいろんな便利なコンポーネントを作っていきましょう!