#

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.