Mappingプラグイン(その2)

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


KnockoutJS Advent Calendar 2014 - Qiita



MappingプラグインはKnockoutJS公式のドキュメントに唯一書かれてあるプラグインです。
http://knockoutjs.com/documentation/plugins-mapping.html


公式ドキュメントには色々書いてありますが、簡単に言うと
「JSのオブジェクト内のプロパティや配列を observable / observableArray に変換」するプラグインです。

前回基本的な使い方を説明したのですが、それだけでは実際に使うのは難しい場面が多いと思うので、
その際に必要になるマッピングオプションを説明しようと思います。

公式ドキュメントを要約した感じなので詳しくはそちらを参照してください。


key, create, update

以下のようなデータがあります。

var data = {
    name: 'Scot',
    children: [
        { id : 1, name : 'Alicw' }
    ]
}
var viewModel = ko.mapping.fromJS(data);

上記のコードにtypoがあるので、以下のコードで上書きをしたいと思いました。

var data = {
    name: 'Scott',
    children: [
        { id : 1, name : 'Alice' }
    ]
}
ko.mapping.fromJS(data, viewModel);

上記のように書くことで name は 'Scott' に変わりましたが、 children は上書きしてほしかったのですが、
上書きされずに一度削除され新しく追加されました。

と公式ドキュメントにはこう書いてあるのですが、挙動が分かりにくいかと思うので

マッピングオプションの create と update も加えてサンプルを使って説明します。

まず先程のサンプルの挙動がどうなるのかを見てみます。

サンプルコード

var data = {
    name: 'Scot',
    children: [
        { id : 1, name : 'Alicw' }
    ]
}
var viewModel = ko.mapping.fromJS(data);
var data = {
    name: 'Scott',
    children: [
        { id : 1, name : 'Alice' }
    ]
}
var mapping = {
    'children': {
        create: function(options) {
            console.log("create");
            return options.data;
        },
        update: function(options) {
            console.log("update");
            return options.data;
        }
    }
}
ko.mapping.fromJS(data, mapping, viewModel);
ko.applyBindings(viewModel);

ログ出力は以下のようになりました。

create
update

これを id を key にして上書きするようにしてみましょう。

<p data-bind="text: name"></p>
<div data-bind="foreach:children">
  <p data-bind="text: id"></p>
  <p data-bind="text: name"></p>
</div>

<script>
var data = {
    name: 'Scot',
    children: [
        { id : 1, name : 'Alicw' }
    ]
}
var viewModel = ko.mapping.fromJS(data);
var data = {
    name: 'Scott',
    children: [
        { id : 1, name : 'Alice' }
    ]
}
var mapping = {
    'children': {
        key: function(data) {
            return ko.utils.unwrapObservable(data.id);
        },
        create: function(options) {
            console.log("create");
            return options.data;
        },
        update: function(options) {
            console.log("update");
            return {id:options.data.id, name:options.data.name};
        }
    }
}
ko.mapping.fromJS(data, mapping, viewModel);
ko.applyBindings(viewModel);
</script>


するとログ出力は以下のようになります。

update

上記のように key を指定することで create を実行せず key の情報をもとに update だけを実行します。



mappedRemove, mappedCreate

公式ドキュメントにありますが、どういう出力結果になるかぱっと見で分からなかったので試してみました。

var obj = [
    { id : 1 },
    { id : 2 }
]
 
var result = ko.mapping.fromJS(obj, {
    key: function(item) {
        return ko.utils.unwrapObservable(item.id);
    },
    create: function(options) {
        console.log("create");
        return options.data;
    },
    update: function(options) {
        console.log("update");
        return options.data;
    },
});
console.log(result().length); // 配列の長さ: 2
console.log(result()[0].id); // 出力結果: 1
console.log(result()[1].id); // 出力結果: 2
result.mappedRemove({ id : 1 });
console.log(result().length); // 配列の長さ: 1
console.log(result()[0].id); // 出力結果: 2

