カスタム要素を使用する

はじめに

ウェブには表現力が欠けています。具体的には、Gmail などの「モダン」なウェブアプリをご覧ください。

Gmail

<div> スープは現代風にない。しかし、これはウェブアプリの構築方法です。悲しい。プラットフォームにもっと求めるべきではないでしょうか?

セクシーなマークアップ。定番にしましょう

HTML はドキュメントを構造化するための優れたツールですが、その語彙は HTML 標準で定義されている要素に限定されます。

Gmail のマークアップがひどくなければ、もしきれいだったら?

<hangout-module>
    <hangout-chat from="Paul, Addy">
    <hangout-discussion>
        <hangout-message from="Paul" profile="profile.png"
            profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.
        <p>Heard of it?
        </hangout-message>
    </hangout-discussion>
    </hangout-chat>
    <hangout-chat>...</hangout-chat>
</hangout-module>

このアプリもまったく理にかなっています。有意義わかりやすく、何よりもメンテナンス可能です。その宣言型バックボーンを調べるだけで 何をするのかわかるでしょう

スタートガイド

カスタム要素を使用すると、ウェブ デベロッパーは新しいタイプの HTML 要素を定義できます。この仕様は、ウェブ コンポーネントの傘下にある新しい API プリミティブの 1 つですが、最も重要な API プリミティブである可能性もあります。カスタム要素によってロックが解除された機能がなければ、Web コンポーネントは存在しません。

  1. 新しい HTML/DOM 要素を定義する
  2. 他の要素から拡張する要素を作成する
  3. カスタム機能を論理的に 1 つのタグにまとめる
  4. 既存の DOM 要素の API を拡張する

新しい要素の登録

カスタム要素は document.registerElement() を使用して作成します。

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

document.registerElement() の最初の引数は、要素のタグ名です。名前にはダッシュ(-)を含める必要があります。たとえば、<x-tags><my-element><my-awesome-app> はすべて有効な名前ですが、<tabs><foo_bar> は無効です。この制限により、パーサーはカスタム要素と通常の要素を区別できるだけでなく、新しいタグが HTML に追加されたときにも上位互換性を確保できます。

2 番目の引数は、要素の prototype を記述する(省略可能な)オブジェクトです。要素にカスタム機能(パブリック プロパティやメソッドなど)を追加する場所です。詳しくは後ほど説明します

デフォルトでは、カスタム要素は HTMLElement から継承します。したがって、上記の例は次の式と同等です。

var XFoo = document.registerElement('x-foo', {
    prototype: Object.create(HTMLElement.prototype)
});

document.registerElement('x-foo') を呼び出すと、ブラウザに新しい要素が通知され、<x-foo> のインスタンスの作成に使用できるコンストラクタが返されます。コンストラクタを使用しない場合は、他の要素をインスタンス化する手法を使用することもできます。

要素を拡張する

カスタム要素を使用すると、既存の(ネイティブ)HTML 要素やその他のカスタム要素を拡張できます。要素を拡張するには、継承元の要素の名前と prototyperegisterElement() に渡す必要があります。

ネイティブ要素の拡張

たとえば、通常のジョー <button> に不満があるとします。この機能をさらに強化して「メガボタン」にしたいと考えています。<button> 要素を拡張するには、HTMLButtonElementprototypeextends 要素の名前を継承する新しい要素を作成します。この例では「button」が次のように機能します。

