39

Is there a way to attach a jQuery event handler such that the handler is triggered before any previously-attached event handlers? I came across this article, but the code didn't work because event handlers are no-longer stored in an array, which is what his code expected. I attempted to create a jQuery extension to do what I wanted, but this is not working (the events still fire in the order they were bound):

$.fn.extend({
    bindFirst: function(type, handler) {

        var baseType = type;
        var dotIdx = type.indexOf('.');
        if (dotIdx >= 0) {
            baseType = type.substr(0, dotIdx);
        }

        this.each(function() {
            var oldEvts = {};
            var data = $.data(this);
            var events = data.events || data.__events__;
            var handlers = events[baseType];
            for (var h in handlers) {
                if (handlers.hasOwnProperty(h)) {
                    oldEvts[h] = handlers[h];
                    delete handlers[h];
                    // Also tried an unbind here, to no avail
                }
            }

            var self = $(this);
            self.bind(type, handler);

            for (var h in oldEvts) {
                if (oldEvts.hasOwnProperty(h)) {
                    self.bind(baseType, oldEvts[h]);
                }
            }
        });
    }
});

Is there a natural way to reorder event handling? If there isn't, do you know of technique I could apply? I'm using jQuery 1.4.1, though I'll upgrade if I must.

Jacob
  • 77,566
  • 24
  • 149
  • 228
  • You'll first have to store all the `self.bind(type, handler)` calls into an array because bind is creating a new instance of that function. – rxgx Jan 20 '11 at 01:46

9 Answers9

15

Here's a simple plugin I did a while back. Lets you bind a handler to the beginning of the list. It is very simple, and I wouldn't guarantee that it works with namespaced events or anything terribly fancy.

For simply binding a single or space separate group of events, it should work.

Example: http://jsfiddle.net/gbcUy/

$.fn.bindUp = function(type, fn) {

    type = type.split(/\s+/);

    this.each(function() {
        var len = type.length;
        while( len-- ) {
            $(this).bind(type[len], fn);

            var evt = $.data(this, 'events')[type[len]];
            evt.splice(0, 0, evt.pop());
        }
    });
};

Or if you wanted to manipulate the Array of handlers in some other manner, just get the handlers for the element you want, and manipulate it however you want:

Example: http://jsfiddle.net/gbcUy/1/

var clickHandlers = $('img').data('events').click;

clickHandlers.reverse(); // reverse the order of the Array
user113716
  • 318,772
  • 63
  • 451
  • 440
  • Unfortunately, this suffers from the same problem as in the article I mentioned. `evt` in your code is not an array any longer, but an associative array. Therefore, `splice` doesn't work. I'm using jQuery 1.4.1 (or higher if I need to). – Jacob Jan 20 '11 at 01:42
  • @Jacob: I've only tested with 1.4.2 and later, and it does indeed work. It is very much an Array. Did you try the examples I posted? – user113716 Jan 20 '11 at 01:44
  • @Jacob: Yep, it must be because of 1.4.1. I just tried with that version, and it was a no go. 1.4.2 and later seems fine. – user113716 Jan 20 '11 at 01:49
  • 5
    @PetrPeller The internal event registration logic changed in jQuery 1.7; you need to use `$._data($el[0]).events` in later versions. – Jordan Gray Nov 19 '13 at 17:22
  • new version of jquery more than 1.8 `$._data($(element).get(0), "events").click.reverse()` //to reverse the order of the Array – T. Abdelmalek Oct 19 '17 at 15:15
9

There is a rather nice plugin called jQuery.bind-first that provides analogues of the native on, bind, delegate and live methods which push an event to the top of the registration queue. It also takes account of differences in event registration between 1.7 and earlier versions. Here's how to use it:

$('button')
    .on     ('click', function() { /* Runs second */ })
    .onFirst('click', function() { /* Runs first  */ });

As with most of these answers, the big disadvantage is that it relies on jQuery's internal event registration logic and could easily break if it changes—like it did in version 1.7! It might be better for the longevity of your project to find a solution that doesn't involve hijacking jQuery internals.

In my particular case, I was trying to get two plugins to play nice. I handled it using custom events as described in the documentation for the trigger method. You may be able to adapt a similar approach to your own circumstances. Here's an example to get you started:

$('button')
    .on('click', function() {
        // Declare and trigger a "before-click" event.
        $(this).trigger('before-click');

        // Subsequent code run after the "before-click" events.
    })
    .on('before-click', function() {
        // Run before the main body of the click event.
    });

And, in case you need to, here's how to set properties on the event object passed to the handler function and access the result of the last before-click event to execute:

// Add the click event's pageX and pageY to the before-click event properties.
var beforeClickEvent = $.Event('before-click', { pageX: e.pageX, pageY: e.pageY });
$(this).trigger(beforeClickEvent);

