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 { // 0 ~ [cur.count - 1] 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); }); // UI操作とページ数によって決まる現在のページ位置を購読 page_index_stream.onValue(function(index){ $("#page-no").html(index + 1); paged_element.setPage(index); // 現在位置を更新 }); // ページ計算を開始 paged_element.setStyle("body", { "flow":"tb-rl", // or "lr-tb" "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を更新できそうな気がしますが、今回のような単純なケースでは少し面倒かもしれません。