var MegaButton = document.registerElement('mega-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

ネイティブ要素から継承するカスタム要素は、型拡張カスタム要素と呼ばれます。「要素 X は Y である」という意味で、特別なバージョンの HTMLElement を継承します。

例:

<button is="mega-button">

カスタム要素の拡張

<x-foo> カスタム要素を拡張する <x-foo-extended> 要素を作成するには、そのプロトタイプを継承し、継承元のタグを指定します。

var XFooProto = Object.create(HTMLElement.prototype);
...

var XFooExtended = document.registerElement('x-foo-extended', {
    prototype: XFooProto,
    extends: 'x-foo'
});

要素のプロトタイプを作成する方法について詳しくは、以下のJS プロパティとメソッドを追加するをご覧ください。

要素のアップグレード方法

HTML パーサーが標準以外のタグでエラーをスローしないのはなぜでしょうか?たとえば、ページで <randomtag> を宣言しても、ブラウザはこれを問題なく受け入れます。HTML 仕様によると、

<randomtag> さん、ごめんなさい!標準ではなく、HTMLUnknownElement から継承しています。

カスタム要素についても同様です。有効なカスタム要素名を持つ要素は HTMLElement を継承します。この事実を確認するには、Console: Ctrl + Shift + J(Mac の場合は Cmd + Opt + J)を起動し、次のコード行を貼り付けます。true が返されます。

// "tabs" is not a valid custom element name
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype

// "x-tabs" is a valid custom element name
document.createElement('x-tabs').__proto__ == HTMLElement.prototype

未解決の要素

カスタム要素は document.registerElement() を使用してスクリプトで登録されるため、ブラウザによって定義が登録される前にカスタム要素を宣言または作成できます。たとえば、ページで <x-tabs> を宣言しても、ずっと後で document.registerElement('x-tabs') を呼び出すことができます。

定義にアップグレードされる前の要素は、未解決要素と呼ばれます。有効なカスタム要素名を持つものの、登録されていない HTML 要素です。

次の表は、整理するのに役立ちます。

名前 継承元
未解決の要素 HTMLElement <x-tabs><my-element>
不明な要素 HTMLUnknownElement <tabs><foo_bar>

要素のインスタンス化

要素を作成する一般的な手法は、カスタム要素にも適用されます。 他の標準要素と同様に、HTML で宣言することも、JavaScript を使用して DOM で作成することもできます。

カスタムタグのインスタンス化

宣言します。

<x-foo></x-foo>

JS で DOM を作成します。

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
    alert('Thanks!');
});

new 演算子を使用します。

var xFoo = new XFoo();
document.body.appendChild(xFoo);

型拡張要素のインスタンス化

タイプ拡張スタイルのカスタム要素をインスタンス化すると、カスタムタグに非常に近くなります。

次のように宣言します。

<!-- <button> "is a" mega button -->
<button is="mega-button">

JS で DOM を作成します。

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

ご覧のとおり、is="" 属性を 2 番目のパラメータとして受け取る document.createElement() のオーバーロード バージョンが追加されています。

new 演算子を使用します。

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

ここまでは、document.registerElement() を使用して新しいタグについてブラウザに通知する方法について学びました。しかし、この方法ではできることは多くありません。プロパティとメソッドを追加しましょう。

JS のプロパティとメソッドの追加

カスタム要素の強力な点は、要素定義でプロパティとメソッドを定義することで、カスタマイズされた機能を要素にバンドルできることです。これは、要素の公開 API を作成する方法と考えてください。

完全な例を次に示します。

var XFooProto = Object.create(HTMLElement.prototype);

// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
    alert('foo() called');
};

// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');

// 5. Add it to the page.
document.body.appendChild(xfoo);

もちろん、prototype を構築する方法は数え切れないほどあります。このようなプロトタイプを作成するのが面倒な場合は、同じことをより簡潔に記述したバージョンを以下に示します。

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function () {
        return 5;
      }
    },
    foo: {
      value: function () {
        alert('foo() called');
      }
    }
  })
});

最初の形式では、ES5 の Object.defineProperty を使用できます。2 つ目のメソッドでは get/set を使用できます。

ライフサイクル コールバック メソッド

要素は、要素の存続期間における重要なタイミングをタップするための特別なメソッドを定義できます。これらのメソッドは、適切にライフサイクル コールバックという名前が付けられています。それぞれに固有の名前と目的があります。

