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.
focus
andblur
) -
provide abstract events that attach to different DOM events based on
the environment (e.g.
gesturemovestart
and 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.
flick
orkey
) -
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
true
if 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.