var newItem = result.mappedCreate({ id : 3 });
console.log(result().length); // 配列の長さ: 2
console.log(result()[0].id); // 出力結果: 2
console.log(result()[1].id); // 出力結果: 3
console.log(newItem); // 出力結果: Object {id: 3}


他にも ignore, include, copy, observe や mappedXxx が色々ありますがまた機会があれば。


明日は @yusuke_nozoe さんです!よろしくお願いします!

Mappingプラグイン(その1)

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


KnockoutJS Advent Calendar 2014 - Qiita


MappingプラグインはKnockoutJS公式のドキュメントに唯一書かれてあるプラグインです。
http://knockoutjs.com/documentation/plugins-mapping.html


公式ドキュメントには色々書いてありますが、簡単に言うと
「JSのオブジェクト内のプロパティや配列を observable / observableArray に変換」するプラグインです。

個人的にはばっちりハマるパターンにまだ落とし込めてないので良さげなパターンあれば教えて欲しいです。

下図の Model 部分についてサーバからJSONを取得してマッピングさせるときに便利なんじゃないかと思います。
※公式ドキュメントでは view model って言っているんですが observable / observableArray だけのViewModelって
あんま使い途なさそうでその辺の説明の意図がよく分かっていません。


http://www.wisetechglobal.com/Portals/WTG/downloads/WTF04-Slides/images/mvvm.png

http://www.wisetechglobal.com/Portals/WTG/downloads/WTF04-Slides/index.html#11



では最初にAPIを見ていきます。

基本的に使うものは以下の4つです。

(1) ko.maping.fromJS
(2) ko.mapping.toJS
(3) ko.mapping.fromJSON
(4) ko.mapping.toJSON

(1) は JSオブジェクト内のプロパティや配列を observable / observableArray に変換します。
実際やってみると以下のような感じになります。

var data = {
    first: 'Hello',
    last: 'World',
    name: ['h','e','l','l','o']
};
var model = ko.mapping.fromJS(data);
console.log(model.first);
console.log(model.first());
console.log(model.first.peek());
console.log(model.name);
console.log(model.name());
console.log(model.name.peek());

出力結果

function d(){if(0<arguments.length)return d.Pa(c,arguments[0])&&(d.X(),c=arguments[0],d.W()),this;a.k.Jb(d);return c}
Hello
Hello
function d(){if(0<arguments.length)return d.Pa(c,arguments[0])&&(d.X(),c=arguments[0],d.W()),this;a.k.Jb(d);return c}
["h", "e", "l", "l", "o"]
["h", "e", "l", "l", "o"]


(2) は逆にJSONオブジェクトに変換します。
こんな感じ。

function ViewModel(){
      var self = this;
      self.first = ko.observable('Hello');
      self.last = ko.observable('World');
      self.name = ko.observableArray(['h','e','l','l','o']);
}
var jsObj = ko.mapping.toJS(new ViewModel());
console.log(jsObj);

出力結果

Object {
  first: "Hello",
  last: "World",
  name: Array[5]
      0: "h"
      1: "e"
      2: "l"
      3: "l"
      4: "o"
      length: 5
      __proto__: Array[0]
  __proto__: Object
}

(3)と(4)はソース読むとすぐ分かるのですが、
(3) は ko.utils.parseJson() した結果を (1) に渡すだけ、
(4) は (2)の結果を ko.utils.stringifyJson() するだけになります。



次に (1) fromJSと (3) fromJSON メソッドの引数について説明します。

この2つのメソッドは引数の数と型によって動作が若干異なります。

・引数が1つの場合
データを引数にマッピングされたモデルを返します。

 例)var viewModel = ko.mapping.fromJSON(data)


・引数が2つの場合

2つめの引数が ViewModel の場合、これを更新します。

 例)ko.mapping.fromJSON(data,koMappingCreatedViewModel)

