【Polymer 1.0】DOMの基本とAPI
目次
Shadow DOM と Shady DOM
次のサンプルをShadow DOMをサポートしているブラウザ(Chrome, Firefox等)で実行することで、Shadow DOMによる実行結果を確認できます:
x-custom.html
<link rel="import" href="../../bower_components/polymer/polymer.html"> <dom-module id="x-custom"> <template> <div style="border: solid red;"> <span>私は</span> <content></content> <span>です。</span> </div> </template> <script> Polymer({ is: 'x-custom' }); </script> </dom-module>
index-shadow.html
<!DOCTYPE html> <html> <head> <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script> // Shadow DOMで実行するよう設定 window.Polymer = window.Polymer || {}; window.Polymer.dom = 'shadow'; </script> <link rel="import" href="x-custom.html"> </head> <body> <x-custom> <span>山田</span> </x-custom> </body> </html>
<x-custom>
の内部のエレメントとindex-shadow.htmlで記述されたエレメント(<span>山田</span>
)が結合され、1つのエレメントとして表示されていることが確認できます。
現在Shadow DOMは仕様策定中で、全てのブラウザがShadow DOMをサポートするに至っていません。そこでPolymerはShadow DOMとほとんど同じ動作をする
先程のプログラムを次の部分だけ書き換えて、Shady DOMで実行してみましょう:
index-shady.html
<!DOCTYPE html> <html> <head> ... <script> // Shady DOMで実行するよう設定 window.Polymer = window.Polymer || {}; window.Polymer.dom = 'shady'; </script> ...
内部的なエレメントの構成はShadow DOMとShady DOMで違いますが、表示内容は同じことが確認できます。
Note: ここではサンプルとしてShady DOMで実行するよう設定しましたが、デフォルトはShady DOMで実行されるので、実際はこの設定自体行う必要はありません。
Local DOM と Light DOM
Local DOMにアクセスするには、Polymer.dom()
メソッドの引数にthis.root
を指定します:
Polymer.dom(this.root).appendChild(nameEl);
Light DOMにアクセスするには、Polymer.dom()
メソッドの引数にthis
を指定します:
Polymer.dom(this).appendChild(nameEl);
次はLocal DOMとLight DOMがどのように結合されるかを説明した図です。
この図を実際のプログラムと照らし合わせて確認してみてください。
<dom-module id="x-custom"> <!-- この中のエレメントはLocal DOMに追加される --> <template> <div id="container" style="border: solid red;"> <span>私は</span> <content></content> <span>です。</span> </div> </template> <script> Polymer({ is: 'x-custom', attached: function () { var i, j, child, grandchild; console.log('■■■ Light DOM ■■■'); var lightChildren = Polymer.dom(this).children; for (i = 0; i < lightChildren.length; i++) { child = lightChildren[i]; console.log(child); } console.log('■■■ Local DOM (結合結果) ■■■'); var resultChildren = Polymer.dom(this.root).children; for (i = 0; i < resultChildren.length; i++) { child = resultChildren[i]; console.log(child); } console.log('■■■ Local DOM (テンプレート) ■■■'); // <template>の中にあるid="container"のdivを取得 var container = Polymer.dom(this.root).children[0]; var containerChildren = Polymer.dom(container).children; for (i = 0; i < containerChildren.length; i++) { child = containerChildren[i]; console.log(child); } } }); </script> </dom-module>
<!-- この中のエレメントはLight DOMに追加される --> <x-custom> <span>山田</span> <span>太郎</span> </x-custom>
<content>エレメント
<content>
エレメントは、Light DOMに所属するエレメントの挿入位置を示します。またselect
属性にCSSセレクタを指定することで、挿入対象のエレメントをフィルタリングすることができます。
<dom-module id="x-custom"> <template> <div> <!-- greeting CSSクラスが当たっているエレメントの挿入位置 --> <content select=".greeting"></content> <span>私は</span> <!-- user CSSクラスが当たっているエレメントの挿入位置 --> <content select=".user"></content> <span>です。</span> </div> </template> <script> Polymer({ is: 'x-custom' }); </script> </dom-module>
<x-custom> <!-- このエレメントはLight DOMに追加される --> <span class="user">山田</span> <span class="user">太郎</span> <span class="greeting">こんにちは。</span> </x-custom>
プログラムでLocal DOMとLight DOMにエレメントを追加する
プログラムでLocal DOMとLight DOMにエレメントを追加し、その違いを見てみましょう。
まずはLocal DOMにエレメントを追加するサンプルです:
<dom-module id="x-custom"> <template> <div style="border: solid red;"> <span>私は</span> <content></content> <span>です。</span> </div> </template> <script> Polymer({ is: 'x-custom', attached: function () { var nameEl = document.createElement('span'); nameEl.innerHTML = '太郎'; // Local DOMへエレメントを追加 Polymer.dom(this.root).appendChild(nameEl); } }); </script> </dom-module>
<x-custom> <span>山田</span> </x-custom>
画面上に"太郎"は表示されていますが、Local DOMに直接追加が行われたという感じになります。
次はLight DOMにエレメントを追加するサンプルです。Local DOMのプログラムを1箇所だけ修正します。
attached: function () { ... // Light DOMへエレメントを追加 Polymer.dom(this).appendChild(nameEl); }
Light DOMに追加されたエレメントがLocal DOMへ結合され、適切な位置に"太郎"が表示されていることが確認できます。
Local DOMテンプレート
カスタムエレメントの作成でLocal DOMを定義したい場合、<dom-module>
と<template>
を使用します。<dom-module>
にはid
属性を追加し、これをPolymer()
関数のプロトタイプ引数で指定するis
と一致させて2つをひも付けます。<template>
は<dom-module>
の中で定義します。Polymerは<template>
で定義したコンテンツをLocal DOMの中へ複製します。
<dom-module id="x-custom"> <template>I am x-custom!</template> <script> Polymer({ is: 'x-custom' }); </script> </dom-module>
カスタムエレメントの定義には指示的なものと宣言的なものとがあります。指示的なものはPolymer({...})
の呼び出しで、宣言的なものは<dom-module>
エレメントです。カスタムエレメントの定義における指示的と宣言的な部分は、同じHTMLファイルに記述できますし、ファイルを分けることもできます。
次は指示的と宣言的な部分をファイル分割したサンプルです:
x-custom.js
Polymer({ is: 'x-custom', properties: { user: {type: String, value: '太郎'} } });
x-custom.html
<link rel="import" href="../../bower_components/polymer/polymer.html"> <dom-module id="x-custom"> <template>I am <span>{{user}}</span> !</template> <script src="x-custom.js"></script> </dom-module>
index.html
<!DOCTYPE html> <html> <head> <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <link rel="import" href="x-custom.html"> </head> <body> <x-custom></x-custom> </body> </html>
カスタムエレメント内のノード検索
PolymerはLocal DOMでインスタンス化されたノードの静的なマップを構築し、これらのノードへアクセスするためのショートカットを提供します。<template>
の中にあるid
付きのノードにアクセスするには、this.$
というハッシュにアクセスしたいノードのid
を続けます。
<dom-module id="x-custom"> <template> Hello World from <span id="user"></span>! </template> <script> Polymer({ is: 'x-custom', ready: function () { // this.$を利用してノードにアクセス this.$.user.textContent = '太郎'; } }); </script> </dom-module>
データバインディングや、dom-repeat
、dom-if
テンプレートを使用して動的に作成されたノードは、this.$
ハッシュに追加されません。ハッシュには静的に作成されたLocal DOMのノードのみが含まれます。
動的に作成されたノードを検索するには$$
メソッドを使用してください:
this.$$(selector)
$$
メソッドは、Local DOMでselectorに一致した最初のノードを返します。
次はノード検索のサンプルです:
- 追加ボタン: 任意のユーザー名を入力して追加ボタンを押下すると、下の方にユーザーが追加されていきます。
- 削除ボタン: 追加されたユーザーには「
id 」が表示されるので、このid を指定して削除ボタンを押下すると、ユーザーが削除されます。
<dom-module id="x-custom"> <template> <div> <input id="userInput" placeholder="ユーザー名を入力"> <button on-tap="addOnTap">追加</button> </div> <div> <input id="removeInput" placeholder="削除するidを入力"> <button on-tap="removeOnTap">削除</button> </div> <div id="container"></div> </template> <script> Polymer({ is: 'x-custom', properties: { seq: {type: Number, value: 1} }, addOnTap: function (e) { // ユーザーリストを追加 var userEl = document.createElement('div'); userEl.id = 'user' + this.seq++; userEl.innerHTML = 'id: ' + userEl.id + ', ユーザー名: ' + this.$.userInput.value; Polymer.dom(this.$.container).appendChild(userEl); }, removeOnTap: function (e) { // 動的に追加されたユーザーを$$メソッドを使用してアクセス var removeTargetEl = this.$$('#' + this.$.removeInput.value); if (!removeTargetEl) { return; } // 指定されたユーザーを削除 Polymer.dom(this.$.container).removeChild(removeTargetEl); } }); </script> </dom-module>
DOM API
PolymerはLocal DOMツリーとLight DOMツリーが独立して維持できるように、DOMを操作するカスタムAPIを提供します。提供されるAPIメソッドとプロパティは標準DOMと同様のシグニチャーをもちますが、メソッドまたはプロパティの戻り値がノードのリストの場合はNodeList
ではなくノードのArray
を返します。
Note: 全てのDOM操作は、標準のDOM APIを直接使用するのではなく、Polymerが提供するAPIを使用しなくてはなりません。
以下にPolymerが提供するメソッドとプロパティを記載します。
- 子ノードの追加/削除:
Polymer.dom(parent).appendChild(node)
Polymer.dom(parent).insertBefore(node, beforeNode)
Polymer.dom(parent).removeChild(node)
Polymer.dom.flush()
非同期オペレーション: 挿入、追加、削除オペレーションは状況によって遅延して処理される場合があります。このため、これらのオペレーションの直後にDOMから情報を取り出す (
offsetHeight
、getComputedStyle()
など) 際は、先にPolymer.dom.flush()
メソッドを呼び出すようにしてください。
親子関係API:
Polymer.dom(parent).children
Polymer.dom(parent).childNodes
Polymer.dom(node).parentNode
Polymer.dom(node).firstChild
Polymer.dom(node).lastChild
Polymer.dom(node).firstElementChild
Polymer.dom(node).lastElementChild
Polymer.dom(node).previousSibling
Polymer.dom(node).nextSibling
Polymer.dom(node).textContent
Polymer.dom(node).innerHTML
クエリセレクタAPI:
Polymer.dom(parent).querySelector(selector)
Polymer.dom(parent).querySelectorAll(selector)
コンテンツAPI:
Polymer.dom(contentElement).getDistributedNodes()
Polymer.dom(node).getDestinationInsertionPoints()
ノード変更API:
Polymer.dom(node).setAttribute(attribute, value)
Polymer.dom(node).removeAttribute(attribute)
Polymer.dom(node).classList
もしここで示したPolymer.dom
APIを使用せず、標準のDOM APIを使用してノードの変更をしたい場合、Polymer.Base
のdistributeContent()
メソッドを呼び出すことでLocal DOMを更新し、Light DOMの結合を強制させることができます。
Distributed children
<content>
に挿入されたノードにアクセスしたい、またはどの<content>
へノードが挿入されたかを知りたい場合、getDistributedNodes()
とgetDestinationInsertionPoints()
によって、これらの情報を取得することができます。
getDistributedNodes()
は<content>
が指定された場合にだけ意味のある結果を返すメソッドで、getDestinationInsertionPoints()
は挿入されたエレメントを指定する場合にだけ意味のある結果を返すメソッドです。
<dom-module id="x-custom"> <template> <content></content> <button on-tap="validOnTap">検証</button> </template> <script> Polymer({ is: 'x-custom', validOnTap: function (e) { // Light DOMからdivエレメント取得 var div = Polymer.dom(this).querySelector('div'); // Local DOMからcontentエレメント取得 var content = Polymer.dom(this.root).querySelector('content'); // 取得したcontentエレメントに挿入されたエレメントを取得 var distributed = Polymer.dom(content).getDistributedNodes()[0]; // 取得したdivエレメントがどこへ挿入されたかを取得 var insertedTo = Polymer.dom(div).getDestinationInsertionPoints()[0]; // 次2つの検証は正常になる console.assert(distributed === div, 'distributedとdivが同一オブジェクトではありません。'); console.assert(insertedTo === content, 'insertedToとcontentが同一オブジェクトではありません。'); } }); </script> </dom-module>
<x-custom><div>山田</div></x-custom>
<content>
に挿入された子ノードを取り出すことは良くあるパターンなので、getContentChildNodes()
、getContentChildren()
というユーティリティメソッドが提供されています。どちらのメソッドも引数にCSSセレクタをとり、このCSSセレクタがエレメントのLocal DOMにある<content>
エレメントを特定します (もしCSSセレクタを指定しない場合、どちらのメソッドもLocal DOMにある最初の<content>
エレメントを対象にします) 。
<dom-module id="x-custom"> <template> <content id="myContent"></content> <button on-tap="validOnTap">検証</button> </template> <script> Polymer({ is: 'x-custom', validOnTap: function (e) { // Light DOMからdivエレメント取得 var div = Polymer.dom(this).querySelector('div'); // contentエレメントに挿入されたエレメントを取得 var distributed = this.getContentChildren('#myContent')[0]; // この検証は正常になる console.assert(distributed === div, 'distributedとdivが同一オブジェクトではありません。'); } }); </script> </dom-module>
<x-custom><div>山田</div></x-custom>
Effective children
まずは「Local DOMを持たない」シンプルなイメージカルーセルエレメントを想像してください:
<simple-carousel> <img src="one.jpg"> <img src="two.jpg"> <img src="three.jpg"> <simple-carousel>
このカルーセルはイメージを管理するために、子エレメントがいくつあるか知りたいとします。子エレメントの数は次のように取得することができます:
attached: function() { var childCount = Polymer.dom(this).children.length; // childCountを使用した処理 ... }
ただし、ここで少し問題が出てきます。新しく<popup-carousel>
というエレメントを作成し、このエレメントの中に<simple-carousel>
を含んだ場合はどうでしょう。新しく作成した<popup-carousel>
も今までと同様の使い方をします:
<popup-carousel> <img src="one.jpg"> <img src="two.jpg"> <img src="three.jpg"> </popup-carousel>
<popup-carousel>
の内部は次のようになります:
<dom-module id="popup-carousel"> <template> <simple-carousel> <content></content> </simple-carousel> </template> ... </dom-module>
<popup-carousel>
は<content>
タグを含むことによって、子エレメントを<simple-carousel>
へシンプルに渡すことができます。
ただし、<simple-carousel>
のattached
メソッドでは望むような子エレメントの数を取得することがでません。Polymer.dom(this).children.length
は常に「1」を返します。なぜなら、<simple-carousel>
は<content>
という1つの子エレメントしか持たないためです。
ここで取得した子ノードは明らかに望んでいたものではありません。実際には、<content>
タグに挿入された子エレメントの数を望んでいたはずです。不幸にもネイティブでこれを解決する機能を持ちません。そこで、PolymerはDOM APIに
Effective childrenのノードは次のように取り出すことができます:
var effectiveChildNodes = Polymer.dom(element).getEffectiveChildNodes();
この他にもPolymerのプロトタイプでは、次のように便利なメソッドが提供されます:
getEffeciveChildNodes()
: エレメントのEffective childrenのノードリストを返します。getEffectiveChildren()
: エレメントのEffective childrenのエレメントリストを返しますqueryEffectiveChildren(selector)
: selectorとマッチする最初のEffective childrenのエレメントを返します。queryAllEffectiveChildren(selector)
: selectorとマッチする全てのEffective childrenのエレメントリストを返します。
<simple-carousel>
では、getEffectiveChildren()
を使用することで、望むような子エレメントの数を取得することができます:
var childCount = this.getEffectiveChildren().length;
次はここで説明した<popup-carousel>
と<simple-carousel>
のサンプルです:
<dom-module id="simple-carousel"> <script> Polymer({ is: 'simple-carousel', attached: function () { var childrenCount = Polymer.dom(this).children.length; var effectiveChildrenCount = this.getEffectiveChildren().length; console.log('childrenCount: ' + childrenCount + ', effectiveChildrenCount: ' + effectiveChildrenCount); } }); </script> </dom-module>
<dom-module id="popup-carousel"> <template> <simple-carousel class="layout horizontal"> <content id="popupContentNode"></content> </simple-carousel> </template> <script> Polymer({ is: 'popup-carousel' }); </script> </dom-module>
<popup-carousel> <img src="images/red1.png"> <img src="images/red2.png"> </popup-carousel>
Effective childrenの追加/削除の監視
カスタムエレメントで、Effective childrenの追加/削除を監視するには、observeNodes()
を利用します:
this._observer = Polymer.dom(this.$.contentNode).observeNodes(function(info) { this.processNewNodes(info.addedNodes); this.processRemovedNodes(info.removedNodes); });
observeNodes()
の引数には、ノードが追加/削除された場合に呼び出されるコールバックを渡してください。このコールバックに渡されるinfo
はaddedNodes
とremovedNodes
というプロパティを持ちます。
戻り値はハンドラが返され、このハンドラは監視を止めるために使用します:
Polymer.dom(node).unobserveNodes(this._observer);
observeNodes()
は、監視するノードによって、わずかに異なった振る舞いをします:
- 監視されるのが
<content>
ノードの場合、ここにノードの追加/削除があるとコールバックが呼び出されます。 - それ以外の場合は、Effective childrenに追加/削除があるとコールバックが呼び出されます。
observeNodes()の注意点
observeNodes()メソッドはDOM APIなので、コールバックは監視ノードをthis
として呼び出します:
this._observer = Polymer.dom(this.$.content).observeNodes(this._childrenChanged);
_childrenChanged
コールバックの中では、this.$.content
をthis
とします。this
をカスタムエレメントにしたい場合は、コールバックをバインドする必要があります:
var boundHandler = this._childNodesChanged.bind(this); this._observer = Polymer.dom(this.$.content).observeNodes(boundHandler);
コールバックの引数で渡されるinfo
オブジェクトのaddedNodes
とremovedNodes
は「ノード」であり「エレメント」ではありません。エレメントのみを取得したい場合、これらのノードリストをフィルタすることができます:
var addedElements = info.addedNodes.filter(function(node) { return (node.nodeType === Node.ELEMENT_NODE) });
observeNodes()
で指定したコールバックの最初の呼び出しでは、追加された全てのノードが含まれています。この全ノードとは、observeNodes()
が設定されてから追加されたノードだけでなく、設定以前に追加されたノードも含まれます。
次はobserveNodes()
を使用したサンプルです。追加/削除ボタンを押下するとアイコンが追加/削除され、observeNodes()
の引数で指定したコールバックが呼び出されます。このコールバックの中では、追加/削除されたエレメントとLight DOMをログ出力しています。サンプルを実行してobserveNodes()
の詳細を確認してみてください:
Note: このサンプルではBehaviorを使用しています。Behaviorの詳細については「Behaviors」を参照ください。
<!DOCTYPE html> <html> <head> <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <link rel="import" href="../../bower_components/polymer/polymer.html"> <link rel="import" href="../../bower_components/iron-flex-layout/classes/iron-flex-layout.html"> </head> <body> <popup-carousel> <img src="images/red1.png"> <img src="images/red2.png"> </popup-carousel> <script> document.addEventListener('WebComponentsReady', function () { CarouselBehavior = { properties: { componentName: String }, ready: function () { var boundHandler = this._childNodesChanged.bind(this); this._observer = Polymer.dom(this).observeNodes(boundHandler); }, _childNodesChanged: function (info) { console.log('■■■■■ ' + this.componentName + ' ■■■■■'); var addedElements = info.addedNodes.filter(function (node) { return (node.nodeType === Node.ELEMENT_NODE) }); var removedElements = info.removedNodes.filter(function (node) { return (node.nodeType === Node.ELEMENT_NODE) }); console.log('● addedElements', addedElements); console.log('● removedElements', removedElements); console.log('● Light DOM'); var lightChildren = Polymer.dom(this).children; for (var i = 0; i < lightChildren.length; i++) { child = lightChildren[i]; console.log(' ', child); } console.log(''); } } }); </script> <dom-module id="simple-carousel"> <script> document.addEventListener('WebComponentsReady', function () { Polymer({ is: 'simple-carousel', behaviors: [CarouselBehavior], created: function () { this.componentName = 'simple-carousel'; } }); }); </script> </dom-module> <dom-module id="popup-carousel"> <template> <simple-carousel class="layout horizontal"> <content id="popupContentNode"></content> </simple-carousel> <button on-tap="_addOnTap">追加</button> <button on-tap="_removeOnTap">削除</button> </template> <script> document.addEventListener('WebComponentsReady', function () { Polymer({ is: 'popup-carousel', behaviors: [CarouselBehavior], created: function () { this.componentName = 'popup-carousel'; }, _addOnTap: function (event) { var imgElements = Polymer.dom(this).querySelectorAll('img'); if (imgElements.length >= 9) { return; } var imgEl = document.createElement('img'); imgEl.src = 'images/red' + (imgElements.length + 1) + '.png'; Polymer.dom(this).appendChild(imgEl); }, _removeOnTap: function (event) { var imgElements = Polymer.dom(this).querySelectorAll('img'); if (imgElements.length <= 0) { return; } var lastImg = imgElements[imgElements.length - 1]; Polymer.dom(this).removeChild(lastImg); } }); }); </script> </dom-module> </body> </html>