Niels Brinch
Niels Brinch

Reputation: 3642

Prepend an onclick action on a button with JQuery

I have a button that already has an onclick-event and an assigned function. I wish to add another function call in front of it. I can imagine it should be possible to fiddle around with the onclick attribute (attr), but I do not think this is best practice.

What would you consider best practice to prepend a function call on an existing onclick-event?

Upvotes: 6

Views: 8062

Answers (7)

Top-Master
Top-Master

Reputation: 8796

Tested with JQuery 3.3.1 (but may work for 1.9.1 and later). See Tomalak's answer for versions before 1.9.1.

One of my company's projects had an issue (now fixed), where I noticed that:

  • Another global-script was already handling the submit, let's call it global-handler,
  • And global-handler was preventing default behavior,
  • And global-handler finally did programmatically cause redirection.

Removing and/or altering said global-handler was not an option at the time.

JQuery calls listeners/handlers in order of registeration, meaning:

  • Because said global-handler's logic was always required,
  • But my own logic was only required on few forms,
  • I needed my own-handler's logic to act as a "Middleware", in other words:
    • I needed a way to prepend my own-handler,
    • Then, only if my own-handler's validation-logic fails, prevent global-handler's logic from being called.
    • Otherwise, always call said global-handler's logic.

Solution

Note that I am using Type-script, but if required, you could cast to JavaScript (simply remove types and declare global section?).

First, define somewhere, maybe plugins.ts file, the helpers and/or JQuery plugins, like:

import $ from 'jquery';

export type Middleware = (next: () => void) => void;

declare global {
  interface JQuery<TElement = HTMLElement> {
    /**
     * Prepends given `middleware` to be called on enter-key and click events.
     *
     * Also, passes a trigger to "default click operation" as first argument
     * (the `next()` callback).
     *
     * WARNING: registering multiple {@link Middleware} is NOT supported yet
     * (if all target same element).
     */
    clickMiddleware(middleware: Middleware): JQuery;

    /**
     * Same as {@link JQuery.trigger} for click, but cross-platform.
     *
     * For example, JQuery intentionally does nothing for Anchor-tags
     * (which is their way to be cross-platform).
     */
    clickCompat(): JQuery;

    /**
     * Same as {@link JQuery.on}, but prepends handler.
     *
     * Additionally, may replace inline-handlers with JQuery handler
     * (because they get called before JQuery's handler).
     */
    prependEvent<TString extends string>(
      events: TString,
      handler: false
        | JQuery.TypeEventHandler<TElement, undefined, TElement, TElement, TString>
    ): this;
  }
}

export function register() {
  (<any>$.fn).clickMiddleware = function (middleware: Middleware) {
    return $(this).prependEvent('keypress click', function (event: any) {
      // May already be past middleware.
      const $element = $(this);
      if ($element.prop('isCallingDefault')) {
        $element.prop('isCallingDefault', false);
        return true;
      }

      // Executes middleware (but only for enter-key and click events).
      const logicalKey = event.key || event.which || event.keyCode || 0;
      if (logicalKey === 13 || event.type === 'click') {
        if (event.stopImmediatePropagation) event.stopImmediatePropagation();
        if (event.stopPropagation) event.stopPropagation();
        event.preventDefault();

        middleware.call(this, function () {
          $element.prop('isCallingDefault', true);
          $element.clickCompat();
        });
        return false;
      }

      return true;
    });
  };

  (<any>$.fn).clickCompat = function () {
    $(this).each(function (index, element: HTMLElement) {
      if ((<any>document).dispatchEvent) {
        let event = null;
        try {
          event = new MouseEvent('click', {
            'view': window,
            'bubbles': true,
            'cancelable': false
          });
        } catch {
          event = document.createEvent('MouseEvents');
          // noinspection JSDeprecatedSymbols
          event.initMouseEvent('click', true, true,
            window, 0, 0, 0, 0, 0,
            false, false, false, false,
            0, null);
        }
        element.dispatchEvent(event);
      } else if ((<any>document).fireEvent) {
        element.click();
      }
    });
  };

  (<any>$.fn).prependEvent = function (eventList: string, handler: any) {
    // Using "container" param instead of "var container;".
    return $(this).each(function () {
      const $element = $(this);
      for (let event of eventList.split(' ')) {
        // Replaces inline-handlers (if possible).
        const attributeName = 'on' + event;
        const oldHandler = this[attributeName];
        if (oldHandler) {
          $element.attr(attributeName, null);
          internalPrependEvent($element, event, function () {
            oldHandler.apply(this, arguments);
          });
        }
        // Actual prepend.
        internalPrependEvent($element, event, handler);
      }
    });
  };
}

/**
 * NOTE: is already called form inside `$selection.each(...)`,
 * hence `$element.get(0)` is enough (no need to handle multiple elements).
 */