コールバック名 呼び出されるタイミング
createdCallback 要素のインスタンスが
attachedCallback ドキュメントにインスタンスが挿入された
detachedCallback インスタンスがドキュメントから削除されました
attributeChangedCallback(attrName, oldVal, newVal) 属性が追加、削除、更新された

例: <x-foo>createdCallback()attachedCallback() を定義する:

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

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

ライフサイクル コールバックはすべて省略可能ですが、必要に応じて定義します。たとえば、要素が十分に複雑で、createdCallback() で IndexedDB への接続を開くとします。DOM から削除される前に、detachedCallback() で必要なクリーンアップ作業を行います。注: ユーザーがタブを閉じた場合など、このイベントに依存しないでください。これは、最適化フックとして使用できるイベントです。

ライフサイクル コールバックのもう 1 つのユースケースは、要素にデフォルトのイベント リスナーを設定することです。

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};

マークアップの追加

<x-foo> を作成して JavaScript API を指定しましたが、空になっています。レンダリングさせます

ここで役立つのがライフサイクル コールバックです。特に、createdCallback() を使用して要素になんらかのデフォルトの HTML を付与できます。

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = "**I'm an x-foo-with-markup!**";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

このタグをインスタンス化して DevTools で調べる(右クリックして [要素を検証] を選択)すると、次のように表示されます。

▾<x-foo-with-markup>
  **I'm an x-foo-with-markup!**
</x-foo-with-markup>

Shadow DOM の内部をカプセル化する

Shadow DOM 自体が、コンテンツをカプセル化するための強力なツールです。カスタム要素と組み合わせて使用すると、魔法のような効果が得られます。

Shadow DOM を使用すると、カスタム要素に次のメリットがもたらされます。

  1. ユーザーの直感を隠して、残虐な実装の詳細からユーザーを守る方法。
  2. スタイルのカプセル化を無料で利用できます。

Shadow DOM から要素を作成することは、基本的なマークアップをレンダリングする要素を作成することに似ています。違いは createdCallback() にあります。

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    // 1. Attach a shadow root on the element.
    var shadow = this.createShadowRoot();

    // 2. Fill it with markup goodness.
    shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

要素の .innerHTML を設定する代わりに、<x-foo-shadowdom> の Shadow Root を作成して、マークアップを入れました。DevTools で [Shadow DOM を表示] 設定を有効にすると、展開可能な #shadow-root が表示されます。

<x-foo-shadowdom>
  ▾#shadow-root
    **I'm in the element's Shadow DOM!**
</x-foo-shadowdom>

これが Shadow ルートです。

テンプレートからの要素の作成

HTML テンプレートは、カスタム要素の世界に適した新しい API プリミティブです。

例: <template> と Shadow DOM から作成された要素を登録する

