Kefir.jsを使ったのは、ドキュメントがわかりやすくて綺麗だったからです。
ビューアーの要件
- NEXTクリックでページを進める。
- PREVクリックでページを戻す。
- $(“#page-count”)に、現在の総ページ数を表示する(非同期で増える)。
- $(“#page-no”)に、現在のページ番号を表示する。
- ただしページ番号は、総ページ数の値を超えることはできない。
実装方針
- 非同期で新しいページが出力されるたびに「現在の総ページ数」が出力されるストリーム
page_count_stream
を作る。
- 現在のページ数とNEXT/PREVクリックのアクションを合成して、現在のページ位置を出力するストリーム
page_index_stream
をる。
page_count_stream
を購読し、$(“#page-count”)を更新する。
page_index_stream
を購読し、$(“#screen”)に該当ページを出力しつつ、$(“#page-no”)にページ番号を出力する。
page_count_stream
とpage_index_stream
を宣言的に(ステートを持たないように)表現できたら成功。
ページ数のストリームを作成
nehan.jsが作ったページ数を受け取るストリームをKefir.bus()
で作成しておきます。
var page_count_stream = Kefir.bus();
ページ位置のストリームを作成
NEXT/PREVのクリックをそれぞれ+1
と-1
を出力するストリームにし、それらを一つに束ねたものをclick_value_stream
として定義します。
var next_click_stream = $("#next").asKefirStream("click");
var prev_click_stream = $("#prev").asKefirStream("click");
var click_value_stream = Kefir.merge([
next_click_stream.mapTo(1),
prev_click_stream.mapTo(-1)
]);
ここからは少し面倒です。
こういうnext/prevでカウントするサンプルって、大抵はストリームの値を合算したものを現在値みたいにするケースが殆どです。
var sum_stream = Kefir.scan(click_value_stream, function(acm, value){
return acm + value;
});
しかしこれだと上手くいかないのです。
なぜならこちらが欲しいのは、下限と上限付きの値だからです。
言うまでもなく、下限は先頭ページ位置の0
で、上限は「現在の」出力可能なページ数をマイナス1した値です。
ページ数が4(最大インデックス3)なのに、4とか5を出力されても困りますよね。
例えば2回だけ上限オーバーした数値は、PREVを2回押して戻さないと使えないってことになりますので。
だから単なる合算ではなく、現在のページ数を参照して合算するストリームが必要になります。
最初にミスったこと
これを実現するにあたって最初にミスったのはKefir.zip
を使って、クリックのストリームとページ数のストリームを合成することでした。
しかしこれは上手く行きません。
なぜならクリックのストリームがclickイベントが発生する度に出力する無限ストリームであるのに対し、ページ数のストリームは最終ページを出力したらもう新しい値を出力しない有限サイズのストリームだからです。
有限のストリームと無限のストリームをzipすると、短い方に合わせて有限のストリームが出来上がります。
つまりKefir.zip
を使って合成したストリームを購読しても、有限回しか操作できません。
こういう2つのストリームを一つの無限ストリームに束ねるにはどうしたらいいのでしょう。
解決策「Kefir.combine」
そこでドキュメントをざっと眺めて見つけたのがKefir.combine
です。
この関数は、対象ストリームの「最新の値」を使ってストリームを合成できます。
つまり片方の長さが3しかなくても、もう片方が無限なら、有限な方の最後の値が無限に参照できます。
結果、こんな実装になりました。
var page_index_stream = Kefir.combine([
click_value_stream,
page_count_stream
], function(value, count){
return {value:value, count:count};
}).scan(function(acm, cur){
return {
value:Math.max(0, Math.min(cur.count - 1, acm.value + cur.value)),
count:cur.count
};
}, {value:0, count:0}).map(function(obj){
return obj.value;
});
それぞれのストリームを購読
さて、なんとかpage_count_stream
とpage_index_stream
を定義することができました。
後は、page_count_stream
とpage_index_stream
をそれぞれ購読し、必要なUIを更新する処理を定義するだけです。
ページ出力のタイミングでpage_count_stream
に最新のページ数がemit
されるので、それに合わせて、紐付いたUIが連動して変化するようになっています。
var paged_element = Nehan.createPagedElement();
page_count_stream.onValue(function(count){
$("#page-count").html(count);
});
page_index_stream.onValue(function(index){
$("#page-no").html(index + 1);
paged_element.setPage(index);
});
paged_element.setStyle("body", {
"flow":"tb-rl",
"font-size":16,
"width":500,
"height":300
}).setContent($("#screen").html(), {
onProgress:function(tree){
page_count_stream.emit(tree.pageNo + 1);
}
});
$("#screen").after(paged_element.getElement());
感想
色々なUI要素が絡むような複雑なアプリなら、注目している値を透過的に取得できるので、安全にUIを更新できそうな気がしますが、今回のような単純なケースでは少し面倒かもしれません。