11

Is there a way to fire a single function once when any event is raised?

For example, if I have the following function: (demo in jsfiddle)

$('input').one('mouseup keyup', function(e){ 
    console.log(e.type);
});

I'd like to only call the function once, regardless of which event fired it.

But according to the docs for .one():

If the first argument contains more than one space-separated event types, the event handler is called once for each event type.

So, currently the function will fire once for each event type.

KyleMit
  • 30,350
  • 66
  • 462
  • 664

5 Answers5

19

Instead of using .one, use .on and remove the binding manually with .off.

$('input').on('mouseup keyup', function(e){
    console.log(e.type);
    $(this).off('mouseup keyup');
});

Fiddle: http://jsfiddle.net/23H7J/3/


This can be done a little more elegantly with namespaces:

$('input').on('mouseup.foo keyup.foo', function(e){
    console.log(e.type);
    $(this).off('.foo');
});

This allows us to use a single identifier (foo) to remove any number of bindings, and we won't affect any other mouseup or keyup bindings the element may have.

Fiddle: http://jsfiddle.net/23H7J/41/

Mooseman
  • 18,763
  • 14
  • 70
  • 93
  • My mind and bacon has been saved! Thank you so much. Using .off with multiple transition events was twisting my melon as it was firing for each event (note to self - read documentation in full). – Submachine23 Feb 06 '17 at 17:00
  • JQuery namespaces not required: `$('input').on('mouseup keyup', function foo(e) { $(this).off('mouseup keyup', foo); });` – Dan Oct 07 '22 at 02:41
8

Great answers! To wrap them into a function, here's a jQuery extension based off the current answers:

//The handler is executed at most once per element for all event types.
$.fn.once = function (events, callback) {
    return this.each(function () {
        $(this).on(events, myCallback);
        function myCallback(e) {
            $(this).off(events, myCallback);
            callback.call(this, e);
        }
    });
};

Then call like this:

$('input').once('mouseup keyup', function(e){ 
    console.log(e.type);
});

Demo in fiddle

Bonus: This has the added benefit of only detaching the handlers for this specific function by passing in the original handler to the off function. Otherwise, you'll need custom namespaces.

Additionally, if you want a handler to fire only once as soon as any of the elements fire any of the events and then immediately detach itself, then just remove the each from the extension like this:

//The handler is executed at most once for all elements for all event types.
$.fn.only = function (events, callback) {
    var $this = $(this).on(events, myCallback);
    function myCallback(e) {
        $this.off(events, myCallback);
        callback.call(this, e);
    }
    return this
};
KyleMit
  • 30,350
  • 66
  • 462
  • 664
  • Can you modify your function to work using the syntax shown in your anwser as well as a syntax of `$('#someParent').once('keyup etc', '.someChildrenOfParent'), function(e){ doStuff(); });` ? – Drew Dec 04 '19 at 21:28
  • @Drew, As in using a delegate? Probably, wanna take a whack at it first? – KyleMit Dec 04 '19 at 21:30
  • This is what I have but I am not sure where to stick the 'scope' and as a result the functions behaves like jQuery's .one(). https://jsfiddle.net/wyeq0h3g/ – Drew Dec 04 '19 at 21:48
  • Hmm, i'm not sure the original solution is working since I would expect the orange background to toggle on/off with each key press in this test fiddle: https://jsfiddle.net/L5uzkwv2/ – Drew Dec 05 '19 at 20:06
4

To fire once and keep firing, the following snippet may be used:

https://jsfiddle.net/wo0r785t/1/

$.fn.onSingle = function(events, callback){
    if ("function" == typeof(callback)) {
        var t = this;
        var internalCallback = function(event){
            $(t).off(events, internalCallback);
            callback.apply(t, [event]);
            setTimeout(function(){
                $(t).on(events, internalCallback);
            }, 0);
        };
        $(t).on(events, internalCallback);
    }
    return $(this);
};

It works by temporarily disabling the events using the internalCallback function, and asynchronously (setTimeout) enabling it again.

Most other suggestions in this thread disable all future events on the target. However, while OP seems satisfied with previous answers, his question does not mention the permanent disabling of events.

Kafoso
  • 534
  • 3
  • 20
2

You could just add $('input').off(); to the end of your function.

Grim...
  • 16,518
  • 7
  • 45
  • 61
-1

A simpler way to fire once but keep firing on subsequent changes, is to temporarily store the previous value and do a quick comparison: If nothing has changed then don't run the rest of the function.

jQUERY

$('input').on('keyup paste input change', function(e){ 
    var oldValue = $(e.target).data('oldvalue');
    var newValue = $(e.target).val();

    if (oldValue === newValue) //nothing has changed
        return;

    $(e.target).data('oldvalue',newValue); //store the newly updated value

    //proceed with the rest of the function
});

Note that I'm using .on to attach the events, instead of .one

PURE JAVASCRIPT

For a pure JavaScript solution, listeners need to be added separately, since it's not possible to pass multiple events to .addEventListener.

var selector = document.querySelector('input')
selector.addEventListener('keyup', updateElement);
selector.addEventListener('paste', updateElement);
selector.addEventListener('input', updateElement);
selector.addEventListener('change', updateElement);

function updateElement(e){
    var oldValue = e.target.getAttribute('oldvalue');
    var newValue = e.target.value;

    if (oldValue === newValue) //nothing has changed
       return; 
    console.log (newValue);
    e.target.setAttribute('oldvalue', newValue); //store the newly updated value

    //proceed with the rest of the function
}

Assuming you want to listen for changes in the input box, I suggest not to use mouseup to detect a possible Right click > paste, since the mouse could be released somewhere else, not necessarily inside the input box. To be on the safe side you better use keyup, paste, input and change, as I did on my code above.

  • Hey Juan, thanks for sharing. I don't think this really answers the question set forth here. If I have a `mouseup` and `keyup` event and I want to handle that only once, regardless of which was fired, comparing `oldValue === newValue` doesn't get us there. As such, it might be worth taking down the answer and posting your own question to spell out what you're trying to solve – KyleMit Sep 13 '19 at 04:20