この記事では、まだ完全に標準化されていない API について述べています。実験的な API を利用する際は、十分ご注意下さい。
はじめに
ウェブには表現力がもっと必要です。この意味を知るには、Gmail のような「モダンな」ウェブアプリを見てみればわかるでしょう:
<div> だらけのモダンなウェブアプリ
<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>
<p>Heard of it?</p>
</hangout-message>
</hangout-discussion>
</hangout-chat>
<hangout-chat>...</hangout-chat>
</hangout-module>
素晴らしいでしょう?このアプリはもちろん意味を持っています。意味を持っているし、理解しやすい はずです。 そして何よりも、メンテナンスが容易 なのです。将来的には、宣言的な仕組みを調査するだけで、何をしているか理解できるようになるでしょう。
Getting started
Custom Elements によって、開発者は 新しい型の HTML 要素を作る ことができるようになります。仕様は Web Components が提唱する一連の API 仕様のひとつですが、最も重要なものと言えます。Web Components は Custom Elements で利用可能になる機能なしには存在し得ないのです:
- 新しい HTML/DOM 要素を定義する
- 他の要素を継承した要素を作り出す
- 論理的にひとつのタグにカスタマイズされた機能をバンドルする
- 既存の DOM 要素の API を拡張する
新しい要素を登録する
Custom Element は document.registerElement() を使って作ることができます:
var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());
最初の引数は要素のタグ名です。名前には ダッシュ (-) を含む必要があります 。例えば <x-tags>、<my-element>、<my-awesome-app> はすべて許可された名前ですが、<tabs> や <foo_bar> は許可されません。この制限により、パーサーは通常の要素と Custom Element を区別することができ、将来的に HTML に新しい要素が追加された際の互換性も保証してくれます。
ふたつめの引数はオプションとなるオブジェクトで、要素の prototype を記述します。これはあなたの要素にカスタム機能 (プロパティやメソッド) を追加するための場所でもあります。詳しくは 後ほど 触れます。
デフォルトでは、Custom Element は HTMLElement を継承します。そのため、先ほどの例は下記と同等になります:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
document.registerElement('x-foo') を呼び出すと、ブラウザは新しい要素を認識し、<x-foo> のインスタンスを作ることができるコンストラクタを返します。
コンストラクタを使いたくなければ、他の方法 を使って要素のインスタンス化することもできます。
グローバルな window オブジェクトを汚染したくない場合は、名前空間 (var myapp = {}; myapp.XFoo = document.registerElement('x-foo');) を使って下さい。
ネイティブ要素を拡張する
ごく普通の <button> に不満があったとしましょう。そしてそれを "Mega Button" に拡張したいとします。<button> 要素を拡張するには、HTMLButtonElement の prototype を継承した新しい要素を作ります:
var MegaButton = document.registerElement('mega-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
要素 A を拡張した 要素 B を作るには、要素 A は 要素 B の prototype を継承しなければなりません。
こういった Custom Elements は 型拡張 Custom Element と呼ばれ、"要素 X は Y である" と宣言するため、HTMLElement の特別版を継承します。
例:
<button is="mega-button">
どのようにして要素はアップグレードされるのか
HTML パーサーがなぜ非標準のタグに対して例外を投げないか不思議に思ったことはありませんか? <randomtag> というタグをページに埋め込んでも、特に文句は言われません。HTML 仕様 によると:
この仕様で定義されていない HTML 要素に対しては HTMLUnknownElement インターフェースを使わなければならない。
HTML 仕様
ごめん、<randomtag>!君は標準じゃないので HTMLUnknownElement から継承されるんだ。
Custom Elements について同じことは言えません。 正当な名前が与えられた Custom Elements は HTMLElement から継承されます。 Console を使うことでこれを確認できます:Ctrl+Shift+J (Mac の場合 Cmd+Opt+J) をし、下記のコードをペーストすると、true を返します。
// "tabs" は正当な Custom Element 名ではありません
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype
// "x-tabs" は正当な Custom Element 名です
document.createElement('x-tabs').__proto__ == HTMLElement.prototype
document.registerElement() をサポートしないブラウザでは、<x-tabs> は HTMLUnknownElement を返します。
未解決の要素
Custom Elements はスクリプト document.registerElement() によって登録されるため、ブラウザによって 定義が登録される前に宣言したり、生成したりすることができます。 例えば <x-tabs> を宣言したとしても、document.registerElement('x-tabs') を呼び出すのはずっと後でも構わないのです。
要素は定義の通りにアップグレードされるまでの間、 未解決の要素 と呼ばれます。これらの HTML 要素は正当な Custom Element 名を持ちますが、まだ登録されていないものです。
一覧表にまとめてみましょう:
| 名前 | 継承元 | 例 |
|---|---|---|
| 未解決要素 | HTMLElement | <x-tabs>, <my-element>, <my-awesome-app> |
| 未知の要素 | HTMLUnknownElement | <tabs>, <foo_bar>
|
未解決の要素はどっちつかずの状態と思って下さい。ブラウザが後からアップグレードする候補です。「新しい要素になるための素質は十分ある。定義さえくれればアップグレードすると約束しよう。」とブラウザが言っているのです。
要素をインスタンス化する
要素を作る際のテクニックがそのまま Custom Element にも当てはまります。他の標準要素と同様、HTML で宣言したり、JavaScript を使って DOM として生成することもできます。
カスタムタグをインスタンス化する
宣言する:
<x-foo></x-foo>
JavaScript で DOM を生成する:
var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
alert('Thanks!');
});
new オペレーター を使う:
var xFoo = new XFoo();
document.body.appendChild(xFoo);
型拡張要素をインスタンス化する
型拡張型の Custom Elements のインスタンス化は 通常の カスタムタグの場合にそっくりです。
宣言する:
<!-- <button> "is a" mega button -->
<button is="mega-button">
JavaScript で DOM を生成する:
var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true
見ての通り、is="" を第二引数に取る document.createElement() のオーバーロード版があります。
new オペレーター を使う:
var megaButton = new MegaButton();
document.body.appendChild(megaButton);
ここまでブラウザに新しいタグを教えるための document.registerElement() について説明してきましたが、まだできることはそれほど多くありません。プロパティとメソッドの追加方法についても見て行きましょう。
JavaScript プロパティとメソッドの追加
Custom Elements のパワフルなところは、要素にプロパティやメソッドを定義することで、一連の機能を付加することができるところです。あなたの要素に公開 API が追加できると考えて下さい。
例を挙げてみましょう:
var XFooProto = Object.create(HTMLElement.prototype);
// 1.x-foo に foo() メソッドを追加
XFooProto.foo = function() {
alert('foo() called');
};
// 2. 読み込み専用のプロパティ "bar" を定義
Object.defineProperty(XFooProto, "bar", {value: 5});
// 3. x-foo の定義を登録
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
// 4. x-foo をインスタンス化
var xfoo = document.createElement('x-foo');
// 5. ページに追加
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 を使っています。二番目のやり方は get/set を使っています。
ライフサイクルコールバックメソッド
Custom Elements では絶妙なタイミングで特別なメソッドを呼び出すことができます。これらのメソッドは ライフサイクルコールバック と呼ばれ、それぞれに特別な名前と目的があります:
| コールバック名 | 呼び出されるタイミング |
|---|---|
| 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 = "<b>I'm an x-foo-with-markup!</b>";
};
var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});
このタグをインスタンス化し、DevTools (右クリックし、Inspect Element を選択) でインスペクトしてみると下記のようなものが見えるはずです:
▾<x-foo-with-markup>
<b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>
内部を Shadow DOM でカプセル化する
Shadow DOM はそれ自体、カプセル化を行うためのパワフルなツールです。Custom Element と組み合わせて使うことで、魔法のような力を発揮することができます!
Shadow DOM と合わせて使うことで Custom Elements は:
- 内部構造を隠蔽し、複雑な実装を見せなくすることができます。
- Style のカプセル化 を手軽に実現できます。
Shadow DOM から要素を作り出すのは、基本的なマークアップをレンダリングするのに似ています。違いは createdCallback() にあります:
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
// 1. shadow root を要素に追加
var shadow = this.createShadowRoot();
// 2. それをマークアップで埋める
shadow.innerHTML = "<b>I'm in the element's Shadow DOM!</b>";
};
var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});
要素の innerHTML を設定する代わりに、Shadow Root を作り、それをマークアップで埋めました。
DevTools の "Show Shadow DOM" 設定を enabled にすることで、#shadow-root を展開して見ることができます:
▾<x-foo-shadowdom>
▾#shadow-root
<b>I'm in the element's Shadow DOM!</b>
</x-foo-shadowdom>
これが Shadow Root です!
テンプレートから要素を作る
HTML Templates は Custom Elements の世界にぴったりの、もうひとつの新しい API です。
<template> 要素 はパースされ、ページロードと同時に有効になり、ランタイム中にインスタンス化される DOM フラグメントを宣言することができます。Custom Element の構造を宣言するのにぴったりのプレースホルダーだと思いませんか?
例: <template> と Shadow DOM から作った要素を登録する:
<template id="sdtemplate">
<style>
p { color: orange; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.</p>
</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>
I'm in Shadow DOM. My markup was stamped from a <template>.
たった数行のコードですが、この中には色んなモノが詰まっています。ここで行われていることを解説してみましょう:
- HTML に新しい要素
<x-foo-from-template>を登録 - その要素の DOM は
<template>から生成 - その要素の中身は Shadow DOM で隠蔽
- Shadow DOM により要素のスタイルはカプセル化されている (
p {color: orange;}はページ全体を オレンジ にしない)
どうですか?
Custom Element にスタイルを付与する
他の HTML タグ同様、あなたの Custom Element ユーザーはセレクターを使ってスタイルを割り当てることができます:
<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 を使った Custom Elements は恩恵も継承します。
Shadow DOM はスタイルのカプセル化の恩恵をもたらします。Shadow Root で定義されたスタイルはホストから漏れることはなく、ページから漏れてくることもありません。Custom Element の場合、要素自体がホストとなります。 Custom Elements は、カプセル化されたスタイルのプロパティのおかげで、デフォルトスタイルを定義することができます。
Shadow DOM のスタイルは複雑です!もっと詳しく知りたければ、私の書いた他の記事をおすすめします:
- Polymer のドキュメントから "A Guide to Styling Elements"
- html5rocks.com の "Shadow DOM 201: CSS & Styling"
:unresolved を使って FOUC を防ぐ
FOUC (Flash of unstyled content: スタイルが与えられていないコンテンツによる明滅) をなくすため、Custom Elements 仕様では、新しい CSS 擬似クラスである :unresolved を定義しています。createdCallback() が呼び出されるまでの、未解決の要素 に使って下さい (ライフサイクルメソッド参照)。一度解決すれば、要素はもう未解決ではなくなります。アップグレードプロセスが完了し、要素は定義された状態に変わります。
CSS :unresolved は Chrome 29 からサポートされています。
例: 登録されるとフェードインする "x-foo" タグ:
<style>
x-foo {
opacity: 1;
transition: opacity 300ms;
}
x-foo:unresolved {
opacity: 0;
}
</style>
:unresolved は 未解決の要素 にのみ適用され、 HTMLUnknownElement (要素のアップグレードについて 参照) から継承された要素には適用されないことに注意して下さい。
<style>
/* すべての未解決の要素に破線を適用 */
:unresolved {
border: 1px dashed red;
display: inline-block;
}
/* 未解決の x-panel は赤 */
x-panel:unresolved {
color: red;
}
/* x-panel の定義が登録されると緑に変わる */
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>
:unresolved についてより詳しく知るには、Polymer の A Guide to styling elements をご覧下さい。
歴史とブラウザーサポート状況
機能検知
機能検知は 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 が最新の仕様に対応した最初のブラウザーということになります。
Custom Elements は Chrome 31 で about:flags の "Experimental Web Platform features" フラグをオンにすることで利用できます。
ブラウザーサポートがよくなるまでの間使うことができる Polyfill がいくつかあります:
HTMLElementElement はどうなったの?
標準化を追いかけてきた人ならば、<element> という仕様が存在していたことをご存知のはずです。これは新しい要素を宣言的に定義できる素晴らしいものでした:
<element name="my-element">
...
</element>
残念なことに、アップグレードプロセス などで対処すべき問題が多過ぎたため、2013 年 8 月、Dimitri Glazkov が、少なくともしばらくは <element> を仕様から取り除くことを public-webapps のメーリングリストで宣言しました。
Polymer では <polymer-element> を使うことで、宣言的に要素を登録する方法を実装しています。どうやって? document.registerElement('polymer-element') と テンプレートから要素を作る で述べたテクニックを使っています。
最後に
Custom Elements は HTML のボキャブラリーを拡張し、新しいトリックや、ウェブプラットフォームの虫食い穴を飛び越えるためのツールを提供してくれます。これを Shadow DOM や <template> と組み合わせて使うことで、Web Components の絵が見えてきます。マークアップはまたセクシーになるのです!
Web Components について調べてみたいと思った方は、ぜひ Polymer をご覧下さい。期待以上のものが待ってるはずですよ!