JavaScript
virtual-dom

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

More than 3 years have passed since last update.


定義

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

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

ここでいう「独自イベント」は、実際のDOM要素を操作し擬似的にイベントを再現するものを意味しています。

例えば、私の書いたinputイベントのクロスブラウザコードは、内部的にはfocus, blur, selectionchange, input等の様々なイベントを要素そのものに設定することで、inputイベントと同等の機能を再現しています。

他にはHammer.jsも、内部的にはtapというイベントそのものは設定せず、mousedowntouchstart等のイベントを要素そのものに設定することでtapイベントを再現しています。

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


注意

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


概要

Matt-Esch/virtual-domは、昨今話題となっているVirtual DOMの軽量な実装の1つです。

以下の投稿でも紹介されています。

JavaScript - 仮想DOMライブラリの「virtual-dom」だけでMV*なビューを書く - Qiita

このライブラリで要素にイベントを設定する場合、onclickのようなイベント名で利用するか、または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

単体のイベントのみを設定するならこれでも問題はありません。

むしろ、一々addEventListenerメソッドなんかを書くよりもずっと分かりやすいです。


問題

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

例えば、冒頭の定義でも紹介したHammer.jsを利用する場合にこの問題が発生します。

Hammer.jstapイベントを検出する場合、以下のように記述します。

// タップを検出する要素

var targetElement = document.getElementById('target');

var mc = new Hammer(targetElement);

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

DOM Nodeそのものを引数に使用しています。

内部的には、mousedowntouchstart等のイベントを要素に設定することで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を更新する度に呼び出されます。

このため、単にイベントを設定してしまうと次々にイベントが定義され、重くなってしまいます。

これを防ぐため、第三引数を利用して初回のみイベントを定義するようにします。


コード

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);
});

サンプル