カスタム要素を使用する

はじめに

ウェブには表現力が欠けています。どういうことか、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> 要素を拡張するには、HTMLButtonElementprototype と要素の名前 extends を継承する新しい要素を作成します。この場合、「button」は次のようになります。

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

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

例:

<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() で必要なクリーンアップ作業を行います。注: ユーザーがタブを閉じた場合など、このイベントに依存しないでください。これは、最適化フックとして使用できるイベントと考えてください。

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

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

マークアップを追加する

<x-foo> を作成して JavaScript API を指定しましたが、空になっています。レンダリングする HTML を渡しましょうか?

ここで役立つのがライフサイクル コールバックです。特に、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> のシャドールートを作成し、マークアップで埋めています。DevTools で [Shadow DOM を表示] 設定を有効にすると、展開可能な #shadow-root が表示されます。

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

これがシャドールートです。

テンプレートから要素を作成する

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 で使用される ポリフィルがあります。

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> などの他の新しいプラットフォーム プリミティブと組み合わせることで、Web Components の全体像が見えてきます。マークアップを再び魅力的に

ウェブ コンポーネントの使用を検討している場合は、Polymer をご確認ください。十分な機能が備わっています。