I've always added click listeners to every separate element that needs to be listened, which can create a big messy Javascript with a lot of event bindings.
I was now thinking of doing it another way; by binding the click event to the entire document and upon click, see if the targeted element has a 'data-action' attribute and if present, execute the function in it. So that clicking:
Will execute function ajax_load_stuff()
It would make my code much cleaner, especially in ajax environments, but I want to know about performance and efficiency of this method. Are there any disadvantages to this approach?
UPDATE code example:
document.body.addEventListener("click", function (e) {
if (e.target) {
var action = e.target.getAttribute("data-action");
if (action) {
e.stopPropagation();
var params = e.target.getAttribute("data-params");
var data = [];
if (params) {
data = params.split(',');
}
window[action].apply(e.target, data);
}
}
}, false);
Ofcourse this approch has several advantages and disadvantages.
First discussing the disadvantages.
Need to handle event propagation perfectly otherwise it could make your system slow.
Passing parameter to click event will be difficult. Maybe need to introduce another attribute like : data-action-param
Advantages:
Less event handling code.
Related
Is there any way to get the list of all event listeners of an element on the HTML page using JavaScript on that page.
Note: I know we can see them with Chrome dev tools event listeners but I want to log/access see list using the JavaScript of the page.
Also, I know we can get them through jQuery but for that, we also have to apply the events using jQuery, but I want something that would be generic so I could also access the event listeners applied to other elements such as web components or react components.
If you really had to, a general way to do this would be to patch EventTarget.prototype.addEventListener:
const listeners = [];
const orig = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(...args) {
if (this instanceof HTMLElement) {
listeners.push({
type: args[0],
fn: args[1],
target: this,
});
}
return orig.apply(this, args);
};
document.body.addEventListener('click', () => console.log('body clicked'));
console.log(listeners[0].fn);
click this body
To find listeners attached to an element, iterate through the listeners array and look for targets which match the element you're looking for.
To be complete, also patch removeEventListener so that items can be removed from the array when removed.
If you need to watch for listeners attached via on, then you'll have to do something similar to the above to patch the HTMLElement.prototype.onclick getter/setter, and for each listener you want to be able to detect.
That said, although you said you want a generic solution, rather than patching built-in prototypes, it'd be better to add the listeners through jQuery or through your own function.
What I did when I had a similar problem is add a data attribute when the listener was set, so I could identify it later.
At the end of the function that adds the listener:
elm.setAttribute('data-has_mask', true);
At the beginning of that same function:
if("true" == elm.getAttribute('data-has_mask')) {
return;
}
Maybe not exactly what the OP is looking for, but I was having a lot of trouble with this, and this is an obvious solution for a particular use case, and I guess it might help someone out.
I have the following buttons:
<button id="abcd" onclick="something()">click</button>
and the following functions are attached to this button apart from the one in its html definition.
$('#abcd').on('click',function(){alert("abcd");});
$('#abcd').on('click',function(){
someAjaxCallWithCallback;
});
Now I want a new function with another ajax call to execute on this button's click, before the above mentioned functions. This new function determines whether the remaining functions would be called or not based on what data is recieved by the ajax call. That is, this pre function should complete its execution before giving control over to the rest of the functions and also determine whether they would run or not.
As an example, without changing the existing validation logics and button code, I have to add a new pre-validation function and similarly and post validation function.
I have a bindFirst method using which I can at least bring my new function to the beginning of the call stack but I have not been able to contain its execution and control further delegation because of callbacks.
If I understand correctly, you are looking for the way to do this, without modifying html and already existing js, only by adding new js-code.
First of all, if onclick handler is set and you want to control it, you should disable it on page load (maybe, saving it to some variable):
$(document).ready(function() {
var onclick = $("#abcd").attr("onclick").split("(")[0];
//to run it in future: window[onclick]();
$("#abcd").attr("onclick", "");
});
Edit: I changed my answer a little, previous approach didn't work.
Now you need to remove all already existing handlers. If number of handlers you want to control is limited, constant and known to you, you can simply call them in if-else after pre-validation inside your pre-function. If you want something more flexible, you are able to get all the handlers before removing, save them and then call them in a loop.
For that "flexible" solution in the end of $(document).ready(); you save all already existing handlers to an array and disable them. Then you write your pre-function and leave it as the only handler.
var handlers = ($._data($("#abcd")[0], "events")["click"]).slice();
$("#abcd").off("click");
$("#abcd").click(function() {
//this is your pre-func
//some code
handlers[1].handler.call();
});
Try console.log($._data($("#abcd")[0], "events")) to see, what it is.
Finally just run your post-function and do whatever you need, using conditions.
So, the general algorithm is as follows:
Disable onclick
Save all handlers
Disable all handlers
Run pre-func first
Run handlers you want to be executed
Run post-func
In fact, you just make your pre-func the only handler, which can run all other handlers you may need.
Although Alex was spot on, I just wanted to add more details to cover certain cases that were left open.
class preClass{
constructor(name,id){
if($(id) && $(id)[0] && $(id)[0]['on'+name])
{
var existing = $(id)[0]['on'+name]
$(id).bindFirst(name,existing);
$(id).removeAttr('on'+name)
alert("here");
}
if($._data($(id)[0],"events")){
this.handlers = $._data($(id)[0],"events")[name].slice();
}
else
{
this.handlers = null;
}
this.id = id;
this.name = name;
}
generatePreMethod(fn,data)
{
$(this.id).off(this.name);
$(this.id).bindFirst(this.name,function(){
$.when(fn()).then(execAll(data));
});
}
}
function exec(item,index){
item.handler.call()
}
function execAll(handlers){
return function(){ handlers.forEach(exec);}
}
This more or less takes care of all the cases.
Please let me know if there is something I missed!
I have a context menuitem which is activated if an image is right-clicked, the exact same way that 'context-copyimage' is activated.
Is it possible to tie/pair that menuitem to the 'context-copyimage' therefore eliminating the need to add extra (duplicate) event-listeners and show/hide handlers??!!
(Adding an observer to 'context-copyimage' defeats the purpose)
If not, is it possible to use the event-listener that 'context-copyimage' uses?
Update:
I am trying to reduce listeners. At the moment, script has a popupshowing listeners. On popupshowing, it checks for gContextMenu.onImag and if true, it shows the menuitem. Firefox's context-copyimage does the exact same thing. I was wondering if it was possible to tie these 2 in order to remove/reduce the in-script event listeners.
I was also chatting with Dagger and he said that:
... the state of built-in items isn't set from an event handler, it's
set from the constructor for nsContextMenu, and there are no
mechanisms to hook into it
So it seems, that is not possible
No, there is no sane way of avoiding the event listener that would perform better than another event listener and is compatible with unloading the add-on in session.
Hooking nsContextMenu
As you have been already told, the state is initialized via gContextMenu = new nsContextMenu(...). So you'd need to hook the stuff, which is actually quite easy.
var newProto = Object.create(nsContextMenu.prototype);
newProto.initMenuOriginal = nsContextMenu.prototype.initMenu;
newProto.initMenu = function() {
let rv = this.initMenuOriginal.apply(this, arguments);
console.log("ctx", this.onImage, this); // Or whatever code you'd like to run.
return rv;
};
nsContextMenu.prototype = newProto;
Now, the first question is: Does it actually perform better? After all this just introduced another link in the prototype-chain. Of course, one could avoid Object.create and just override nsContextMenu.prototype.initMenu directly.
But the real question is: How would one remove the hook again? Answer: you really cannot, as other add-ons might have hooked the same thing after you and unhooking would also unhook the other add-ons. But you need to get rid of the reference, or else the add-on will leak memory when disabled/uninstalled. Well, you could fight with Components.utils.makeObjectPropsNormal, but that doesn't really help with closed-over variables. So lets avoid closures... Hmm... You'd need some kind of messaging, e.g. event listeners or observers... and we're back to square one.
Also I wouldn't call this sane compared to
document.getElementById("contentAreaContextMenu").addEventListener(...)
I'd call it "overkill for no measurable benefit".
Overriding onpopupshowing=
One could override the <menupopup onpopupshowing=. Yeah, that might fly... Except that other add-ons might have the same idea, so welcome to compatibility hell. Also this again involves pushing stuff into the window, which causes cross-compartment wrappers, which makes things error-prone again.
Is this a solution? Maybe, but not a sane one.
What else?
Not much, really.
Yes this is absolutely possible.
Morat from mozillazine gave a great solution here: http://forums.mozillazine.org/viewtopic.php?p=13307339&sid=0700480c573017c00f6e99b74854b0b2#p13307339
function handleClick(event) {
window.removeEventListener("click", handleClick, true);
event.preventDefault();
event.stopPropagation();
var node = document.popupNode;
document.popupNode = event.originalTarget;
var menuPopup = document.getElementById("contentAreaContextMenu");
var shiftKey = false;
gContextMenu = new nsContextMenu(menuPopup, shiftKey);
if (gContextMenu.onImage) {
var imgurl = gContextMenu.mediaURL || gContextMenu.imageURL;
}
else if (gContextMenu.hasBGImage && !gContextMenu.isTextSelected) {
var imgurl = gContextMenu.bgImageURL;
}
console.log('imgurl = ', imgurl)
document.popupNode = node;
gContextMenu = null;
}
window.addEventListener("click", handleClick, true);
this gives you access to gContextMenu which has all kinds of properties like if you are over a link, or if you right click on an image, and if you did than gContextMenu.imageURL holds its value. cool stuff
This code here console logs imgurl, if you are not over an image it will log undefined
I am creating an Ajax Client Control in ASP.Net. By inheriting from IScriptControl and then adding the relavant javascript class (which would inherit from a javascript control). I have found a memory leak in the following code:
Type.registerNamespace("mynamespace");
myClass = function (element) {
myClass.initializeBase(this, [element]);
}
myClass.prototype = {
initialize: function () {
myClass.callBaseMethod(this, 'initialize');
var me = this;
$(document).ready(function () {
me._initializeControl();
me._hookupEvents();
});
},
dispose: function () {
//Add custom dispose actions here
myClass.callBaseMethod(this, 'dispose');
},
//...other code ...
_hookupEvents: function () {
var me = this;
var e = this.get_element();
$("#viewRates", e).click(function () {
me.openDialog();
});
},
//...other code...
myClass.registerClass('myClass', Sys.UI.Control);
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
_hoookupEvents is a function in my javascript file. The leak is related ot the line me.openDialog. If I remove this line, there is no leak. However, I need this line to be able to call a function from the class (I cannot just use 'this' in the function because it would refer to the button). Is there a better way to do this? Or maybe I just need to call some methods in the dispose function to clean such things?
The memory leak at this code can happen on this line, as you also note
$("#viewRates", e).click(function () {
me.openDialog();
});
when you call it with UpdatePanel, or in general call it for the same component and with out first clear the previous events for the click, the previous handler stay on, and here we have two cases.
To register the same click event more than ones.
To update the dom with ajax, and not previous clear that handlers, as results the previous code stay for ever (for ever == until you leave the page).
In general the solution is to clear any previous handler for the click,
before add a new one.
when initialize a new ajax call with UpdatePanel and before get the new response.
Use a function like that to remove the click and clear the resource for the handler.
this.get_events().removeHandler('click');
I'm extremely hesitant to call it a memory leak if there are only 2 instance of myclass. If there are 2,000 instances of myclass there's DEFINITELY a leak.
I'd search real hard for any dynamic instantiation statements that you have, that create myClass on certain conditions. That is what i see a lot (creating classes in loops at application init, perhaps a form submit can trigger instantiation and it wasn't fully QA'd to see if you can get a submission to create multiple objects, etc).
I have a web page with DIVs with a mouseover handler that is intended to show a pop-up information bubble. I don't want more than one info bubble to be visible at a time. But when the user moves the mouse rapidly over two items, I sometimes get two bubbles. This should not happen, because the code for showing a pop-up cancels the previous pop-up.
If this were a multi-threaded system then the problem would be obvious: there are two threads trying to show a pop-up, and they both cancel existing pop-ups then pop up their own pop-ups. But I assumed JavaScript is always run single-threaded, which would prevent this. Am I wrong? Are event handlers running asynchronously, in which case I need synchronized access to shared data, or should I instead be looking for bugs in the library code for cancelling pop-ups?
Edited to add:
The library in question is SIMILE Timeline and its Ajax library;
The event handler does call SimileAjax.DOM.cancelEvent(domEvt), which I assume based on the name cancels the bubbling of events;
Just to make thing s more complicated, what I am actually doing is starting a timeout that if not cancelled by a moustout shows the pop-up, this being intended to prevent pop-ups flickering annoyingly but annoyingly having the reverse effect.
I'll have another poke at it and see if I can work out where I am going wrong. :-)
Yes, Javascript is single-threaded. Even with browsers like Google Chrome, there is one thread per tab.
Without knowing how you are trying to cancel one pop-up from another, it's hard to say what is the cause of your problem.
If your DIVs are nested within one another, you may have an event propagation issue.
I don't know the library you are using, but if you are only trying to display one tooltip of somesort at a time... use a flyweight object. Basically a flyweight is something that is made once and used over and over again. Think of a singleton class. So you call a class statically that when first invoked automatically creates an object of itself and stores it. One this happens every static all references the same object and because of this you don't get multiple tooltips or conflicts.
I use ExtJS and they do tooltips, and message boxes as both flyweight elements. I'm hoping that your frameworks had flyweight elements as well, otherwise you will just have to make your own singleton and call it.
It is single threaded in browsers. Event handlers are running asynchroniously in one thread, non blocking doesn't allways mean multithreaded. Is one of your divs a child of the other? Because events spread like bubbles in the dom tree from child to parent.
Similar to what pkaeding said, it's hard to guess the problem without seeing your markup and script; however, I'd venture to say that you're not properly stopping the event propagation and/or you're not properly hiding the existing element. I don't know if you're using a framework or not, but here's a possible solution using Prototype:
// maintain a reference to the active div bubble
this.oActiveDivBubble = null;
// event handler for the first div
$('exampleDiv1').observe('mouseover', function(evt) {
evt.stop();
if(this.oActiveDivBubble ) {
this.oActiveDivBubble .hide();
}
this.oActiveDivBubble = $('exampleDiv1Bubble');
this.oActiveDivBubble .show();
}.bind(this));
// event handler for the second div
$('exampleDiv2').observe('mouseover'), function(evt) {
evt.stop();
if(this.oActiveDivBubble) {
this.oActiveDivBubble.hide();
}
this.oActiveDivBubble = $('exampleDiv2Bubble');
this.oActiveDivBubble .show();
}.bind(this));
Of course, this could be generalized further by getting all of the elements with, say, the same class, iterating through them, and applying the same event handling function to each of them.
Either way, hopefully this helps.
FYI: As of Firefox 3 there is a change pretty much relevant to this discussion: execution threads causing synchronous XMLHttpRequest requests get detached (this is why the interface doesn't freeze there during synchronous requests) and the execution continues. Upon synchronous request completion, its thread continues as well. They won't be executed at the same time, however relying on the assumption that single thread stops while a synchronous procedure (request) happening is not applicable any more.
It could be that the display isn't refreshing fast enough. Depending on the JS library you are using, you might be able to put a tiny delay on the pop-up "show" effect.
Here's the working version, more or less. When creating items we attach a mouseover event:
var self = this;
SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mouseover", function (elt, domEvt, target) {
return self._onHover(labelElmtData.elmt, domEvt, evt);
});
This calls a function that sets a timeout (pre-existing timeouts for a different item is cancelled first):
MyPlan.EventPainter.prototype._onHover = function(target, domEvt, evt) {
... calculate x and y ...
domEvt.cancelBubble = true;
SimileAjax.DOM.cancelEvent(domEvt);
this._futureShowBubble(x, y, evt);
return false;
}
MyPlan.EventPainter.prototype._futureShowBubble = function (x, y, evt) {
if (this._futurePopup) {
if (evt.getID() == this._futurePopup.evt.getID()) {
return;
} else {
/* We had queued a different event's pop-up; this must now be cancelled. */
window.clearTimeout(this._futurePopup.timeoutID);
}
}
this._futurePopup = {
x: x,
y: y,
evt: evt
};
var self = this;
this._futurePopup.timeoutID = window.setTimeout(function () {
self._onTimeout();
}, this._popupTimeout);
}
This in turn shows the bubble if it fires before being cancelled:
MyPlan.EventPainter.prototype._onTimeout = function () {
this._showBubble(this._futurePopup.x, this._futurePopup.y, this._futurePopup.evt);
};
MyPlan.EventPainter.prototype._showBubble = function(x, y, evt) {
if (this._futurePopup) {
window.clearTimeout(this._futurePopup.timeoutID);
this._futurePopup = null;
}
...
SimileAjax.WindowManager.cancelPopups();
SimileAjax.Graphics.createBubbleForContentAndPoint(...);
};
This seems to work now I have set the timeout to 200 ms rather than 100 ms. Not sure why too short a timeout causes the multi-bubble thing to happen, but I guess queuing of window events or something might still be happening while the newly added elements are being laid out.