Asial Blog

Recruit! Asialで一緒に働きませんか?

Web Componentsでお気に入りボタンを作ってみましょう!

カテゴリ :
バックエンド(プログラミング)
タグ :
JavaScript
HTML5
Web Components
Custom Elements

はじめに



最近、Onsen UIでWeb Components(ウェブ・コンポーネント)を使い始めました。このAPIで、ウェブ開発者は新しいHTMLのタグを楽に作れるようになります。Web Componentsを学びながら、下のようなシンプルな「お気に入りボタン」を作ってみましょう。






今回作成する「お気に入りボタン」は、カスタムエレメントなので作成後は簡単に利用できます。

  1. <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()という関数を使います。引数としては、「タグの名前」と「オプションオブジェクト」を入れます。

  1. window.FavoriteStarElement = document.registerElement('favorite-star', {
  2.   prototype: proto
  3. });

prototypeというオプションでは、エレメントの動作を定義するのでとても大事です。

HTMLのタグを作っているので、プロトタイプはHTMLElement.prototypeををコピーして継承しましょう。

  1. var proto = Object.create(HTMLElement.prototype);

<template>タグ


<template>タグの中には、カスタムエレメントで表示するお気に入りボタン(ここではUTF-8の★が入っている<span>タグ)とそれに適用するスタイルシートを入れます。

  1. <template>
  2.   <style>
  3.     ...
  4.   </style>
  5.   <span class="favorite-star-character">&#x2605;</span>
  6. </template>

この記事ではスタイルシートについては触れませんが、興味がある方はこちらを参照してください。

シャドウDOMとは?


Web Componentsのスペックの一番紛らわしい部分は、多分シャドウDOMだと思います。シャドウDOMは標準DOMに付いている特別なサブツリーです。シャドウDOMに入っているスタイルシートはそのツリーのエレメントのみに影響を与えます。

これはカスタムエレメントを作る時には非常に便利です。エレメントの中に定義されているスタイルシートは副作用があるかどうか心配する必要がなくなりますね。

新しいシャドウDOMのサブツリーを作るにはdocument.createShadowRoot()を使います。

ChromeはもうポリフィルがなくてもシャドウDOMを対応していますので、DevToolsのインスペクターでシャドウDOMはこのように見えます。



Web Componentsのライフサイクルコールバック


カスタムエレメントのライフライクルには、特別なコールバックが四つあります。これらのコールバックで、エレメントの動作を制御することができます。コールバックはエレメントのプロトタイプオブジェクトに付けます。

一番大事なのはcreatedCallbackです。エレメントが生まれるときに起動される関数です。これを使うだけで、かなり複雑なカスタムエレメントがつくれます。

  1. proto.createdCallback = function() {
  2.   console.log('ハロー・ワールド');
  3. };

これに加えて、attachedCallbackdetachedCallbackという関数もあります。attachedCallbackはエレメントがDOMに付くときに呼び出されます。detachedCallbackは逆にDOMから外されたときに呼びさだれます。

この二つの関数には、メモリーリークを起こさないようにイベントリスナーを登録したり、イベントリスナーを削除したりするととても便利です。

attributeChangedCallbackは、エレメント属性は変わった時に呼び出されます。

ここでは、まずお気に入りボタンのcreatedCallbackを作りましょう。

  1. // documentオブジェクトを取ります。
  2. var currentScript = document._currentScript || document.currentScript,
  3.   doc = currentScript.ownerDocument,
  4.  
  5. // プロトタイプオブジェクトを作成します。
  6. var proto = Object.create(HTMLElement.prototype);
  7.  
  8. // この関数はエレメントが生まれたときに呼び出されます。
  9. proto.createdCallback = function() {
  10.   // <template>タグのコンテンツを取ります。
  11.   var template = doc.querySelector('template'),
  12.       clone = document.importNode(template.content, true);
  13.  
  14.   // シャドウDOMのサブツリーを作成して、コンテンツを
  15.   // 付けます。
  16.   this.shadowRoot = this.createShadowRoot();
  17.   this.shadowRoot.appendChild(clone);
  18.  
  19.   // 星の<span>タグを探します。
  20.   this.element = this.shadowRoot.querySelector('.favorite-star-character');
  21.  
  22.   // イベントリスナー
  23.   this.boundOnClick = this.onClick.bind(this);
  24.   this.boundOnMouseover = this.onMouseover.bind(this);
  25.   this.boundOnMouseout = this.onMouseout.bind(this);
  26.  
  27.   // 生まれたときにactiveの属性が付いてたら
  28.   // <span>タグにactive属性を付けます。
  29.   if (this.hasAttribute('active')) {
  30.     this.element.setAttribute('active', '');
  31.   }
  32. };

ユーザーがクリックした場合とエレメントの上にホバーした場合の動作もつけないといけないので、
attachedCallbackでイベントリスナーを登録します。

  1. proto.attachedCallback = function() {
  2.   var el = this.element;
  3.  
  4.   // イベントリスナーを登録します。
  5.   el.addEventListener('click', this.boundOnClick);
  6.   el.addEventListener('mouseout', this.boundOnMouseout);
  7.   el.addEventListener('mouseover', this.boundOnMouseover);
  8. }
  9.  
  10. proto.detachedCallback = function() {
  11.   var el = this.element;
  12.  
  13.   // イベントリスナーを削除します。
  14.   el.removeEventListener('click', this.boundOnClick);
  15.   el.removeEventListener('mouseout', this.boundOnMouseout);
  16.   el.removeEventListener('mouseover', this.boundOnMouseover);
  17. }
  18.  
  19. proto.toggle = function() {
  20.   if (this.hasAttribute('active')) {
  21.     this.removeAttribute('active');
  22.   }
  23.   else {
  24.     this.setAttribute('active', '');
  25.   }
  26. }
  27.  
  28. proto.onClick = function() {
  29.   this.toggle();
  30. }
  31.  
  32. // この場合は:hoverクラスを使えないので、hoverというクラスを
  33. // 作成します。:hoverを使ったら、金色の星をクリックしたら動作は
  34. // 間違っています(灰色になりません)。
  35. proto.onMouseover = function() {
  36.   var el = this.element;
  37.   if (!el.hasAttribute('active')) {
  38.     el.setAttribute('hover', '');
  39.   }
  40. }
  41.  
  42. proto.onMouseout = function() {
  43.   var el = this.element;
  44.   el.removeAttribute('hover');
  45. }

開発者の中には、element.setAttributeelement.removeAttributeで直接制御する可能性もありますので、それに対応するattributeChangedCallbackも作ります。

  1. proto.attributeChangedCallback = function(attr) {
  2.   if (attr === 'active') {
  3.     var el = this.element;
  4.     if (this.hasAttribute('active')) {
  5.       el.setAttribute('active', '');
  6.     }
  7.     else {
  8.       el.removeAttribute('active');
  9.       el.removeAttribute('hover');
  10.     }
  11.   }
  12. }

出来上がりです!

おわりに


まだこのAPIを対応していないブラウザはあるけど、ポリフィルを入れることで全てのブラウザに対応できるため、Web Componentsは既に実践に使える技術になったと感じますね。

私はちょっとしか触ってないけど、こんな風に新しいタグを作るのはとても便利なことだと思います。中身はどんなに複雑でも、外から見ればとても使いやすいHTMLタグにしか見えないです。

これからもいろんな便利なコンポーネントを作っていきましょう!

参考情報