タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

RxJSでリスト要素絞り込み操作を簡単に実装する

Reactive-Extensions/RxJS · GitHub というものがあります。
詳しいところはGitHubのREADMEとかを読んでもらうとして、RxJSはReactiveExtensionという、'.NET上でLINQ風にReactiveProgrammingを行えるようにする' ライブラリのJavaScript版実装です。

僕の記憶だと、2年くらい前はメソッド名がC#風のままだったりしててちょっとアレな感じだったのですけれども、この前見てみたらSelect -> map というように、より一般的な命名に変更になってました。

こんなことを説明しても、知らん人にはなんのこっちゃって感じだとおもうので、ざっと説明しますと、非同期なイベントストリームをメソッドチェインで、あたかも同期的な見た目で記述出来るようにしてくれるライブラリです。多分。僕もまださわりくらいしかわかってません。

リアクティブプログラミングについてよく知りたいという向きは、以下の素敵な翻訳記事があるので眺めてみるのがいいとおもいます。


【翻訳】あなたが求めていたリアクティブプログラミング入門 - ninjinkun's diary   

というわけで、実際にデモを動かしてみませう。
実際に動作させているコードはgistsにあげております。 https://gist.github.com/hachibeeDI/483408f2639725b546f2

今回実装するものの要件は

  • テキストフィールドに入力した文字でリストを絞り込みたい(pecoみたいな感じの動作)
  • ただし入力中に常に動作させるとうっとおしいので、入力が落ち着いてから絞り込む
  • カーソル移動は無視する
  • 後で、非同期通信による自動更新とかもしたいので拡張性高めでよろしく

そんな感じです。
JavaScript部分の実装は以下みたいな感じになります。

   <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/rxjs/2.3.5/rx.all.min.js" async></script>
    <script type="text/javascript">
var _turnObjToArray = function(obj) {
  return [].map.call(obj, function(element) {
    return element;
  })
};
 
 
var main = function main() {
  var query_field = document.getElementById('query-field');
  var query_targets = _turnObjToArray(document.querySelectorAll('.query-target'));
  var unhideAllTargets = function unhideAllTargets() {
    query_targets.map(function(node) { node.classList.remove('hidden'); });
  };
  var hideTarget = function hideTarget(node) { node.classList.add('hidden'); };
 
  var query_field_observer = Rx.Observable.fromEvent(query_field, 'keyup');
  query_field_observer
    .map(function(e) { return e.target.value; })
    .throttle(300)
    .distinctUntilChanged()
    .filter(function(txt) {
      if (txt.length == 0) { unhideAllTargets(); }
      return txt.length >= 2;
    })
    .subscribe(function(txt) {
      var matcher = new RegExp(".*" + txt + ".*");
      query_targets
        .filter(function(node) { return ! node.textContent.match(matcher); })
        .map(hideTarget);
    });
};
 
window.addEventListener('load', main);
    </script>

一つづつ説明します。

まずはcdnからrx.jsのコードをロードしています。
_turnObjToArray関数ですが、これはNodeListでもfilterを使えるようにするためのヘルパーです。jQueryとかunderscore.jsとか使えば多分これいらないですが、今回は混乱をさけるためにRxJS以外のライブラリは使いません。

次に、イベントを監視するためのテキストボックスと、絞り込み対象となるliエレメントを取得しています。ついでに、エレメントを隠すための関数と元に戻すためのヘルパー関数も分離して書いておきます。
インラインでの書き込みではなく、クラスの付与でエレメントを隠しているので、対応するcssが必要になることに注意してください。

var query_field_observer = Rx.Observable.fromEvent(query_field, 'keyup');
ここで、query_fieldkeyupイベントを監視することを宣言しています。Rx.Observableというところからもわかるように、RxJSは基本的にObserverパターンを用いてイベントなどの監視を行います。

.map(function(e) { return e.target.value; })
C#だとSelectですね。keyupイベントのストリームを監視して、イベントごとにTextBoxの中身を流しています。

.throttle(300)
.distinctUntilChanged()
throttleはRxJSが用意してくれている便利メソッドで、イベントの起きる間隔が指定した値(ミリ秒)以上空くまで待機してくれています。今回みたいな実装だと恩恵は薄めですが、イベントを引き金に非同期通信を行う場合などに有効でしょう。
distinctUntilChangedも同様で、こちらはメソッド名の通り、前のストリームから流れてくる値の中身が変わるまで無視してくれます。こいつのおかげで、カーソル移動などの操作から生じるイベントは無視されます。

    .filter(function(txt) {
      if (txt.length == 0) { unhideAllTargets(); }
      return txt.length >= 2;
    })

テキストの入力が空であれば、全てのリストを元に戻します。
また、二文字以下であればそのイベントを無視します。

    .subscribe(function(txt) {
      var matcher = new RegExp(".*" + txt + ".*");
      query_targets
        .filter(function(node) { return ! node.textContent.match(matcher); })
        .map(hideTarget);
    });

subscribeは、foreachみたいな感じでイベントストリームに対してコールバックの中身を適用していきます。
ここで絞り込み操作を行っています。

こんな感じで、単純なようで結構めんどくさい要件も超簡単に実装できましたね。
コードも、「何をしているか」ではなく「何をしたいか」に集中して書かれているので可読性も高く、今後しばらくは仕様の変更や追加に耐えられることでしょう。

とまぁ、こんな感じで便利っぽいし、AngularJSとかみたいな他のライブラリとも共存して使えるように頑張ってるとかいう感じみたいなのでしばらく使ってみようかなと思います。日本語の情報がほぼないに等しいのがちょっと悲しいですが。
あと僕は片手間JSerなので、本記事に対するツッコミとかは大歓迎なのであります。

以上。