<template id="sdtemplate">
  <style>
    p { color: orange; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<script>
  var proto = Object.create(HTMLElement.prototype, {
    createdCallback: {
      value: function() {
        var t = document.querySelector('#sdtemplate');
        var clone = document.importNode(t.content, true);
        this.createShadowRoot().appendChild(clone);
      }
    }
  });
  document.registerElement('x-foo-from-template', {prototype: proto});
</script>

<template id="sdtemplate">
  <style>:host p { color: orange; }</style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

この数行のコードには、たくさんの効果があります。状況を整理してみましょう。

  1. HTML に新しい要素を登録しました: <x-foo-from-template>
  2. 要素の DOM が <template> から作成された
  3. 要素の恐ろしいディテールは Shadow DOM によって隠されている
  4. Shadow DOM は要素のスタイルをカプセル化します(例: p {color: orange;} はページ全体をオレンジ色にしません)。

すごい!

カスタム要素のスタイル設定

他の HTML タグと同様に、カスタムタグではセレクタを使用してスタイルを設定できます。

<style>
  app-panel {
    display: flex;
  }
  [is="x-item"] {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  [is="x-item"]:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > [is="x-item"] {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-panel>
    <li is="x-item">Do</li>
    <li is="x-item">Re</li>
    <li is="x-item">Mi</li>
</app-panel>

Shadow DOM を使用する要素のスタイル設定

Shadow DOM を導入すると、その穴はずっと深くなります。Shadow DOM を使用するカスタム要素は、その大きなメリットを継承します。

Shadow DOM によって、要素にスタイルのカプセル化が組み込まれます。シャドールートで定義されたスタイルは、ホストから漏洩せず、ページから漏洩することもありません。カスタム要素の場合、要素自体がホストになります。スタイルのカプセル化のプロパティを使用すると、カスタム要素でデフォルトのスタイルを定義することもできます。

Shadow DOM のスタイル設定は広範なトピックです。詳しくは、以下の記事をご参照ください。

:unresolved を使用した FOUC の防止

FOUC を軽減するため、カスタム要素は新しい CSS 疑似クラス :unresolved を指定します。ブラウザが createdCallback() を呼び出すまで、未解決の要素をターゲットにします(ライフサイクル メソッドを参照)。解決されると、その要素は未解決要素ではなくなります。アップグレード プロセスが完了し、要素が定義に変換されました。

: 「x-foo」タグが登録されたときにフェードインします。

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

:unresolved は、HTMLUnknownElement から継承する要素ではなく、未解決の要素にのみ適用されます(要素のアップグレード方法をご覧ください)。

<style>
  /* apply a dashed border to all unresolved elements */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* x-panel's that are unresolved are red */
  x-panel:unresolved {
    color: red;
  }
  /* once the definition of x-panel is registered, it becomes green */
  x-panel {
    color: green;
    display: block;
    padding: 5px;
    display: block;
  }
</style>

<panel>
    I'm black because :unresolved doesn't apply to "panel".
    It's not a valid custom element name.
</panel>

<x-panel>I'm red because I match x-panel:unresolved.</x-panel>

履歴とブラウザのサポート

特徴検出

特徴検出は、document.registerElement() が存在するかどうかを確認するだけです。

function supportsCustomElements() {
    return 'registerElement' in document;
}

if (supportsCustomElements()) {
    // Good to go!
} else {
    // Use other libraries to create components.
}

ブラウザ サポート

document.registerElement() はまず、Chrome 27 と Firefox で約 23 で旗の後ろにいます。しかし、それ以降、仕様はかなり進化しました。Chrome 31 は、更新された仕様を真にサポートする最初のバージョンです。

ブラウザのサポートが十分に進むまで、Google の Polymer と Mozilla の X-Tag で使用される polyfill が提供されています。

HTMLElementElement はどうなったのですか?

標準化作業に追随している方は、かつて <element> があったことをご存じでしょう。それがはちの膝だった。これを使用して、新しい要素を宣言的に登録できます。

<element name="my-element">
    ...
</element>

残念ながら、アップグレード プロセス、コーナーケース、ハルマゲドンのようなシナリオにはタイミングの問題が多すぎてすべて解決できませんでした。「<element>」は棚に置く必要がありました。2013 年 8 月、Dimitri Glazkov は public-webapps に投稿し、少なくとも現時点では削除することを発表しました。

Polymer は <polymer-element> を使用して要素登録の宣言型を実装していることに注意してください。このアプリでは、ここでは、document.registerElement('polymer-element') と、テンプレートから要素を作成するで説明した手法を使用します。

まとめ

カスタム要素は、HTML の語彙を拡張し、新しい手法を教え、ウェブ プラットフォームのワームホールを飛び越えるツールを提供します。これらを Shadow DOM や <template> などの他の新しいプラットフォーム プリミティブと組み合わせることで、ウェブ コンポーネントの全体像が実現します。マークアップでセクシーな美しさを取り戻すことができます。

ウェブ コンポーネントの使用を検討している場合は、Polymer をご確認ください。十分な機能を備えており、着実に前進することができます。