Master PG

プログラムし続ける...

【Polymer 1.0】DOMの基本とAPI


目次


Shadow DOM と Shady DOM

Shadow DOMとはW3Cで策定中の機能で、エレメントを部品として扱うことができます。Shadow DOMによって、複数のエレメントで構成されたDOMツリーをまるで1つのエレメント(部品)のように表示させたり、操作することができます。

次のサンプルを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>

f:id:masterpg:20160923102947p:plain

<x-custom>の内部のエレメントとindex-shadow.htmlで記述されたエレメント(<span>山田</span>)が結合され、1つのエレメントとして表示されていることが確認できます。

現在Shadow DOMは仕様策定中で、全てのブラウザがShadow DOMをサポートするに至っていません。そこでPolymerはShadow DOMとほとんど同じ動作をするShady DOMというカスタム実装を作成し、これを使用することにより全てのブラウザでPolymerが動作する環境を整えました。現在のPolymerはShadow DOMをサポートしている環境でもShady DOMをデフォルトで使用します。


先程のプログラムを次の部分だけ書き換えて、Shady DOMで実行してみましょう:

⇒ ソースコード

index-shady.html

<!DOCTYPE html>
<html>
<head>
  ...
  <script>
    // Shady DOMで実行するよう設定
    window.Polymer = window.Polymer || {};
    window.Polymer.dom = 'shady';
  </script>
  ...

f:id:masterpg:20160923103049p:plain

内部的なエレメントの構成はShadow DOMとShady DOMで違いますが、表示内容は同じことが確認できます。

Note: ここではサンプルとしてShady DOMで実行するよう設定しましたが、デフォルトはShady DOMで実行されるので、実際はこの設定自体行う必要はありません。


Local DOM と Light DOM

Local DOMとはカスタムエレメントが作成/管理するDOMのことをいいます。Local DOMはカスタムエレメントの子エレメントとは明確に異なります。この2つを明確に区別するためにカスタムエレメントの子エレメントはLight 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がどのように結合されるかを説明した図です。

f:id:masterpg:20160923103137p:plain

この図を実際のプログラムと照らし合わせて確認してみてください。

⇒ ソースコード

<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>

f:id:masterpg:20160923103222p:plain


<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>

f:id:masterpg:20160923103311p:plain


プログラムで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に直接追加が行われたという感じになります。

f:id:masterpg:20160923103410p:plain


次はLight DOMにエレメントを追加するサンプルです。Local DOMのプログラムを1箇所だけ修正します。

⇒ ソースコード

attached: function () {
  ...
   // Light DOMへエレメントを追加
   Polymer.dom(this).appendChild(nameEl);
}

Light DOMに追加されたエレメントがLocal DOMへ結合され、適切な位置に"太郎"が表示されていることが確認できます。

f:id:masterpg:20160923103501p:plain


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-repeatdom-ifテンプレートを使用して動的に作成されたノードは、this.$ハッシュに追加されません。ハッシュには静的に作成されたLocal DOMのノードのみが含まれます。

動的に作成されたノードを検索するには$$メソッドを使用してください:

    this.$$(selector)

$$メソッドは、Local DOMでselectorに一致した最初のノードを返します。


次はノード検索のサンプルです:

  • 追加ボタン: 任意のユーザー名を入力して追加ボタンを押下すると、下の方にユーザーが追加されていきます。
  • 削除ボタン: 追加されたユーザーには「id」が表示されるので、このidを指定して削除ボタンを押下すると、ユーザーが削除されます。

⇒ ソースコード

f:id:masterpg:20160923103539p:plain

<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から情報を取り出す (offsetHeightgetComputedStyle()など) 際は、先に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.domAPIを使用せず、標準のDOM APIを使用してノードの変更をしたい場合、Polymer.BasedistributeContent()メソッドを呼び出すことで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

Effective childrenは、カスタムエレメントの全ての挿入ポイントに挿入されたLight DOMノードのことを指します。つまり挿入ポイントが複数あった場合は、そこに挿入された全てのノードが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という概念を追加しました。

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>のサンプルです:

⇒ ソースコード

f:id:masterpg:20160923103633p:plain

<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()の引数には、ノードが追加/削除された場合に呼び出されるコールバックを渡してください。このコールバックに渡されるinfoaddedNodesremovedNodesというプロパティを持ちます。

戻り値はハンドラが返され、このハンドラは監視を止めるために使用します:

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.$.contentthisとします。thisをカスタムエレメントにしたい場合は、コールバックをバインドする必要があります:

var boundHandler = this._childNodesChanged.bind(this);
this._observer = Polymer.dom(this.$.content).observeNodes(boundHandler);


コールバックの引数で渡されるinfoオブジェクトのaddedNodesremovedNodesは「ノード」であり「エレメント」ではありません。エレメントのみを取得したい場合、これらのノードリストをフィルタすることができます:

var addedElements = info.addedNodes.filter(function(node) {
  return (node.nodeType === Node.ELEMENT_NODE)
});


observeNodes()で指定したコールバックの最初の呼び出しでは、追加された全てのノードが含まれています。この全ノードとは、observeNodes()が設定されてから追加されたノードだけでなく、設定以前に追加されたノードも含まれます。


次はobserveNodes()を使用したサンプルです。追加/削除ボタンを押下するとアイコンが追加/削除され、observeNodes()の引数で指定したコールバックが呼び出されます。このコールバックの中では、追加/削除されたエレメントとLight DOMをログ出力しています。サンプルを実行してobserveNodes()の詳細を確認してみてください:

Note: このサンプルではBehaviorを使用しています。Behaviorの詳細については「Behaviors」を参照ください。

⇒ ソースコード

f:id:masterpg:20160923103722p:plain

<!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>