Extend jQuery's .on() method with custom events - javascript

There are numerous questions on SO about custom jQuery events. Almost all of them only have answers suggesting the use of $.fn.extend().
Here is my script using $.fn.extend() to detect when an element has stopped scrolling:
$.fn.extend({
scrollStopped: function (callback) {
var $this = $(this)
$this.scroll(function () {
clearTimeout($this.data('scrollTimeout'))
$this.data('scrollTimeout', setTimeout(callback.bind(this), 150))
}.bind(this))
return this
}
})
$('element').scrollStopped(function () {
alert('Scrolling has stopped')
})
Like the title of this question suggests, what I want, however, is not an event that has to be added with .scrollStopped(), but one that can be added with the .on() method, like so:
$('window').on('scrollStopped', function () {
alert('Window has stopped scrolling')
})
$('div').on('scrollStopped', function () {
alert('Div has stopped scrolling')
})
I've been scanning the jQuery source trying to find a way to do this. After copy-pasting a bunch of snippets for an hour or two, I decided to look for an answer on SO, which was surprisingly hard to find. This answer helped me a bit, but (like I said in the comments of that answer, one hour ago) the events that trigger the custom event there are hardcoded to the body and then sent to the elements to which the .on() method is bound.
There are two problems with the approach:
The event logic still isn't part of jQuery's internal structure. It's simply part of an event triggered by the body (not a deal-breaker per say)
Scroll events (like the one I'm trying to use) don't bubble, meaning that converting the events from keydown to scroll won't work
I've simplified the Fiddle from the answer here.
I know I could use a native event and set useCapture to true to capture all scroll events on the page, but seeing there are no answers anywhere on SO that explain how to do what I want, I think it's worthwhile to take a look at this.
The only reason I want to do this, is because I want to be able to write things like:
$(window).on('beforeunload scrollStopped', function () { savePageScroll() })
Instead of:
$(window)
.on('beforeunload', function () { savePageScroll() })
.scrollStopped( function () { savePageScroll() })`

Related

Jquery trigger works only for one event specified by addEventListener at a time

I'm building a library without depending on Jquery in order to better my javascript knowledge. However in writing tests for the library I'm using some Jquery methods.
I have a test that triggers events listeners added using the native EventTarget.addEventListener method using Jquery's .trigger method.
var elem = document.getElementById('square');
elem.addEventListener('click', function () {
alert('click');
});
elem.addEventListener('mouseover', function () {
alert('mouseover');
});
$(elem).trigger('click');
$(elem).trigger('mouseover');
When I trigger two different events on the same element, only one handler fires.
This can be observed in this JSFiddle
Can someone explain why this happens and how to fix it?
If you are using addEventListener, you can't use jQuery's trigger method. (It will work for some events like click, but not for everyone, like mouseover, as you noticed). This is also explained in this question.
You have two options. Either you use jQuery's on event like this:
$(elem).on('mouseover', function () {
alert('mouseover');
});
Or the other is to trigger the event without using jQuery. You can check this question to do that.

Overwriting tap event with preventDefault

I had a lot of:
$('#element').on('tap', function(){
// some code ..
})
I searched many questions about the tap event problem firing twice, and I solved my problem using e.preventDefault(), now I have a lot of:
$('#element').on('tap', function(e){
e.preventDefault();
// some code ..
})
Ok, but as I said, I have many of these calls and I don't like much to write every time e.preventDefault(), then I typed $.fn.tap on chrome's console and it showed me:
function (a){return a?this.bind(c,a):this.trigger(c)}
I tried to overwrite it this way:
$.fn.tap = function (a) {
a.preventDefault();
return a?this.bind(c,a):this.trigger(c)
}
But it didn't worked as it did in the previous e.preventDefault().
I'm not seeing anything obvious and I'm out of ideas for this.
Any help or idea is appreciated. Thanks in advance.
This is how you can create your $.fn.tap:-
$.fn.tap = function(f) {
$(this).on('tap', function(e) {
e.preventDefault();
f();
});
return this;
};
//usage
$('.selector').tap(function() {
alert('foo bar')
})
#Washington Guedes - overwrite the default tap-event to always use e.preventDefault()
rather than changing from $(element).on('tap', function(){}) to
$(element).tap(function(){})
You could add a delegate event to body for tap, without specifying a target. This will then fire for all tap events on the body, which you can then check if the target has its own tap event, so you can then e.preventDefault();.
NOTE: This will not work for delegated tap events as shown.
// global tap handler
$('body').on('tap', function(e) {
if ($._data(e.target, "events").tap)
e.preventDefault();
});
// tap event
$('a.tap').on('tap', function(e) {
$(this).css('color', 'red');
});
// delegated tap event
$('body').on('tap', 'a.delegate', function(e) {
$(this).css('color', 'green');
});
a {
display: block;
margin: 20px 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.js"></script>
<a class="tap" href="www.google.co.uk">tap event, prevented.</a>
<a class="delegate" href="www.google.co.uk">delegate tap event, not prevented.</a>
no tap event, not prevented
One of the cool features of jQuery (I usually don't use 'jQuery' and 'cool' in a single sentence) is that it lets you specify custom behaviour for events, using the $.event.special object.
There is very little documentation on the subject, so a little example would be in order.
A working fiddle using the click event (as this was more convenient for me to write on my laptop) can be found here
Translated to your request to have all tap events have e.preventDefault() invoked before the actual handler, would look like:
$.event.special.tap.add = function(options) {
var handler = options.handler;
options.handler = function(e) {
e.preventDefault();
return handler.apply(this, arguments);
}
}
What this code does (should do, as I haven't actually tested the tap version above) is telling jQuery that you want a special treatment for the tab event, more particularly you want to provide a 'wrapping handler' which does nothing more than call e.preventDefault() before calling the provided event handler.
UPDATE: prevented the default tap-settings from being overwritten, for future visitors
NOTE: Before you make any attempt on changing the default behaviour of things, you should ask yourself why the defaults don't work for you. Mostly because changing default (=expected) behaviour will upset your future self (or worse, another person) while maintaining your code and adding features.
In order to create a maintainable (and predictable) flow in your code the suggested solution to create a special case function ($.fn.tap) is in fact a very viable solution, as it does not interfere with the default (=expected) behaviour of things.
From the links I provided you should also be able to create your own event type (e.g. tapOnly) and make it more obvious there is some custom work involved. Then again, both of these solutions will require you to change your event bindings, which is what you are trying to prevent.
I knew can be a bad idea but I've just tested this in Chrome
$('*').on('tap',function(e){
e.preventDefault();
});
$('#element').on('tap', function(){
// some code ..
});
and if you don't need this for all elements:
$('*').not('#aCertainElement').on('tap',function(e){
e.preventDefault();
});
I had a similar problem, in which e.preventDefault() would work on some cases, but not on others. It showed no errors, and using try-catch was not displaying the catch alert. Adding e.stopImmediatePropagation() did the trick, in case it helps anyone

jQuery off then on chaining

I was wondering why this code would be effective, and if it is not, is there a better way to make sure that event handlers are removed before the new event handler is attached:
$('.selector')
.off('click', '.item')
.on('click', '.item', function() {
// code goes here
})
Thank you for help in knowing if this is an optimal way to make sure the event handler has been removed before adding another to the selector.
You can create a jQuery function that calls this two functions at once:
$.fn.onAndOff = function(event, selector, handler) {
this.off(event, selector)
.on(event, selector, handler);
}
Then use it like this:
$(".selector").onAndOff("click", ".item", function() {
// code goes here
});
If there's a risk of multiple identical click events being created for a particular element in your app, this may be a solution. Typically, however, the logic of your application is designed to prevent that.

What's the best way to event handle dynamically created HTML? [duplicate]

This question already has answers here:
Adding event listeners to dynamically added elements using jQuery [duplicate]
(4 answers)
Closed 8 years ago.
It's very easy to event-handle when dealing with items the document has from the get go:
$(document).ready(function() {
$('.element-in-question').on("event", function (event) {
//do what you need to do during the event
});
});
My problem is how would I best deal with dynamic elements. For example, let's say I dynamically load notifications, some of which are friend requests during an AJAX request. Would I create the event-handler in the success callback, or would I do it somewhere else?
The way I would currently go about it:
$(document).ready(function() {
$.ajax({
url: '/friendships/requests',
type: 'GET',
success: function(responseData) {
//dynamically create your elements (with classes accepted and rejected)
$('.accepted, .rejected').on("click", function(event) {
//do what is needed in this event
});
}
});
});
Is this the idiomatic way to go about it, or is there another way I probably should be going about it?
use jquery's "on" merhod to bind event handler to parent element (which will not change) and pass a selector of the element you want to listen to:
$('.parent').on('event', '.child', handlerFunction);
If you dynamically create an element, such as a 'button', that was not on the page before, handle it like this:
$(function() {
(function() {
$('body').append('<button id="newButton">Hello World!</button>');
})();
$('body').on('click','#newButton',function() {
console.log($(this).html()); //"Hello World!"
});
});
I think this is the (partly) right approach. You cannot and should not apply eventhandlers to objects that might or might not be available, even if possible.
If the situation would involve 10000 different eventhandlers, they should be only available when present in dom. When removed the eventhandler should be removed as well.
The way you do it is rudimentary but correct.
2 other thoughts. If you bind the listener in the ajax callback you might add to the "stack" of events, since they are not replaced. Not a good thing. If the ajax query will happend more than once, do not add it again, if not removed first.
Another aproach might be to just add them to all pages, if this is a small page/application and first check that the element exist. Like so:
if ($('#id').size() > 0) {
// bind events for #id here
}

How do I unbind events properly when using allowSamePageTransition in jQuery Mobile?

I'm using the jQuery Mobile option allowSamePageTransition, which enables me to go from
page A > page A > page A ...
I need this to allow browsing through a catalogue of items. My problem is, the items need some form of interaction and I used to attach the interaction binding to document, because it is set before the elements affected are generated.
However, reloading the same page over and over again will re-bind my event handlers every time I reload.
My first idea was to use .off when the page is being hidden, but reloading a page #foo, will trigger pagehide on the same page being shown, so all bindings set on
$(document).on("pagebeforeshow.foo_events", "#foo", function(e) {
// bind when shown
});
will be unbound again by the previous #foo being hidden
$(document).on("pagehide", "#foo", function (e) {
$(this).off(".foo_events");
// removes bindings on #foo being hidden AND shown
});
The only solution I have come up with is plastering the document with classes, which I don't like doing:
priv.setBindings = function (param) {
var doc = $(document);
doc
.filter(function() { return $(this).is(".e_gallery") !== true; })
.on("pagebeforeshow.gallery", param.pageId, function (e) {
doc.addClass(".e_gallery");
// run stuff
});
};
But I'm no fan of attaching classes to the dom.
Question:
Is there a way to prevent multiple event bindings set on $(document) when going to the same page over and over again WITHOUT toggling classes?
Solution 1
Best solution would be to use pageinit to bind events. If you take a look at an official documentation you will find out that pageinit will trigger ONLY once, just like document ready, so there's no way events will be bound again. This is best solution because you don't have processing overhead like when removing events with off method.
Working jsFiddle example: http://jsfiddle.net/Gajotres/AAFH8/
Of course this will fail in case multiple HTML solution is used.
Solution 2
Remove event before you bind it:
$(document).on('pagebeforeshow', '#index', function(){
$(document).off('click', '#test-button').on('click', '#test-button',function(e) {
alert('Button click');
});
});
Working jsFiddle example: http://jsfiddle.net/Gajotres/K8YmG/
Solution 3
Use a jQuery Filter selector, like this:
$('#carousel div:Event(!click)').each(function(){
//If click is not bind to #carousel div do something
});
Because event filter is not a part of official jQuery framework it can be found here: http://www.codenothing.com/archives/2009/event-filter/
This is probably best solution because event is going to be bound ONLY once.
Solution 4
Probably an easiest of them all.
$(document).on('pagebeforeshow', '#index', function(){
$(document).on('click', '#test-button',function(e) {
if(e.handled !== true) // This will prevent event triggering more then once
{
alert('Clicked');
e.handled = true;
}
});
});
Working jsFiddle example: http://jsfiddle.net/Gajotres/Yerv9/
This is a 180 percent different solution then solution 3, in this case event is going to be bound numerous times but it will be allowed to execute only once.
More info
If you want to find more about this problem take a look at this article, working examples are included.

Categories