D3.js で自作クラスにイベント発行機能を追加する

D3.js を使っていると、自作クラスのイベント発行も D3.js を使いたくなる。そんなときには d3.dispatch を使うとよい。

使う側の実装

イメージしやすいように、最初に使う側のコードを示しておく。

var myButton = new MyButton(d3.select("button"));
myButton.on("myclick", function(e) {
  alert(e.name); // MyEvent
  console.log(this, e); // MyButton, { name: "MyEvent", MouseEvent }
});

こんな感じで、自作の MyButton クラスで myclick イベントを発行したい。

MyButton の実装

では、さっそく MyButton の実装。最初はコンストラクターから。

function MyButton(selector) {
  // ボタンがクリックされたときに onClick を呼ぶ
  selector.on('click', this.onClick.bind(this));

  // myclick イベントを発行する dispatcher を作成
  this.dispatcher = d3.dispatch("myclick");
}

d3.dispatch の引数に「発行したいイベント名」を渡して、dispatcher オブジェクトを取得している。複数のイベントを出す場合は d3.dispatch("event1", "event2"); のようにする。

dispatcher オブジェクトには次の 2 種類のメソッドが定義される。

  • イベントを発行するためのメソッド (イベント名と同じ名前)
  • イベントを監視するための on(type, listener)

イベント発行処理

イベントの発行処理からみていこう。

MyButton.prototype.onClick = function() {
  this.dispatcher.myclick.call(this, {
    name: "MyEvent",
    event: d3.event
  });
};

ボタンがクリックされたときに myclick イベントを発行している。今回は myclick イベントと名付けたので、dispatcher.myclick() を実行すれば、myclick イベントが発火する。引数はそのままリスナーに渡される。

Function.call() を使っているのは、リスナー側で thisdispatcher ではなく MyButton にしたいから。

on の実装

次に MyButton.prototype.on() の実装。こちらは、単純に dispatcher.on() に中継している。

MyButton.prototype.on = function() {
  return this.dispatcher.on.apply(this.dispatcher, arguments);
};

単純に中継しているだけなので、もっと簡略化して書けそうである。そう、d3.rebind を使えばね。

function MyButton(selector) {
  selector.on('click', this.onClick.bind(this));
  this.dispatcher = d3.dispatch("myclick");

  // !!! ここが追加 !!!
  d3.rebind(this, this.dispatcher, "on");
}

コンストラクターに 1 行追加したおかげで、MyButton.prototype.on() が不要になった。コードは短くなったが、d3.rebind() の学習コストが増えたので微妙なところではある。

ちなみに、d3.rebind() は 11 行の短い関数。上で手で書いたのとだいたい同じ動作になるのが分かるはず。

d3.rebind = function(target, source) {
  var i = 1, n = arguments.length, method;
  while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]);
  return target;
};
function d3_rebind(target, source, method) {
  return function() {
    var value = method.apply(source, arguments);
    return value === source ? target : value;
  };
}

これで目的は完遂。めでたし。

完成後の全体のソースコードを貼っておく。

<!DOCTYPE html>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<body>
<button>click me</button>

<script>
function MyButton(selector) {
  selector.on('click', this.onClick.bind(this));
  this.dispatcher = d3.dispatch("myclick");
  d3.rebind(this, this.dispatcher, "on");
}

MyButton.prototype.onClick = function() {
  this.dispatcher.myclick.call(this, {
    name: "MyEvent",
    event: d3.event
  });
};

var myButton = new MyButton(d3.select("button"));
myButton.on("myclick", function(e) {
  alert(e.name); // MyEvent
  console.log(this, e); // MyButton, { name: "MyEvent", MouseEvent }
});
</script>
</body>

複数イベント登録の罠

D3.js のイベントで厄介な点は、1 つのイベント名に複数のイベントを登録できないところ。新しいイベントを登録したら、古いほうは消される…。DOM イベントと同じ感覚でいると混乱してしまう。

これを回避するには myclick.foomyclick.bar のように optional namespace をつけてイベント登録する必要がある。くわしくは selection.on を参照あれ。