または2つめの引数をマッピングの際のオプションとして渡すことができます。

 例)var viewModel = ko.mapping.fromJSON(data,options)

・引数が3つの場合

2つめの引数をオプション、3つめの引数をターゲットとして更新します。

 例)ko.mapping.fromJSON(data, options, target)


基本的な使い方はこれだけなのですが、実際使おうとするとこれだけではなかなか難しいので、
以下のマッピング時のオプションが用意されています。


それらの使い方についてはまた次回。


明日は @sukobuto さんの「SPA で Enter キーフォーカス遷移」です!
よろしくおねがいします!

Knockoutの template バインディング

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


KnockoutJS Advent Calendar 2014 - Qiita




KnockoutJSには template バインディングなるものがあります。

これは何かというと HTMLの一部を切り出してテンプレート化して使える機能です。

利用用途としては「Componentsにするほどでもないけどこの画面でよく使うので切り出しておきたい」と

いった場合などでしょうか。

それではさっそく公式サイトのドキュメントからかいつまんで説明していきたいと思います。

http://knockoutjs.com/documentation/template-binding.html


公式ドキュメントにあるように、 template バインディングの書き方は以下のようになります。

<h2>Participants</h2>
Here are the participants:
<div data-bind="template: { name: 'person-template', data: buyer }"></div>
<div data-bind="template: { name: 'person-template', data: seller }"></div>
 
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>
 
<script type="text/javascript">
function MyViewModel() {
    this.buyer = { name: 'Franklin', credits: 250 };
     this.seller = { name: 'Mario', credits: 5800 };
}
ko.applyBindings(new MyViewModel());
</script>

テンプレート化したい部分を で切り出して ID を振ります。
そしてその振ったIDを templateバインディングの name プロパティに指定してバインドさせます。

data プロパティにはテンプレートにレンダリングしたいオブジェクトを設定します。
このオブジェクトのプロパティがテンプレート内の変数として利用できます。

ドキュメントには data に値を設定しない場合は foreach のパラメータか現在のモデルオブジェクトを使用するとあります。

早速試してみましょう。dataを省いた以下のようなコードでもちゃんと動作しました。

http://jsfiddle.net/2jcuzg4r/

<div data-bind="foreach: people">
  <div data-bind="template: { name: 'person-template' }"></div>
</div>

<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p><span data-bind="text: gender"></span></p>
</script>
 
<script type="text/javascript">
function Person(name, gender){
  this.name = name;
  this.gender = gender;
}
function ViewModel() {
  this.people = ko.observableArray([
    new Person("Bob", "Male"),
    new Person("Mike", "Male"),
    new Person("Katherine", "Female")
  ]);
}
ko.applyBindings(new ViewModel());
</script>

紹介した name, data プロパティ以外にもtemplateバインディングには
以下のようなプロパティが使えます。

if, foreach, as, afterRender, afterAdd, beforeRemove


if はプロパティにの値が true だったら表示、そうじゃなかったら非表示っていう感じです。

簡単なサンプル作ってみました。

http://jsfiddle.net/tan_go238/uyhtd1k7/

<h2>Participants</h2>
Here are the participants:<br>
<input type="checkbox" data-bind="checked: show1"> <br>
<input type="checkbox" data-bind="checked: show2"> <br>
<div data-bind="template: { name: 'person-template', data: buyer, if: show1 }"></div>
<div data-bind="template: { name: 'person-template', data: seller, if: show2 }"></div>
 
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>

<script type="text/javascript">
function MyViewModel() {
    this.show1 = ko.observable();
    this.show2 = ko.observable();
    this.buyer = { name: 'Franklin', credits: 250 };
    this.seller = { name: 'Mario', credits: 5800 };
}
ko.applyBindings(new MyViewModel());
</script>


foreach はオブジェクトの配列をぐるぐる回す感じで使います。

テンプレート内の変数になるのは(配列の中の)それぞれオブジェクトのプロパティです。

