Jump to Table of Contents

Creating Synthetic Events

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 and blur)
  • 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 or key)
  • 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:

  1. node.on("eventName", ...), Y.on("eventName", ...), and family
  2. node.delegate("eventName", ...) or Y.delegate("eventName", ...)
  3. node.detach(...) or subscription.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:

  1. an array of the subscription arguments for analysis
  2. a boolean true if the subscription is being made through delegate(...)

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.