アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

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は各ブラウザにはまだ実装されていないため、SafariIEなどで対応するには、ポリフィルを使わないといけませんが、近い将来には必要なくなると思います。)

このチュートリアルのコードはここでダウンロードできます。

それでは始めましょう!


まずは、新しいタグを登録しないといけません。カスタムエレメントを登録するには新しくできた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"> &#x2605;</span>
</template>


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

シャドウDOMとは?


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

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

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

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



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


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

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



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


これに加えて、attachedCallbackdetachedCallbackという関数もあります。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.setAttributeelement.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タグにしか見えないです。

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

参考情報