また "foreach: someExpression" は "template: { foreach: someExpression }" と同じになります。

使い方は公式サイトにサンプルがあるのでそれを参考にしてください。


foreachの中でこのようにテンプレートをネストすることもできます。

http://jsfiddle.net/tan_go238/1gwh5a45/1/

<div data-bind="template: { name: 'parent-template', foreach: people }"></div>

<script type="text/html" id="parent-template">
    <h3 data-bind="text: name"></h3>
    <div data-bind="template: { name: 'child-template', foreach: address }"></div>
</script>
<script type="text/html" id="child-template">
    <p><span data-bind="text: region"></span>, <span data-bind="text: country"></span></p>
</script>

<script type="text/javascript">
function Address(region, country) {
  this.region = region;
  this.country = country;
}
function Person(name, address){
  this.name = name;
  this.address = address;
}
function ViewModel() {
  this.people = ko.observableArray([
    new Person("Bob", new Address("Kyoto", "Japan")),
    new Person("Mike", new Address("Tokyo", "Japan")),
    new Person("Katherine", new Address("New York", "America"))
  ]);
}
ko.applyBindings(new ViewModel());
</script>

as は foreach バインディング内で使う $parent のバインディングコンテキストのかっこいい書き方です。

公式サイトのサンプルはこんな感じです。

http://jsfiddle.net/tan_go238/70pd11kp/

<ul data-bind="template: { name: 'seasonTemplate', foreach: seasons, as: 'season' }"></ul>
 
<script type="text/html" id="seasonTemplate">
    <li>
        <strong data-bind="text: name"></strong>
        <ul data-bind="template: { name: 'monthTemplate', foreach: months, as: 'month' }"></ul>
    </li>
</script>
 
<script type="text/html" id="monthTemplate">
    <li>
        <span data-bind="text: month"></span>
        is in
        <span data-bind="text: season.name"></span>
    </li>
</script>
 
<script>
    var viewModel = {
        seasons: ko.observableArray([
            { name: 'Spring', months: [ 'March', 'April', 'May' ] },
            { name: 'Summer', months: [ 'June', 'July', 'August' ] },
            { name: 'Autumn', months: [ 'September', 'October', 'November' ] },
            { name: 'Winter', months: [ 'December', 'January', 'February' ] }
        ])
    };
    ko.applyBindings(viewModel);
</script>

ネストしたテンプレート内で親の変数が取得できます。

ではもうちょっと頑張って上のサンプルをもうひとつネストして子の子の子のテンプレートから同じように取得できるか試してみましょう。

http://jsfiddle.net/tan_go238/70pd11kp/1/

<ul data-bind="template: { name: 'seasonTemplate', foreach: seasons, as: 'season' }"></ul>
 
<script type="text/html" id="seasonTemplate">
    <li>
        <strong data-bind="text: name"></strong>
        <ul data-bind="template: { name: 'monthTemplate', foreach: months, as: 'month' }"></ul>
    </li>
</script>
 
<script type="text/html" id="monthTemplate">
    <li>
        <span data-bind="template: { name: 'detailTemplate', data: month }"></span>
    </li>
</script>

<script type="text/html" id="detailTemplate">
    <li>
        <span data-bind="text: name"></span>
        is in
        <span data-bind="text: season.name"></span>
    </li>
</script>
<script>
var viewModel = {
    seasons: ko.observableArray([
        { name: 'Spring', months: [ {name:'March'}, {name:'April'}, {name:'May'} ] },
        { name: 'Summer', months: [ {name:'June'}, {name:'July'}, {name:'August'} ] },
        { name: 'Autumn', months: [ {name:'September'}, {name:'October'}, {name:'November'} ] },
        { name: 'Winter', months: [ {name:'December'}, {name:'January'}, {name:'February'} ] }
    ])
};
ko.applyBindings(viewModel);
</script>

動作しました。asを使うといくつネストしようが親のプロパティの値を同じようにして取得できるみたいですね。

