モジュールの公開 I/F に Bacon.Bus 便利っぽい

続・Bacon.js

ある処理を連結した EventStream に外から値を流し込んだり、動作の起点になるユーザーアクション(event)をつなぎたいときの話。全体からの抜粋コードで説明してるので、雑な感じなのはご容赦願いたい。

Before

スライドのコントロールをしてくれるモジュールで、適当な EventStream や Property を公開していたのですが肝心のスライドを next, prev させるユーザー操作を外に出せていなかった。

// スライドのページ送りとかをしてくれる
// 公開している EventStream は現在のページ番号表示とかを更新するためのもの
export default function(options) {

  let right = control.key('right');
  let left  = control.key('left');

  right = right.merge(control.click(options.nextButton));
  left  = left.merge(control.click(options.prevButton));

  let next = right.map(1);
  let prev = left.map(-1);

  let initialPage = options.startPage || 1;
  let correctPage = util.compose(inRangeOf(1, options.endPage), add);

  let both    = next.merge(prev);
  let current = both.scan(initialPage, correctPage).skipDuplicates();

  Bacon.combineAsArray(current, options.slideElements).onValue(function(data) {
      let [current, all] = data;
      all.forEach(toInvisible);
      toVisible(all[current - 1 /* fix page to index */]);
    });

  return {
    current : current,
    start   : current.filter((v) => v === 1),
    end     : current.filter((v) => v === options.endPage),
    onNext  : next,
    onPrev  : prev
  };
}

というのも Bacon.fromEventTarget で作成したユーザーアクションに紐付く EventStream に直接それ以降の処理をつないでいたから。これだと、ユーザーアクションの EventStream ありきでしか書けない。

After

ということで、次のように new Bacon.Bus() で生成した bus オブジェクトを公開するように修正した。

export default function(options) {

  let nextBus    = new Bacon.Bus();
  let prevBus    = new Bacon.Bus();

  let nextEs = nextBus.map(1);
  let prevEs = prevBus.map(-1);

  let initialPage = options.startPage || 1;
  let correctPage = util.compose(inRangeOf(1, options.endPage), add);

  let bothEs  = nextEs.merge(prevEs);
  let current = bothEs.scan(initialPage, correctPage).skipDuplicates();

  Bacon.combineAsArray(current, options.slideElements)
    .onValue(function(data) {
      let [current, all] = data;
      all.forEach(toInvisible);
      toVisible(all[current - 1 /* fix page to index */]);
    });

  return {
    currentEs : current,
    nextBus   : nextBus,
    prevBus   : prevBus
  };
}

bus は外から別のイベントストリームをつないだり ( bus.plug(eventStream) ) 、任意の値をストリームに流す ( bus.push(value) ) ことができる。

Usage

paging モジュールが公開している nextBusprevBus に、キーボード操作の EventStream を後から bus.plug() している。こうしておけば他のキーやUIのクリックなどを後付けするのも容易だ。

import Paging from './paging';
import control from './control';

let paging = Paging({
  startPage     : params.startPage || 1,
  endPage       : slides.length,
  slideElements : toArray(document.querySelectorAll(`.slide`))
});

paging.nextBus.plug(control.key('right'));
paging.prevBus.plug(control.key('left'));

paging.nextBus.plug(control.click(document.getElementById('next')));
paging.prevBus.plug(control.click(document.getElementById('prev')));

補足しておくと、ここで併用している control モジュールは次のような感じ。

'use strict';
import Bacon   from 'baconjs';
import keycode from 'keycode';
const EVENT_KEYUP = Bacon.fromEventTarget(document, 'keyup');
/**
 * create EventStream from user input
 */
export default {
  /**
   * @param {String|Number} charKey
   * @returns {EventStream}
   */
  key(charKey) {
    let keyCode = typeof(charKey) === 'string' ? keycode(charKey)
                                               : charKey;
    return EVENT_KEYUP.filter(keyCodeIs(keyCode));
  },
  /**
   * @param {Element} el
   * @returns {EventStream}
   */
  click(el) {
    return Bacon.fromEventTarget(el, 'click');
  }
};
/**
 * @param {Number} keyCode
 * @returns {Function}
 */
function keyCodeIs(keyCode) {
  return function(event) {
    return event.keyCode === keyCode;
  };
}

Bacon.Bus

Bus is an EventStream that allows you to push values into the stream. It also allows plugging other streams into the Bus. The Bus practically merges all plugged-in streams and the values pushed using the push method. Bacon.js - API reference

とのことであります。 Ajax とか他の非同期処理の結果をあとから bus.push() して EventStream を流す、とかにも使える感じなのでマジメに使おうと思うと必須っぽい。