Matt-Esch/virtual-domで独自イベントを利用する方法


## 定義

この投稿で言う「独自イベント」とは、「カスタムイベント」のことではありません。

「カスタムイベント」は独自の名前のイベントを`addEventListener`で設定し、`dispatchEvent`でそのイベントを発火させることで動作させるものです。

ここでいう「独自イベント」は、実際のDOM要素を操作し擬似的にイベントを再現するものを意味しています。
例えば、私の書いた[inputイベントのクロスブラウザコード](http://qiita.com/sounisi5011/items/767392c1f7736e2e5d4c)は、内部的には`focus`, `blur`, `selectionchange`, `input`等の様々なイベントを要素そのものに設定することで、`input`イベントと同等の機能を再現しています。
他には[Hammer.js]も、内部的には`tap`というイベントそのものは設定せず、`mousedown`や`touchstart`等のイベントを要素そのものに設定することでtapイベントを再現しています。

本投稿ではこのような、DOM要素そのものが必要な独自イベント設定コードと[Matt-Esch/virtual-dom]を共存させる方法について書きます。

## 注意

例示するコードがややこしくなるため、Internet Explorer 8以下には対応させていません。

## 概要

[Matt-Esch/virtual-dom]は、昨今話題となっているVirtual DOMの軽量な実装の1つです。
以下の投稿でも紹介されています。

[JavaScript - 仮想DOMライブラリの「virtual-dom」だけでMV*なビューを書く - Qiita](http://qiita.com/seanchas_t/items/05d70bf4e095865583cf)

このライブラリで要素にイベントを設定する場合、`onclick`のようなイベント名で利用するか、または[Raynos/dom-delegator](https://github.com/Raynos/dom-delegator)と合わせて`ev-click`のように定義する必要があります。

[ev-click event binding not working, what dependencies do I need? (dom-delegator? value-event?) · Issue #166 · Matt-Esch/virtual-dom](https://github.com/Matt-Esch/virtual-dom/issues/166#issuecomment-75493057)

単体のイベントのみを設定するならこれでも問題はありません。
むしろ、一々`addEventListener`メソッドなんかを書くよりもずっと分かりやすいです。

## 問題

しかし、DOM要素に直接複数のイベントを設定する必要があるライブラリ等を利用する場合はどのようにすれば良いのでしょうか?

例えば、冒頭の[定義](#%E5%AE%9A%E7%BE%A9)でも紹介した[Hammer.js]を利用する場合にこの問題が発生します。
[Hammer.js]で`tap`イベントを検出する場合、以下のように記述します。

```js
// タップを検出する要素
var targetElement = document.getElementById('target');

var mc = new Hammer(targetElement);

mc.on('tap', function (ev) {
    // タップされた場合の処理
});
```

DOM Nodeそのものを引数に使用しています。
内部的には、`mousedown`や`touchstart`等のイベントを要素に設定することで`tap`イベントを再現しています。
これを[Matt-Esch/virtual-dom]で利用するにはどうすれば良いのでしょうか?

## Hooks

[Matt-Esch/virtual-dom]には、[Hooks]というものが存在します。
これは、[Matt-Esch/virtual-dom]が内部で生成するDOM Nodeを直接取得できるものです。
(英語が読めないため正しい理解は出来ていませんが、おおまかには合っているハズです)
これを利用することで、DOM Nodeを要する独自イベントを[Matt-Esch/virtual-dom]でも利用できるようになります。

ただし、この[Hooks]はDOMを更新する度に呼び出されます。
このため、単にイベントを設定してしまうと次々にイベントが定義され、重くなってしまいます。
これを防ぐため、第三引数を利用して初回のみイベントを定義するようにします。

# コード

```js
var h = require('virtual-dom/h');
var diff = require('virtual-dom/diff');
var patch = require('virtual-dom/patch');
var createElement = require('virtual-dom/create-element');
var Hammer = require('hammerjs');
var requestAnimationFrame = require('raf');

/**
 * イベント用のHook、`EvHook`を定義
 */
function EvHook(listener) {
    this.listener = listener;
}

EvHook.prototype.hook = function (node, propertyName, previousValue) {
    if (!previousValue) {
        /**
         * 前回の値が未定義 = 初回のDOM生成時のみ、イベントを定義する
         */
        var type = propertyName.substr(2);
        var listener = this.listener;

        if (type === 'tap') {
            /**
             * tapイベントの場合、Hammer.jsを使用する
             */
            var mc = new Hammer(node, {
                // Hammer.jsが自動で追加するCSSプロパティを無効化
                touchAction: '',
                cssProps: {}
            });
            mc.on('tap', listener);
        } else {
            /**
             * それ以外の場合、addEventListenerメソッドで定義する
             */
            node.addEventListener(type, listener, false);
        }
    }
};

/**
 * 以下、virtual-domのコード
 */

var state = {
    value: 0
};

function incrementValue() {
    state.value = state.value + 1;
}

function render() {
    return h('div', [
        'The state ',
        h('code', 'tapCount'),
        ' has value: ' + state.value + '.',
        h('input.button', {
            type: 'button',
            value: 'Tap me!',
            'ontap': new EvHook(incrementValue) // EvHookを使用する
        })
    ]);
}

var tree = render();
var rootNode = createElement(tree);
document.body.appendChild(rootNode);

requestAnimationFrame(function tick() {
    var newTree = render();
    var patches = diff(tree, newTree);

    rootNode = patch(rootNode, patches);
    tree = newTree;

    requestAnimationFrame(tick);
});
```

[サンプル](http://requirebin.com/?gist=62acb030277849b10ff4)

[Matt-Esch/virtual-dom]:https://github.com/Matt-Esch/virtual-dom
[Hooks]:https://github.com/Matt-Esch/virtual-dom/blob/master/docs/hooks.md
[Hammer.js]:http://hammerjs.github.io/