長くなってきましたので最後は一気にいきます。

afterRender, afterAdd, beforeRemove

これはテンプレート化したHTMLがレンダリングされたとき(afterRender)や、
foreachで追加されたとき(afterAdd)や削除された前(beforeRemove)に呼び出されるメソッドを指定する感じです。

http://jsfiddle.net/tan_go238/qncja7b8/1/

<h2>Participants</h2>
Here are the participants:<br>
<input type="checkbox" data-bind="checked: show1"> <br>
<input type="checkbox" data-bind="checked: show2"> <br>
<div data-bind="template: { name: 'person-template', data: buyer, if: show1 , afterRender: rendered }"></div>
<div data-bind="template: { name: 'person-template', data: seller, if: show2 }"></div>
 
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>

<script>
function MyViewModel() {
    this.rendered = function(){ alert("rendered!") };
    this.show1 = ko.observable();
    this.show2 = ko.observable();
    this.buyer = { name: 'Franklin', credits: 250 };
    this.seller = { name: 'Mario', credits: 5800 };
}
ko.applyBindings(new MyViewModel());
</script>


今日は templateバインディングを紹介しました。

templateバインディングはデフォルトで ko.nativeTemplateEngine を使用してレンダリングします。
もし jquery.tmpl を読み込んでいたら jquery.tmpl 用のテンプレートエンジン(ko.jqueryTmplTemplateEngine)を使用します。
※判定方法は ko.jqueryTmplTemplateEngine の jQueryTmplVersion あたりを参考にしてください。




明日は @isoden_ さんの「案件で使ってみてあーだったこーだった的な話です」です!
よろしくおねがいします!

カスタムバインディングの作り方

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


KnockoutJS Advent Calendar 2014 - Qiita


KnockoutJSではすでに組み込まれているvalueバインディングやclickバインディングなど以外でも
自分でバインディングを作成することができます。

作り方は公式サイトにあります。
Knockout : Creating custom bindings


使い方は公式サイトのとおりです。


こういうふうに定義すると・・・・

ko.bindingHandlers.yourBindingName = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called when the binding is first applied to an element
        // Set up any initial state, event handlers, etc. here
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called once when the binding is first applied to an element,
        // and again whenever any observables/computeds that are accessed change
        // Update the DOM element based on the supplied values here.
    }
};


こういうふうに使用できます。

<div data-bind="yourBindingName: someValue"> </div>


カスタムバインディングでは init と update のメソッドがあり、
初回時のロードで init 、 カスタムバインディングに適用した要素や依存しているobservables/computedsに更新があれば updateが実行されます。

よく使うメソッドの引数は以下のような感じです。

element
要素自体が渡されます。

valueAccessor
この要素にバインドされているViewModelのプロパティをメソッドで渡されます。
データを取得するには valueAccessor() のようにして取得します。

allBindings
この要素に対してdata-bindで渡されたすべてのデータが渡されます。


基本的な使い方は以上ですが、カスタムバインディングの実行前に実行されるメソッドも定義することができます。

ko.bindingHandlers.<name>.preprocess(value, name, addBindingCallback)

要素が使用されなくなったときに破棄したい場合は以下を使用すると良いです。

ko.utils.domNodeDisposal.addDisposeCallback(node, callback)

公式ドキュメントではそれぞれここに記載されています。
Knockout : Extending Knockout's binding syntax using preprocessing

Knockout : Custom disposal logic



最後に、だいぶ汚いコードですがサンプル書いてみました。
BootstrapのPopoverを使ってメモボタンをカスタムバインディングで書いてみました。


Examples

<a href="#" class="memo-btn" data-bind="memo:{ editable:true, target: 'memo1', content: memo1()}" data-placement="right" tabindex="-1" role="button">メモ</a>
<input type="hidden" name="memo1" data-bind="value: memo1"/>

