Normally to get listeners on that DOM node I am using
$('selector').data('events');
However this does not show event listeners that are being add via delegation, e.g
$(document).on('click', 'selector', handlerFunction)
One obvious way is to traverse up the DOM tree and look if any of parents are delegating events to element at hand, by concurrently calling $('selector').parent().data('events') until no parent can be found, however this does not strike me as very efficient or standard way of doing things, and I think of it this sort of problem is too common not to have a better solution.
How to find all the event listeners including delegated ones?
ATM I am using functions below, not to elegant - but saves me quite some time.
var getAllEventListeners = function (options) {
if (options.internalArr == undefined)
options.internalArr = [];
if (options.elements.data('events') != undefined) {
options.internalArr.push({
elements: options.elements,
events: options.elements.data('events')
});
}
if (options.elements.parent().length != 0) {
getAllEventListeners({
elements: options.elements.parent(),
internalArr: options.internalArr
});
}
}
var findAllListeners = function (selector) {
var opt = {
elements: $(selector),
internalArr: []
};
getAllEventListeners(opt);
return opt.internalArr;
}
Related
I'm attempting to create a script for a platform that allows users to inject javascript. They are using YUI and specifically Y.one('body).delegate('click',...) to attach an event to a button. I would like to intercept this button but I cannot figure out how to stop, block, remove or otherwise prevent the event handler from firing.
Note: I don't have direct access to the handler returned by `Y.delegate()
So far I've tried
Y.detachAll('click');
Y.unsubscribeAll('click');
Y.one('body').detachAll('click');
Y.one('body').unsubscribeAll('click');
Y.one('.the-delegated-class').detachAll('click');
Y.one('.the-delegated-class').unsubscribeAll('click');
All to no avail. In fact the only success I've had is to completely remove and replace the body HTML which obviously takes all the event handlers with it as opposed to just the one I want to remove.
Any insights?
Turns out one of my attempts was the correct method but my usage was wrong. I was (unknowingly) attempting to detach the event prior to it being attached in the first place.
That said in the case of:
Y.one('body).delegate('click',...)
This works:
Y.one('body').detach('click')
Though ideally you'd call detach direct on the EventHandle returned by the delegate call.
The delegate Event method does not appear to store the handles anywhere, you could potentially create a patch replacement for Event.delegate that stores the handles against the delegate element. A basic example of patching YUI: https://gist.github.com/tivac/1424351
Untested code:
var config = {
groups : {
patches : {
base : "/js/patches/",
modules : {
"node-event-delegate-patches" : {
path : "node-event-delegate.js",
condition : {
name : "node-event-delegate-patches",
trigger : "node-event-delegate",
test : function() { return true; }
}
}
}
}
}
};
YUI.add("node-event-delegate-patches", function(Y) {
var L = Y.Lang;
Y.Node.prototype.delegate = function(type) {
var args = Y.Array(arguments, 0, true),
handle,
index = (L.isObject(type) && !L.isArray(type)) ? 1 : 2;
args.splice(index, 0, this._node);
if (!L.isArray(this._node._delegate_event_handles)){
this._node._delegate_event_handles = [];
}
handle = Y.delegate.apply(Y, args);
this._node._delegate_event_handles.push( handle );
return handle;
};
Y.Node.prototype.detachDelegates = function(){
Y.Array.each(this._node._delegate_event_handles, function(handle){
handle.detach();
});
}
});
Let's say I have a setup like this.
var Account = function(data) {
this.data = data;
this.domElement = (function(){ code that generates DOM element that will represent this account })();
this.domElement.objectReference = this;
}
Account.prototype = {
show: function() { this.domElement.classList.remove('hidden'); },
hide: function() { this.domElement.classList.add('hidden') }
}
My question is about the last line: this.domElement.objectReference = this;
It would be a useful thing to have because then I can add event listeners to my DOM element and still get access to the object itself. I can add methods that would affect my DOM element, such as hide() and show(), for instance, without having to resort to modifying visibility of the DOM element using CSS directly.
I tested this code and it works like I want it to, but I'm curious whether this would cause memory leaks or some other unpleasantness or if it's an acceptable thing to do?
Thank you!
Luka
I know this has been answered by #PaulS. already, but I find the answer counter intuitive (returning a DOM element from the Account constructor is not expected) and too DOM-centric, but at the same time the implementation is very simple, so I am not sure what to think ;)
Anyway, I just wanted to show a different way of doing it. You can store Account instances in a map and give them a unique id (perhaps they have one already), then you store that id as a data attribute on the DOM element. Finally you implement a getById function or something similar to retrieve the account instance by id from the listeners.
That's pretty much how jQuery's data works.
Here's an example with delegated events like you wanted from the comments.
DEMO
var Account = (function (accounts, id) {
function Account(data) {
accounts[this._id = ++id] = this;
this.el = createEl.call(this);
}
Account.prototype = {
constructor: Account,
show: function() { this.el.classList.remove('hidden'); },
hide: function() { this.el.classList.add('hidden'); }
};
function createEl() {
var el = this.el = document.createElement('div');
el.className = 'account';
el.innerHTML = el.dataset.accountId = this._id;
return el;
}
Account.getById = function (id) {
return accounts[id];
};
Account.init = function () {
//add delegate listeners
document.addEventListener('click', function (e) {
var target = e.target,
account = Account.getById(target.dataset.accountId);
if (!account) return;
account.hide();
});
};
return Account;
})({}, 0);
//once DOM loaded
Account.init(); //start listening to events
var body = document.body;
body.appendChild(new Account().el);
body.appendChild(new Account().el);
Why not have domElement as a variable, and return it from your function? To keep the reference to your constructed Object (but only where this is as expected), you could do a if (this instanceof Account) domElement.objectReference = this;
You've now saved yourself from circular references and can access both the Node and the Object. Doing it this way around is more helpful if you're expecting to lose the direct reference to your Account instance, but expect to need it when "looking up" the Node it relates to at some later time.
Code as requested
var Account = function (data) {
var domElement; // var it
this.data = data;
domElement = (function(){/* ... */}()); // use var
if (this instanceof Account)
domElement.objectReference = this; // assign `this`
return domElement;
};
// prototype as before
Returned element is now the Node, not the Object; so you'd access the Account instance like this
var domElement = new Account();
domElement.objectReference.show(); // for example
In my opinion there is nothing good about referencing the object inside of the object itself. The main reason for this is complexity and obscurity.
If you would point out how exactly are you using this domElement.objectReference later in the code, I am sure that I or someone else would be able to provide a solution without this reference.
We are using YUI's onclick events, but we create and delete dome nodes rapidly and this leads to memory leak.
Consider this example code below, We have many 3 nested divs many times. The top and the bottom div have YUI onclick events attached. What is the proper way to get rid of those dom elements and not leak memory:
I'm really out of any ideas. As you can see I tried implementing our own destroy function. Actually destroy works and does not leak, but it is slow.
The destroy2 function is 'copy' of the YUI destroy function where we used to debug what is the problem. It looks like the recursive clean up of YUI can not find the child nodes in the _instances dictionary
<!DOCTYPE html5>
<html>
<head>
<script src="http://yui.yahooapis.com/3.4.1/build/yui/yui-min.js"></script>
</head>
<body>
<div id="main">hi there</div>
<script>
YUI().use("node", "event", function(Y) {
window.Y = Y;
function destroy(node) {
(new Y.Node(node)).destroy();
var children = node.children;
for (var i = 0; i<children.length; i++) {
destroy(children[i]);
}
}
function destroy2(node, recursive) {
var UID = Y.config.doc.uniqueID ? 'uniqueID' : '_yuid';
// alert(1);
if (recursive) {
var all = node.all("*");
// alert(all);
Y.NodeList.each(all, function(n) {
instance = Y.Node._instances[n[UID]];
// alert(instance);
if (instance) {
destroy2(instance);
}
});
}
node._node = null;
node._stateProxy = null;
delete Y.Node._instances[node._yuid];
// node.destroy();
}
var main = new Y.Node("#main");
var divs = [];
var iter = 0;
Y.later(10, window, function() {
iter ++ ;
var i;
for (i=0; i<divs.length; i++) {
var d = divs[i];
d.parentNode.removeChild(d);
// (new Y.Node(d)).destroy(true);
//destroy(d);
//destroy2(new Y.Node(d), true);
(new Y.Node(d)).destroy(true);
}
divs = [];
for (i=0; i<1000; i++) {
var d = document.createElement("div");
var i1;
var i2;
d.appendChild(i1=document.createElement("div"));
i1.appendChild(document.createTextNode('inner 1'));
i1.appendChild(i2=document.createElement("div"));
i2.appendChild(document.createTextNode('inner 2'));
Y.on("click", function() {
alert("inner click")
}, i2);
// try to tell YUI to make Node elements
Y.Node.one(d);
Y.Node.one(i1);
Y.Node.one(i2);
// new Y.Node(d);
// new Y.Node(i1);
// new Y.Node(i2);
d.appendChild(document.createTextNode("this is div " + iter + " " + i));
Y.on("click", function(){ alert("you clicked me");}, d);
main.appendChild(d);
//divs.push(i2);
divs.push(d);
}
}, null, true);
})
</script>
</body>
</html>
I'm not sure what you're trying to accomplish here, but a few things stand out in the included code:
var Y = YUI().use(…) -- use() returns the YUI instance. No need to assign window.Y = Y;
Use Y.one(el) instead of new Y.Node(el) or Y.Node.one(el)
Use event delegation rather than subscribing to the click event on each inner div
Use Y.Node.create('<div><div>inner 1<div>inner2</div></div></div>'). You probably don't need Node instances for each div
You're likely creating more work for yourself over the long term by mixing raw DOM interaction and YUI Nodes. If you're using YUI, use YUI's APIs.
The most important of those points are #3 and #4. If you use Node.create (or append, insert, prepend, etc), the markup passed in won't have Nodes created for each element, only the outer most element. If you use event delegation, you won't need individual Nodes, which means you can add your div structures and immediately call node.destroy() (notice not passing true because the inner markup doesn't have nodes needing purging). node.destroy() will purge event listeners, which you don't have because you're using event delegation, and remove the Node from the _instances dictionary to free up memory before any user interaction. If a user clicks on one of the nodes, the event will be caught by the delegate handler and a Node will be created for the e.target element. You're free to call this.destroy() inside your event handler to re-purge the target's Node.
YMMV, considering your code snippet doesn't reflect a real use case. You can also stop into #yui on freenode to get some help or to walk through the issue.
Here is what I ended up doing to avoid the memory leaks:
var destroy = function(dom) {
var ynode = new Y.Node(dom);
ynode.purge(true);
ynode.destroy(true);
}
I have to say I'm very disapointed by the YUI documentation that says this for the YUI destroy function.
With true as a first argument, it works like node.purge(true). The
destroy method does more than detaching event subscribers. Read the
API docs for details.
http://yuilibrary.com/yui/docs/event/
I kind of consider this bug in YUI because it does not call recursively purge when you call destroy recursively. Also it looks like the above destroy function is very slow on Firefox 8 (maybe other versions too) I did write my own recursion down the dom tree and called purge
and destroy without true.
I'm attempting the surprisingly difficult task of finding out which element was clicked. I have these functions from Head First AJAX:
function getActivatedObject(e) {
var obj;
if (!e) {
obj = window.event.srcElement;
} else if (e.srcElement) {
obj = e.srcElement;
} else {
obj = e.target;
}
return obj;
}
function addEventHandler(obj, eventName, handler) {
if (document.attachEvent) {
obj.attachEvent("on" + eventName, handler);
} else if (document.addEventListener) {
obj.addEventListener(eventName, handler, false);
}
}
And my code:
mainPane = document.getElementById("mainDiv");
contactPane = document.getElementById("contactDiv");
addEventHandler(mainPane, "click", openFunction);
addEventHandler(contactPane, "click", openFunction);
function openFunction(e) {
var me = getActivatedObject(e);
//Some other stuff
}
Unfortunately, the me variable sometimes refers to the div, but it sometimes refers to the image inside the div. Even though the image has no onclick function or any other kind of event! So how can I get the div that triggered the event?
You're dealing with Event bubbling.
Basically, your click on a child element bubbles up through all of that child's parents; if those parents have a click handler bound to them, it will execute.
In pseudocode: you can see what element your current target is, and if it's not a DIV, you know that you need to look for that element's parent.
Using your example, it would look something like this (simplified a bit for your example; in reality, you'd need something much more robust):
function getActivatedObject(e) {
var obj;
if (!e) {
obj = window.event.srcElement;
} else if (e.srcElement) {
obj = e.srcElement;
} else {
obj = e.target;
}
// Images are direct children of our DIV. If our source element was an img, we should
// fetch its parent
if(obj.tagName.toLowerCase() === "img"){
obj = obj.parentNode;
}
return obj;
}
Note that this will work for your example only; in the real world, you would likely need to iterate back up the DOM tree, comparing parentNodes with the elements you are interested in until you've found what you're looking for.
Most JavaScript libraries abstract this away for you (jQuery, for example, sets a currentTarget property of all Events it dispatches that refers to the element you need, encapsulating all the traversal work for you). Makes it easier, but it's not too terrible a job to write that yourself and I'd argue against including the overhead of an entire JavaScript library if that's all you need to do. :-)
EDIT: Totally destroyed my formatting! D'oh!
I'm working on a fiddly web interface which is mostly built with JavaScript. Its basically one (very) large form with many sections. Each section is built based on options from other parts of the form. Whenever those options change the new values are noted in a "registry" type object and the other sections re-populate accordingly.
Having event listeners on the many form fields is starting to slow things down, and refreshing the whole form for each change would be too heavy/slow for the user.
I'm wondering whether its possible to add listeners to the registry object's attributes rather than the form elements to speed things up a bit? And, if so, could you provide/point me to some sample code?
Further information:
This is a plug-in for jQuery, so any functionality I can build-on from that library would be helpful but not essential.
Our users are using IE6/7, Safari and FF2/3, so if it is possible but only for "modern" browsers I'll have to find a different solution.
As far as I know, there are no events fired on Object attribute changes (edit: except, apparently, for Object.watch).
Why not use event delegation wherever possible? That is, events on the form rather than on individual form elements, capturing events as they bubble up?
For instance (my jQuery is rusty, forgive me for using Prototype instead, but I'm sure you'll be able to adapt it easily):
$(form).observe('change', function(e) {
// To identify changed field, in Proto use e.element()
// but I think in jQuery it's e.target (as it should be)
});
You can also capture input and keyup and paste events if you want it to fire on text fields before they lose focus. My solution for this is usually:
Gecko/Webkit-based browsers: observe input on the form.
Also in Webkit-based browsers: observe keyup and paste events on textareas (they do not fire input on textareas for some reason).
IE: observe keyup and paste on the form
Observe change on the form (this fires on selects).
For keyup and paste events, compare a field's current value against its default (what its value was when the page was loaded) by comparing a text field's value to its defaultValue
Edit: Here's example code I developed for preventing unmodified form submission and the like:
What is the best way to track changes in a form via javascript?
Thanks for the comments guys. I've gone with the following:
var EntriesRegistry = (function(){
var instance = null;
function __constructor() {
var
self = this,
observations = {};
this.set = function(n,v)
{
self[n] = v;
if( observations[n] )
for( var i=0; i < observations[n].length; i++ )
observations[n][i].apply(null, [v, n]);
}
this.get = function(n)
{
return self[n];
}
this.observe = function(n,f)
{
if(observations[n] == undefined)
observations[n] = [];
observations[n].push(f);
}
}
return new function(){
this.getInstance = function(){
if (instance == null)
{
instance = new __constructor();
instance.constructor = null;
}
return instance;
}
}
})();
var entries = EntriesRegistry.getInstance();
var test = function(v){ alert(v); };
entries.set('bob', 'meh');
entries.get('bob');
entries.observe('seth', test);
entries.set('seth', 'dave');
Taking on-board your comments, I'll be using event delegation on the form objects to update the registry and trigger the registered observing methods.
This is working well for me so far... can you guys see any problems with this?
You could attach a listener to a container (the body or the form) and then use the event parameter to react to the change. You get all the listener goodness but only have to attach one for the container instead of one for every element.
$('body').change(function(event){
/* do whatever you want with event.target here */
console.debug(event.target); /* assuming firebug */
});
The event.target holds the element that was clicked on.
SitePoint has a nice explanation here of event delegation:
JavaScript event delegation is a simple technique by which you add a single event handler to a parent element in order to avoid having to add event handlers to multiple child elements.
Mozilla-engined browsers support Object.watch, but I'm not aware of a cross-browser compatible equivalent.
Have you profiled the page with Firebug to get an idea of exactly what's causing the slowness, or is "lots of event handlers" a guess?
Small modification to the previous answer : by moving the observable code to an object, one can make an abstraction out of it and use it to extend other objects with jQuery's extend method.
ObservableProperties = {
events : {},
on : function(type, f)
{
if(!this.events[type]) this.events[type] = [];
this.events[type].push({
action: f,
type: type,
target: this
});
},
trigger : function(type)
{
if (this.events[type]!==undefined)
{
for(var e = 0, imax = this.events[type].length ; e < imax ; ++e)
{
this.events[type][e].action(this.events[type][e]);
}
}
},
removeEventListener : function(type, f)
{
if(this.events[type])
{
for(var e = 0, imax = this.events[type].length ; e < imax ; ++e)
{
if(this.events[type][e].action == f)
this.events[type].splice(e, 1);
}
}
}
};
Object.freeze(ObservableProperties);
var SomeBusinessObject = function (){
self = $.extend(true,{},ObservableProperties);
self.someAttr = 1000
self.someMethod = function(){
// some code
}
return self;
}
See the fiddle : https://jsfiddle.net/v2mcwpw7/3/
jQuery is just amazing. Although you could take a look to ASP.NET AJAX Preview.
Some features are just .js files, no dependency with .NET. May be you could find usefull the observer pattern implementation.
var o = { foo: "Change this string" };
Sys.Observer.observe(o);
o.add_propertyChanged(function(sender, args) {
var name = args.get_propertyName();
alert("Property '" + name + "' was changed to '" + sender[name] + "'.");
});
o.setValue("foo", "New string value.");
Also, Client Side templates are ready to use for some interesting scenarios.
A final note, this is fully compatible with jQuery (not problem with $)
Links: Home page, Version I currently use
I was searching for the same thing and hitted your question... none of the answers satisfied my needs so I came up with this solution that I would like to share:
var ObservedObject = function(){
this.customAttribute = 0
this.events = {}
// your code...
}
ObservedObject.prototype.changeAttribute = function(v){
this.customAttribute = v
// your code...
this.dispatchEvent('myEvent')
}
ObservedObject.prototype.addEventListener = function(type, f){
if(!this.events[type]) this.events[type] = []
this.events[type].push({
action: f,
type: type,
target: this
})
}
ObservedObject.prototype.dispatchEvent = function(type){
for(var e = 0; e < this.events[type].length; ++e){
this.events[type][e].action(this.events[type][e])
}
}
ObservedObject.prototype.removeEventListener = function(type, f){
if(this.events[type]) {
for(var e = 0; e < this.events[type].length; ++e){
if(this.events[type][e].action == f)
this.events[type].splice(e, 1)
}
}
}
var myObj = new ObservedObject()
myObj.addEventListener('myEvent', function(e){// your code...})
It's a simplification of the DOM Events API and works just fine!
Here is a more complete example