読者です 読者をやめる 読者になる 読者になる

ko.utils(その他)その2

これはKnockoutJSアドベントカレンダー10日目の記事です。


KnockoutJS Advent Calendar 2014 - Qiita



今日もまた Knockout の便利機能が入った ko.utils について適当に紹介します。
まだまだあるよ!

今日紹介するのは以下のメソッド

  • ko.utils.domData.clear
  • ko.utils.domNodeDisposal.cleanNode / ko.utils.domNodeDisposal.removeNode
  • ko.utils.domNodeDisposal.addDisposeCallback
  • ko.utils.parseHtmlFragment
  • ko.utils.setHtml
  • ko.utils.compareArrays
  • ko.utils.setDomNodeChildrenFromArrayMapping


内部的に使われているのが殆どなので、実際に使う機会はほとんどないと思いますが気にしないでいきましょう。
※たぶん使うとしたら ko.utils.domNodeDisposal.addDisposeCallback ぐらい?



  • ko.utils.domData.clear

ko.utils.domData というオブジェクトが持っているメソッドです。
clearだけじゃなくて set/get/nextKey があります。

elementオブジェクトに対してkoで使う内部的なデータを保持しています。
ko.utils.domData.clear だけ独立して exportSymbol してるのはテストとかで内部的な値をクリアするときに使うからっぽいです。
内部的なやつなので殆ど使うことはないでしょう。

サンプル

<div id="sample"></div>
<script>
var output = null;
ko.utils.domData.set(document.getElementById("sample"), "foo", "bar");
output = ko.utils.domData.get(document.getElementById("sample"), "foo");
console.log(output);
ko.utils.domData.clear(document.getElementById("sample"));
output = ko.utils.domData.get(document.getElementById("sample"), "foo");
console.log(output);
</script>

結果

bar
undefined
  • ko.utils.domNodeDisposal.cleanNode / ko.utils.domNodeDisposal.removeNode

それぞれ ko.cleanNode と ko.removeNode です。

ko.cleanNodeは ko.applyBindings で ViewModelとDOM要素とバインディングしたデータをクリアできます。
例えば以下のような HTML を用意します。

<input id="sample1" type="text" data-bind="textInput: foo" />
<span id="sample2" data-bind="text: bar"></span>
<script>
function ViewModel(){
  var self = this;
  self.foo = ko.observable();
  self.bar = ko.pureComputed(function(){
    return self.foo();
  });
}
ko.applyBindings(new ViewModel());
</script>

ここで ID=sample1 の入力フィールドに値をいれるとID=sample2 の値も同時に変わります。
次に、(Chromeの場合)デベロッパーコンソールの Console を開いて以下を入力してみます。

ko.cleanNode(document.getElementById("sample2"));

するとどうでしょう。
さっきまで反応していた ID=sample2 が入力フィールドに何を入れても反応しなくなりました。

今度は Console に以下を入力してみましょう。

ko.removeNode(document.getElementById("sample1"));

今度は入力フィールドが削除されました。
Knockoutを使ってバインディングされた要素はこのように removeNode で削除してください。
これをしないで単純にDOM要素を削除してしまうとメモリリークの原因になるので要注意です。
※このメソッドは要素を削除する前に内部でko.cleanNodeを呼び出して要素のデータをクリアしています。



  • ko.utils.domNodeDisposal.addDisposeCallback

説明は公式ドキュメントにあります。
http://knockoutjs.com/documentation/custom-bindings-disposal.html

公式ドキュメントにもあるようにこのメソッドはカスタムバインディングハンドラでよく使用します。