上記のように memo というバインディングハンドラを書いて中にオブジェクトで設定を渡しています。
1つ目の引数 "editable" は編集可能かどうか
2つめの引数 "targe" は保存時に更新するViewModelのプロパティ名
3つめの引数 "content" はPopoverに表示する値
のようにしています。

ちょっとした応用ですがこのように引数をオブジェクトで渡すことで設定値とかも渡せるので便利です。
また3.2では ここ で書いた Componentsを使うとさらに簡潔に書くことができます。

明日も自分です。書き溜めてた分がなくなったので頑張ります。

Yeomanを使ってKnockoutのプロジェクトを作成する

今回は Yeoman、 Gulp、Karma を使ってKnockoutJSのプロジェクトを作成しようと思います。

元ネタは作者のブログのここの動画です。

Steve Sanderson - Architecting large Single Page Applications with Knockout.js on Vimeo


この動画の内容を今回のアドベントカレンダーで何回かに分けて紹介したいと思います。

下準備

まず yeoman を以下のコマンドでインストールします。

npm install -g yo

つぎに KnockoutJSを使ったプロジェクトを生成するために generator-ko を以下のコマンドでインストールします。

npm install -g generator-ko

基本これでOKですが、ローカルで簡単に確認するためのWebサーバがさくっと用意できない人も多いと思いますので、簡易Webサーバを用意しておきましょう。

つぎのコマンドでインストールします。(このWebサーバの起動方法は後述)

npm install -g http-server

Scaffolding

ではプロジェクトの雛形を生成します。このように雛形を生成することをScaffoldingと呼びます。

最初にプロジェクトのディレクトリを作成し、以下の yo コマンドを実行します。

mkdir TVGuide
cd TVGuide
yo ko 

すると以下のような画面になるので、気にせず全部エンターキーを押しましょう。

f:id:tan_go238:20141107005328p:plain


すると以下のような構造でファイルが作成されます。

f:id:tan_go238:20141107010731p:plain


プロジェクトの雛形が生成されたら、Webサーバを起動してブラウザで確認してみましょう。

以下のコマンドをプロジェクトの一番トップのディレクトリで実行します。

% pwd
/path/to/TVGuide    // プロジェクトの一番上のディレクトリ

% http-server src
Starting up http-server, serving src on: http://0.0.0.0:8080


ブラウザを開き以下のように表示されたら成功です。

f:id:tan_go238:20141107011254p:plain

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 開発」です!
よろしくお願いします!

ko.utils(その他)

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


KnockoutJS Advent Calendar 2014 - Qiita



今日は Knockout の便利機能が入った ko.utils について適当に紹介します。

前回は配列だったのでそれ以外を紹介します。

ソースコードはここ。

https://github.com/knockout/knockout/blob/master/src/utils.js


紹介するのは以下のメソッドです。

  • ko.utils.extend
  • ko.utils.fieldsIncludedWithJsonPost
  • ko.utils.getFormFields
  • ko.utils.peekObservable
  • ko.utils.postJson
  • ko.utils.postJson
  • ko.utils.parseJson
  • ko.utils.stringifyJson
  • ko.utils.range
  • ko.utils.registerEventHandler
  • ko.utils.triggerEvent
  • ko.utils.unwrapObservable
  • ko.utils.objectForEach
  • ko.utils.toggleDomNodeCssClass
  • ko.utils.addOrRemoveItem


それでは順番にいきましょう。

  • ko.utils.extend

Objectを拡張します。

サンプル

var target = {a:1, b:2}
var option = {d:3, e:4, f:5, a:6}
var result = ko.utils.extend(target, option);
console.log(result);

結果

Object {a: 1, b: 2, d: 3, e: 4, f: 5}
  • ko.utils.fieldsIncludedWithJsonPost

これはメソッドではなく配列です。あとで出てくる ko.utils.postJson のデータを送信するときに自動的に含めるフィールド名が入ってます。

fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],