function internalPrependEvent(
  $element: JQuery<any>, eventName: string, handler: any
) {
  // Append new-listener.
  $element.on(eventName, handler);
  // Move above added new-listener to first in list.
  const container = (<any>$)._data($element.get(0), 'events')[eventName];
  container.unshift(container.pop());
}

Usage

Finally, import and use above where required.

For example, OP may use it like:

require('./plugins').register();

$(function () {
    $('my-selector').prependEvent('click', function (event) {
        // Do something here...
    });
});

But I use it like:

require('./plugins').register();

$(function () {
    $('my-selector').clickMiddleware(function (next) {
        var valid = ... my logic here ...;
        if (valid) {
            next();
        }
    });
});

Note that my web-site fetches JQuery from CDN, and I have in my web-pack config something like:

externals: {
   jquery: 'jQuery',
}

Upvotes: 0

Regular Jo
Regular Jo

Reputation: 5510

This appears to work in 1.9.1 through 3.1.4. See Tomalak's awesome answer for versions before 1.9.1.

$(function() {
  $("input" ).mousedown( function() {
    console.log( "mousedown" );
  });

  $("input" ).click( function() {
    console.log( "click" );
  });
    
  $.fn.extend({exposeListeners: 
      function (fn) {
        return $._data( this[0], "events") || {};
      },
    prependListener: function (types, fn) {
      var elems = this;
      $(elems).each(function() {
        var elem = this;
        var $elem = $(elem)
        $elem.on(types, fn);

        var events = $._data( $elem[0], "events")[types];

        if (events.length > 1) { // its the only event, no reason to shift
            events.unshift(events.pop());  // put last as first
        }
    
        return this;
      });
    }
  })
  
  $("#prep").click(function () {
    console.log($("input").exposeListeners());
    $("input:not(:last)").prependListener("click", function () {
      console.log('click, prepended');
    })
    $("a").prependListener("click", function () {
      console.log('click event created');
    })
    console.log($("body").exposeListeners());
  })
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<input type="button" value="input, Will accept prepend"><br>
<input type="button" value="input, Will also accept prepend"><br>
<input type="button" value="input, Will not"><br>
<a href="#">Link with no pre-exting handlers</a><br><br>

<Button id="prep">Prepend an event</Button>

And here's a barebones version

$(function() {
  $.fn.extend({prependListener: function (types, fn) {
      var elems = this;
      $(elems).each(function() {
        var elem = this;
        var $elem = $(elem)
        $elem.on(types, fn);

        var events = $._data( $elem[0], "events")[types];

        if (events.length > 1) {
          events.unshift(events[events.length-1]);
          events.pop();
        }
    
        return this;
      });
    }
  })
})

Upvotes: 0

Tomalak
Tomalak

Reputation: 338316

If you are not afraid to mess with the guts of jQuery:

// "prepend event" functionality as a jQuery plugin
$.fn.extend({
  prependEvent: function (event, handler) {
    return this.each(function () {
      var events = $(this).data("events"), 
          currentHandler;

      if (events && events[event].length > 0) {
        currentHandler = events[event][0].handler;
        events[event][0].handler = function () {
          handler.apply(this, arguments);
          currentHandler.apply(this, arguments);
        }      
      }
    });
  }
});

$("#someElement").prependEvent("click", function () {
    whatever();
});​

See it live: http://jsfiddle.net/Tomalak/JtY7H/1/

Note that there must already be a currentHandler or the function will not do anything.

Disclaimer: I consider this a hack. It depends on jQuery internals that might change in the next version of the library. It works now (I tested jQuery 1.7.2), but don't be surprised if it breaks with a future version of jQuery.

Upvotes: 10

marknatividad
marknatividad

Reputation: 630

I don't think there's a way to prepend a function call on an existing onclick event, however you can try this:

$('#foo').unbind('click');
$('#foo').click(function() { 
   // do new function call..
   // call original function again...
});

Hope that helps.

Upvotes: 2

noob
noob

Reputation: 9212

I've written a short example here:

​$("button").click(function() { // this is already existing
    if ($("span").length) {
        alert("ok");
    }
});

var clicks = $("button").data("events").click.slice();
$("button")
    .unbind("click")
    .click(function() { // this is the new one which should be prepended
        $("body").append($("<span>"));
    });
$.each(clicks, function(i, v) {
    $("button").click(v);
});

Upvotes: 6

Ajinkya
Ajinkya

Reputation: 22720

You can add function on single event but I try to avoid two functions on onclick event. Instead you can
1. Add new code in existing function.
2. Add call to new function from existing function.

Upvotes: 1

simekadam
simekadam

Reputation: 7384

If you have binded the event like this

$(elem).on('click', functionName);

you can modify that like this:

$(elem).on('click', function(){

 anotherFunctionNameOrWhateverYouWant();

 functionName();
});

Upvotes: -2

Related Questions