はじめに
最近、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タグにしか見えないです。
これからもいろんな便利なコンポーネントを作っていきましょう!