軽くググった感じだとRailsCSRF対策で自動的に埋め込まれるトークン用、もう一つは ASP.NETでリクエストの検証トークン用っぽいです。

  • ko.utils.getFormFields

フォームから指定した文字列もしくは正規表現に一致するフィールドを配列で返します。

HTML

<form id="form1" action="/" method="post">
  <input type="text" name="f1" value="" />
  <input type="checkbox" name="f2" value="" />
</form>
var form = document.getElementById("form1");
var result1 = ko.utils.getFormFields(form, "f2");
console.log("result1 length: " + result1.length);
console.log("result1: " + result1[0].getAttribute("name"));
var result2 = ko.utils.getFormFields(form, /f[12]/);
console.log("result2 length: " + result2.length);
console.log("result2: " + result2[0].getAttribute("name"));
console.log("result2: " + result2[1].getAttribute("name"));

結果

result1 length: 1
result1: f2
result2 length: 2
result2: f2
result2: f1
  • ko.utils.peekObservable

ko.observableの peek() と同じです。

<div id="peekObservable">
<p>ko.utils.peekObservable</p>
<form id="form1" action="/" method="post">
  <input type="text" name="f1" value="" data-bind="value: input1"/>
  <input type="text" name="f2" value="" data-bind="value: input2"/>
  <p data-bind="text: result"></p>
  <input type="checkbox" name="f3" value="1" data-bind="checked: input3" id="chk"/><label for="chk">ここを押せば更新される</label>
</form>
<script>
function ViewModel() {
  var self = this;
  self.input1 = ko.observable();
  self.input2 = "Input 2";
  self.input3 = ko.observable(false);
  self.result = ko.pureComputed(function(){
    var i1 = ko.utils.peekObservable(self.input1); // self.input.peek() と同じ
    var i2 = ko.utils.peekObservable(self.input2); // observableじゃなくても使える(無視される)
    var checked = self.input3();
    return i1 + " - " + i2 + " " + checked;
  });
}
ko.applyBindings(new ViewModel(), document.getElementById('peekObservable'));
</script>
</div>
  • ko.utils.postJson

ko.utils.postJson(urlOrForm, data, options)

urlOrForm
 URLかフォーム要素をいれる
data
 送信するデータ。Modelでもいいけどprototypeで継承してきたプロパティも送信するってソースに書いてる
 送信する値は ko.utils.stringifyJson で JSON化される
options
 オブジェクトで指定する。配列でもいけるかもしれないけど試してない。
 指定できるオプションは次のとおり
  params
   送信するパラメータ。dataとほぼ一緒だけどこちらは JSON化しない
  includeFields
   送信する対象のフィールドを配列で指定する
   指定がない場合は自動的に以下のフィールドが指定される(CSRF対策のため)
    authenticity_token
    __RequestVerificationToken_ から始まるフィールド
  submitter
   これに内部で作成したフォームを引数として渡して実行する

<form id="sendForm" action="/">
<input type="text" name="includeField1" value="1" />
<input type="text" name="includeField2" value="2" />
<input type="text" name="excludeField1" value="3" />
<input type="text" name="excludeField2" value="4" />
<input type="text" name="otherField" value="5" />
</form>
<script>
var model = {a:{b:"bb",c:["cc1","cc2"]}, d:["ddd"]};
var mySubmitter = function(data){ console.log(data); };
ko.utils.postJson(document.getElementById('sendForm'), model, {params: {x:'xx', y:'yy'}, includeFields: [/includeField[0-9]/, 'otherField'], submitter: mySubmitter});
</script>

結果(内部で生成されてPOSTされるフォーム)

<form action="http://localhost:8080/" method="post" style="display: none;">
    <input type="hidden" name="a" value="{"b":"bb","c":["cc1","cc2"]}">
    <input type="hidden" name="d" value="["ddd"]">
    <input type="hidden" name="x" value="xx">
    <input type="hidden" name="y" value="yy">
    <input type="hidden" name="otherField" value="5">
    <input type="hidden" name="includeField1" value="1">
    <input type="hidden" name="includeField2" value="2">