pureComputedで使ったサンプルを使用してこれに独自のカスタムバインディングを作って使ってみましょう。

    <!--ko if: step() == 0-->
    <p>First name: <input data-bind="textInput: firstName" /></p>
    <!--/ko-->
    <!--ko if: step() == 1-->
    <p>Last name: <input data-bind="textInput: lastName, hoge" /></p>
    <!--/ko-->
    <!--ko if: step() == 2-->
    <div>Prefix: <select data-bind="value: prefix, options: ['Mr.', 'Ms.','Mrs.','Dr.']"></select></div>
    <h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
    <!--/ko-->
    <p><button type="button" data-bind="click: next">Next</button></p>
      ko.bindingHandlers.hoge = {
        init: function(element, valueAccessor) {
          alert("init");
          ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            alert("dispose");
          });
        }
      };
      function AppData() {
        this.firstName = ko.observable('John');
        this.lastName = ko.observable('Burns');
        this.prefix = ko.observable('Dr.');
        this.fullName = ko.pureComputed(function () {
          var value = this.prefix() + " " + this.firstName() + " " + this.lastName();
          return value;
        }, this);
 
        this.step = ko.observable(0);
        this.next = function () {
          this.step(this.step() === 2 ? 0 : this.step()+1);
        };
      };
      ko.applyBindings(new AppData());

hoge というバインディングハンドラを作り2つめの画面のテキストフィールドに適用します。

サンプル
http://jsfiddle.net/tan_go238/86vxks2u/1/

最初の画面からNextボタンを押して、2つめの画面が呼び出されたときに hogeバインディングハンドラが初期化されます。
そのページからNextボタンを押して次のページに移動しようとしたときにバインディングハンドラが破棄されます。
上記のサンプルでは、初期化、破棄のそれぞれタイミングでアラートダイアログが表示されます。

  • ko.utils.parseHtmlFragment

文字列のHTMLコードからDOM要素を作成して返します。jQueryが使える場合は jQuery.parseHTMLを呼び出します。
knockoutは"jQueryが使えれば jQuery を使う、でなければ自前のやつを使う" みたいな実装のところがちらほらあるので、
KnockoutJSを使うときは jQuery も一緒に使うとより安心して使えると思います。


  • ko.utils.setHtml

ko.utils.setHtml(node, html)

第二引数で渡したHTMLコードをDOM要素にして第一引数に渡したノードにセットします。
jQueryがある場合は $.html(htmlString) を使用します。
ない場合は ko.utils.parseHtmlFragment を使いDOM要素を作成してappendChildしていきます。


  • ko.utils.compareArrays

配列同士を比較します。
結果は Objectの配列になります。
Objectのプロパティには status と value があり、valueは実際の値、statusは3つの状態("added", "retained", "deleted")のいずれかになります。

var foo = ["aaa", "bbb", "ccc"];
var bar = ["bbb", "ddd", "eee"];
var differences = ko.utils.compareArrays(foo, bar);
var addedValues = [];
var retainedValues = [];
var deletedValues = [];
ko.utils.arrayForEach(differences, function (difference) {
    if (difference.status === "added") {
        addedValues.push(difference.value);
    } else if (difference.status === "retained") {
        retainedValues.push(difference.value);
    } else if (difference.status === "deleted") {
        deletedValues.push(difference.value);
    }
});
console.log(addedValues);
console.log(retainedValues);
console.log(deletedValues);

結果

 ["ddd", "eee"]
 ["bbb"]
 ["aaa", "ccc"]
  • ko.utils.setDomNodeChildrenFromArrayMapping

option バインディングや foreach バインディングに使用しています。
子要素が追加されたり削除されたりしたときにコールバックを呼んだり削除時にko.removeNodeを呼んだりと色々なことをしてくれます。



3回に分けてざっと ko.utils のメソッドやオブジェクトを紹介してきましたが、
まだあると思うので探してみるといいかと思います。
3.1までのバージョンなら以下に一覧があります。
※一部ko内部でしか使えないものもあり


KnockoutJS 3.1.0 utils (ko.utils) signatures


明日は @sukobuto さんの「KO + TypeScript で大規模 SPA 開発」です!
よろしくお願いします!