// beforeClickEvent.result holds the return value of the last before-click event.
if (beforeClickEvent.result === 'no-click') return;
Jordan Gray
  • 16,306
  • 3
  • 53
  • 69
4

As answered here https://stackoverflow.com/a/35472362/1815779, you can do like this:

<span onclick="yourEventHandler(event)">Button</span>

Warning: this is not the recommended way to bind events, other developers may murder you for this.

Community
  • 1
  • 1
Linh Dam
  • 2,033
  • 1
  • 19
  • 18
1

what about this? bind the event and than do this:

handlers.unshift( handlers.pop() );
phoenix12
  • 23
  • 3
1

Here's a combination of some prior methods including support for handlers, namespacing, non-jquery bindings, and once support:

$.fn.oneFirst = function(event_type, event_callback, handler) {
    return this.bindFirst(event_type, event_callback, handler, "one");
},
$.fn.bindFirst = function(event_type, event_callback, handler, bind_type) {
    var event_types = event_type.split(/\s+/);
    var pos;
    handler = (handler == undefined ? event_callback : handler);
    event_callback = (typeof event_callback == "function" ? {} : event_callback);

    this.each(function() {
        var $this = $(this);
        for (var i in event_types) { // each bound type
            event_type = event_types[i];

            var event_namespace = ((pos = event_type.indexOf(".")) > 0 ? event_type.substring(pos) : "");
            event_type = (pos > 0 ? event_type.substring(0, pos) : event_type);
            var current_attr_listener = this["on" + event_type];

            if (current_attr_listener) { // support non-jquery binded events
                $this.bind(event_type, function(e) {
                    return current_attr_listener(e.originalEvent);
                });
                this["on" + event_type] = null;
            }

            if (bind_type == "one") {
                $this.one(event_type + event_namespace, event_callback, handler);
            }
            else {
                $this.bind(event_type + event_namespace, event_callback, handler);
            }

            var all_events = $.data(this, 'events') || $._data(this).events;
            var type_events = all_events[event_type];
            var new_event = type_events.pop();
            type_events.unshift(new_event);
        }
    });
};
roktir
  • 11
  • 1
1

In addition to the selected answer, consider it's missing parameters:

jQuery.fn.bindUp = function (type, parameters, fn) {
    type = type.split(/\s+/);

    this.each(function () {
        var len = type.length;
        while (len--) {
            if (typeof parameters === "function")
                jQuery(this).bind(type[len], parameters);
            else
                jQuery(this).bind(type[len], parameters, fn);

            var evt = jQuery._data(this, 'events')[type[len]];
            evt.splice(0, 0, evt.pop());
        }
    });
};
reformed
  • 4,505
  • 11
  • 62
  • 88
jLuna
  • 321
  • 2
  • 3
1

@patrick: I've been trying to solve the same problem and this solution does exactly what I need. One minor problem is that your plug-in doesn't handle namespacing for the new event. This minor tweak should take care of it:

Change:

var evt = $.data(this, 'events')[type[len]];

to:

var evt = $.data(this, 'events')[type[len].replace(/\..+$/, '')];
Cosmin
  • 21,216
  • 5
  • 45
  • 60
dave
  • 11
  • 2
0

My best attempt.
I had code that was structured as follows:

var $body = jQuery('body');
$body.on({
    'click': function(event){
    }
});

To then ensure that the callback was the first one called, I used this function:

/**
 * promoteLastEvent
 * 
 * @access  public
 * @param   jQuery $element
 * @param   String eventName
 * @return  void
 */
function promoteLastEvent($element, eventName) {
    var events = jQuery._data($element.get(0), 'events'),
        eventNameEvents = events[eventName],
        lastEvent = eventNameEvents.pop();
    eventNameEvents.splice(1, 0, lastEvent);
};

This is called as follows:

promoteLastEvent($body, 'click');

It works quite well for me, given my limited use of $.fn.on.

onassar
  • 3,313
  • 7
  • 36
  • 58
0

I create a very simple function to put my click event handler at first position, and all existing click handlers will only be triggered manually.

$.fn.bindClickFirst = function (eventHandler) {
    var events = $._data($next[0], "events");
    var clickEvtHandlers = [];

    events.click.forEach(function (evt) {
        clickEvtHandlers.push(evt.handler);
    });
    $next.off("click");

    $next.on("click", function () {
        var evtArg = event;
        eventHandler(evtArg, function () {
            clickEvtHandlers.forEach(function (evt) {
                evt(evtArg);
            });
        });
    })
}

And to use the function:

$btn.bindClickFirst(function (evt, next) {
    setTimeout(function () {
        console.log("bbb");
        next();
    }, 200)
})
Charlie
  • 2,141
  • 3
  • 19
  • 35