</form>
  • ko.utils.parseJson

内部的に JSON.parse が呼ばれるのでほぼ同じものとして利用すればよい。
違いは引数をJSON.parseする前にトリムすることと、JSON.parseに対応していない古いブラウザにも対応していること
古いブラウザはあんまり安全でない(Fallback on less safe)と書いてある

  • ko.utils.stringifyJson

JSON.stringifyを内部で呼び出すのでほぼ同じものとして利用すればよい
違いは IE8より前だとエラーをメッセージ付きで投げることと、
JSON.stringifyに渡すJavaScriptオブジェクトを ko.utils.unwrapObservableしてから渡す

  • ko.utils.range

ko.utils.range(min, max)
最小(min)から最大(max)までの範囲の値を配列にして返します

var result = ko.utils.range(2, 5);
console.log(result);

結果

[2, 3, 4, 5]
  • ko.utils.registerEventHandler

要素にイベントハンドラを登録できる

<button id="btnClick">Click</button>
<script>
console.log("ko.utils.registerEventHandler");
ko.utils.registerEventHandler(document.getElementById('btnClick'), 'click', function(){
  alert('clicked!');
});
</script>
  • ko.utils.triggerEvent

イベントをディスパッチできる。

<button id="btnClick">Click</button>
<script>
console.log("ko.utils.registerEventHandler");
ko.utils.registerEventHandler(document.getElementById('btnClick'), 'click', function(){
  alert('clicked!');
});
ko.utils.triggerEvent(document.getElementById('btnClick'), 'click');
</script>
  • ko.utils.unwrapObservable

ko.unwrapはこれのショートカット。observableは値を取り出すときに unwrapしないと取り出せない。
とはいえ observable でないものを手動で unwrap する判断が間違え易いのでこのメソッドがあると思ってる。
例えば
self.a = ko.observable();
で宣言した変数の値を取り出すときは
self.a()
としてやらないといけないが、
self.b = 1;
で宣言した変数の値を取り出すときに
self.b();
とするとエラーになるため。

  • ko.utils.objectForEach

オブジェクトのプロパティをぐるぐるする

<script>
var obj = {a:1, b:2, c:3};
ko.utils.objectForEach(obj, function(item, index){
  console.log(index + ":" + item);
});
</script>

結果

1:a
2:b
3:c
  • ko.utils.toggleDomNodeCssClass

jQueryでいう removeClass や addClass ができる

jQuery

$(element).addClass(value);
$(element).removeClass(value)

KnockoutJS

ko.utils.toggleDomNodeCssClass(element, value, true);
ko.utils.toggleDomNodeCssClass(element, value, false);
<style>
.add {
  color: red;
}
.remove {
  text-decoration:line-through;
}
</style>
<span id="myText" class="remove">Hello</span>
<script>
ko.utils.toggleDomNodeCssClass(document.getElementById('myText'), 'add', true);
ko.utils.toggleDomNodeCssClass(document.getElementById('myText'), 'remove', false);
</script>
  • ko.utils.addOrRemoveItem

配列に特定の要素を追加もしくは削除する

ko.utils.toggleDomNodeCssClass の内部や
"checked" のバインディングハンドラでモデルが更新されたときにチェックボックスを更新するのに使われている

APIは以下のようになる
ko.utils.addOrRemoveItem(array, value, included)

サンプル

var arr = ["aaa","bbb"];
ko.utils.addOrRemoveItem(arr, "ccc", true);
console.log(arr);
ko.utils.addOrRemoveItem(arr, "bbb", false);
console.log(arr);

結果

["aaa", "bbb", "ccc"]
["aaa", "ccc"]


明日は @hkusu_ さんの 「ko.editables で入力をロールパックしてみる」です!
よろしくお願いします!