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/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を省いた以下のようなコードでもちゃんと動作しました。
<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を使ってメモボタンをカスタムバインディングで書いてみました。
<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
すると以下のような画面になるので、気にせず全部エンターキーを押しましょう。
すると以下のような構造でファイルが作成されます。
プロジェクトの雛形が生成されたら、Webサーバを起動してブラウザで確認してみましょう。
以下のコマンドをプロジェクトの一番トップのディレクトリで実行します。
% pwd /path/to/TVGuide // プロジェクトの一番上のディレクトリ % http-server src Starting up http-server, serving src on: http://0.0.0.0:8080
ブラウザを開き以下のように表示されたら成功です。
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(_.*)?$/],
軽くググった感じだとRailsのCSRF対策で自動的に埋め込まれるトークン用、もう一つは 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 ができる
$(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 で入力をロールパックしてみる」です!
よろしくお願いします!