Synthetic events are usually named abstractions that bind to existing DOM events to monitor user actions for specific patterns. However, at heart they are no more than a set of callbacks executed in response to various triggering methods in the DOM event system.
You can do all sorts of things with synthetic events, including:
-
redefine native DOM events that behave inconsistently across
browsers (e.g.
focusandblur) -
provide abstract events that attach to different DOM events based on
the environment (e.g.
gesturemovestartand family) -
create events with different subscription signatures (e.g.
hover) -
create configurable events that only execute subscribers when
criteria passed during subscription are met (e.g.
flickorkey) -
create events that encapsulate common UX patterns (e.g.
clickoutside) -
create fun little easter eggs (e.g.
konami) - and more...
The hooks
Synthetic events hook into the subscription binding and unbinding methods. Specifically:
node.on("eventName", ...),Y.on("eventName", ...), and familynode.delegate("eventName", ...)orY.delegate("eventName", ...)node.detach(...)orsubscription.detach()
With the exception of a separate detachDelegate() method, the names used when defining synthetic events are the same as these basic methods.
Y.Event.define("tripleclick", {
on: function (node, subscription, notifier) {
// called in response to individual subscriptions
},
delegate: function (node, subscription, notifier, filter) {
// called in response to delegate subscriptions
},
detach: function (node, subscription, notifier) {
// called when individual subscriptions are detached in any way
},
detachDelegate: function (node, subscription, notifier) {
// called when delegate subscriptions are detached in any way
}
});
Subscriptions and Notifiers
In addition to the subscribing Node, each method receives a
subscription and a notifier. Use the subscription
to store event handles or other data that may be needed by another method. Use
notifier.fire(e) to dispatch the event to the callbacks that were
bound to it.
Y.Event.define("tripleclick", {
on: function (node, subscription, notifier) {
var count = 0;
subscription._handle = node.on("click", function (e) {
if (++count === 3) {
// Call notifier.fire(e) to execute subscribers.
// Pass the triggering event facade to fire()
notifier.fire(e);
} else {
...
}
});
},
detach: function (node, subscription, notifier) {
subscription._handle.detach();
},
delegate: function (node, subscription, notifier, filter) { ... },
detachDelegate: function (node, subscription, notifier) { ... }
});
Subscribers to the synthetic event should receive a DOMEventFacade. The
easiest way to provide one is to pass the triggering DOM event's facade to
notifier.fire(e). The facade's e.type will be updated to the name of the
synth. You will also have the opportunity to add extra data to the event
before dispatching to the subscription callbacks.
Y.Event.define('multiclick', {
on: function (node, sub, notifier) {
var count = 0,
timer;
sub._handle = node.on('click', function (e) {
count++;
if (timer) {
timer.cancel();
}
timer = Y.later(200, null, function () {
e.clicks = count;
count = 0;
// subscribers will get e with e.type == 'multiclick'
// and extra property e.clicks
notifier.fire(e);
});
});
},
...
});
Delegation support
The delegate function implementation takes an extra argument, the filter that was passed node.delegate(type, callback, HERE). It's your responsibility to make sense of this filter for your event.
Typically, it is just passed along to a node.delegate(...) call against another event, deferring the filtration to the core delegate() method.
Y.Event.define("tripleclick", {
on: function (node, subscription, notifier) { ... },
detach: function (node, subscription, notifier) { ... },
delegate: function (node, subscription, notifier, filter) {
var activeNode = null,
count = 0,
timer;
subscription._handle = node.delegate("click", function (e) {
if (timer) {
timer.cancel();
}
if (this !== activeNode) {
activeNode = this;
count = 0;
}
if (++count === 3) {
// Call notifier.fire(e) just as with `on`
notifier.fire(e);
} else {
timer = Y.later(300, null, function () {
count = 0;
});
}
}, filter); // filter is passed on to the underlying `delegate()` call
},
detachDelegate: function (node, subscription, notifier) {
subscription._handle.detach();
}
});
Extra Arguments
Supply a processArgs method in the event definition to support a custom
subscription signature. The method receives two arguments:
- an array of the subscription arguments for analysis
-
a boolean
trueif the subscription is being made throughdelegate(...)
If this method is supplied, it
- MUST remove the extra arguments from the arg array that is passed in, and
- SHOULD return the extra data relevant to the subscription.
The same processArgs method is used by both on and delegate, but there
are various signatures to account for. The easiest way to accept extra
arguments is to require them from index 3 in the argument list. It's also best
to limit the number of extra arguments to one and require an object literal to
allow for future changes.
// for an event that takes one extra param
processArgs: function (args, isDelegate) {
var extra = args[3];
// remove the extra arguments from the array
args.splice(3,1);
return extra;
}
// for an event that takes three extra args
processArgs: function (args, isDelegate) {
return args.splice(3,3);
}
Requiring extra params start at index 3 of the args array results in the
following subscription signatures:
var extraConfig = { ... };
// Third argument for node.on() and node.delegate
node.on('extraArgEvent', callback, extraConfig, thisOverride, arg...);
node.delegate('extraArgEvent', callback, extraConfig, filter, thisOverride, arg...);
// Fourth argument for Y.on() and Y.delegate
Y.on('extraArgEvent', callback, targetSelector, extraConfig, thisOverride, arg...);
Y.delegate('extraArgEvent', callback, parentSelector, extraConfig, filter, thisOverride, arg...);
For some custom signatures, the placement of the extra argument for
implementers using Y.on() or Y.delegate() may look awkward. Sometimes you
can support extras at other indexes if you can reliably tell that the argument
is not part of
the extended
signature for on(...) or delegate(...). See the source for the "hover"
event for an example of supporting multiple signatures.
The return value of processArgs is assigned to subscription._extras for the on and delegate definition methods.
Y.Event.define('multiclick', {
processArgs: function (args, isDelegate) {
// The args list will look like this coming in:
// [ type, callback, node, (extras...), [filter,] thisObj, arg0...argN ]
return args.splice(3,1)[1] || {};
},
// Custom subscription signatures don't change the params of on/delegate
on: function (node, sub, notifier) {
var clicks = 0,
// data returned from processArgs is available at sub._extras
min = sub._extras.minClicks || 3,
max = sub._extras.maxClicks || 10,
timer;
sub._handle = node.on('click', function (e) {
if (timer) {
timer.cancel();
}
if (++clicks === max) {
e.clicks = clicks;
notifier.fire(e);
} else {
timer = Y.later(200, null, function () {
if (clicks > min) {
e.clicks = count;
notifier.fire(e);
}
count = 0;
});
}
});
},
...
});
Usage of this synthetic event then expects a third argument as a
configuration object with minClicks and maxClicks properties.
node.on('multiclick', obj.method, {
minClicks: 5,
maxClicks: 8
}, obj);
// extra args are supplied before the delegate filter
container.delegate('multiclick', doSomething, {
minClicks: 3,
maxClicks: 55
}, '.clickable');
A Tip to Make Your Synth Definition Smaller
If the only difference between your on and delegate definitions is which method is used to bind to the supporting events, then you can propably get away with defining delegate and aliasing it to on (and so with detach and detachDelegate). See the
source for the "hover"
event for an example of this approach.