Problem Fiddle:
Click To View (Attempt #1, using Delegated Events)
Click To View (Attempt #2, brute force approach as below)
Click To View (Attempt #3, refactored, has problem I am trying to solve)
On a project I'm working with, I'm exploring a rather dynamic form. In addition to some static elements, there are various interactive elements, which can be cloned from a hidden 'template' markup set and added at various points in the business process.
Because of the dynamic nature, my tried-and-true method of setting up a jQuery element cache and event handlers on-load, then letting the user do whatever isn't working out, because of this dynamic nature; I was finding that my dynamically-added elements had no click events.
To solve this problem, I manually set up a rebind method for each scripted element in question. The rebind process involves A) re-acquiring the set of elements for a given descriptive selector, B) dropping any existing events on that cache, as those events apply to an incomplete element set, and C) calling a bind method to apply the new events to the entire set.
The brute-force, working way that I got, had this going on:
var $elementCache = $('.some-class');
function rebindSomeLink() {
// Re-acquire the element cache...
$elementCache = $('.some-class');
// Drop all existing events on the cache...
$elementCache.unbind();
// Call a bind function to establish new events.
bindSomeLink();
}
function bindSomeLink() {
$elementCache.click(function (e) {
// ...Behavior...
});
}
// There are four other links with a similar rebind/bind function relationship set up.
Naturally, I seized on the rebind being repeated so often with nearly the exact same code - ripe for a refactor. We have a common library namespace, where I added a rebindEvents function...
var MyCommon = function () {
var pub = {};
pub.rebindEvents = function($elementCache, selector, bindFunction) {
$elementCache = $(selector);
$elementCache.unbind();
bindFunction();
};
return pub;
}();
Upon trying to call that, and run the site, I immediately stubbed my toe on an UncaughtTypeError: method click cannot be called on object undefined.
As it turns out, it seems when I call the following:
MyCommon.rebindEvents($elementCache, '.some-class', bindSomeLink);
The $elementCache is not being passed to the rebindEvents method; when I step to it in my debugger, $elementCache inside of rebindEvents is undefined.
Some handy StackOverflow research revealed to me that JavaScript does not have referential-passing, at least in the C/C++/C# sense that I am familiar with, which leads me to my two Questions:
A) Is it even possible for me to refactor this rebind functionality with a cache reference pass of some sort?
B) If it's possible for me to refactor my rebind function to my common namespace, how would I go about doing it?
Use Jquery On to bind events at an element high-up in the DOM that is always present.
http://api.jquery.com/on/
This is the simplest way to handle event binding to dynamically created elements.
here is a jsfiddle that shows an example.
$('#temp').on('click', 'button', function(){
alert('clicked');
});
$('#temp').append('<button>OK</button>');
The event is bound to a div, which later has a button dynamically added. Because the button has no click event, the event "bubbles" up the DOM tree to its parent element which does have a click event for a button, so it handles it and the event "bubbles" no further.
Related
Is it a good idea to manage all click events under the document element? The DOM is being constantly manipulated, so instead of constantly registering new events for each newly created DOM element, can't I just assign one event handler on the document element? For example:
document.onclick = function(event) {
switch(event.target.id) {
case 'someid':
// SOME ACTION
break;
case 'someotherid':
// SOME OTHER ACTION
break;
default:
// A CLICK WITH NO ACTION
}
};
Yes. This pattern is called event delegation, you can find a great article on the blog of David Walsh
You should also take a look at the Element matches / matchesSelector API
-https://developer.mozilla.org/es/docs/Web/API/Element/matches
-https://davidwalsh.name/element-matches-selector
You can do this, but it's not as efficient as binding events to specific elements. It means your function will run if someone clicks in a place that isn't mentioned in any of your cases. And even if it is, it will have to search sequentially through your cases until it finds the right one.
A somewhat better way to do it is to use an object keyed off the IDs.
var handlers = {
"someid": function(event) { // some action
},
"someotherid": function(event) { // some other action
},
...
}
document.onclick = function(event) {
if (handlers[event.target.id]) {
handlers[event.target.id](event);
} else {
// default action
}
}
This addresses the sequential searching problem, but it still runs when someone clicks on an unbound element. This probably isn't much of an issue for clicks, but imagine doing the same thing for mouse movement events, which occur almost constantly.
Also, this doesn't generalize easily to binding handlers to classes or more complicated selectors.
What you're doing is similar to how jQuery implements .on() event binding, when you write:
$(document).on("click", "someSelector", handlerFunction);
This form is generally only used when specifically needed, which is when the elements that match the selector are created dynamically -- it allows you to define the handler once, not add and remove it as elements change. But for static elements, we generally use the simpler
$("selector").on("click", handlerFunction);
because then the browser takes care of running the handler only when one of the selected elements is clicked.
I have a situation where I am using the data attribute named data-command in many instances throughout a specific section of a site and instead of binding tons of separate click events I decided to just use the one and use a switch such as:
$('[data-command]').on('click', function(event) {
// Prevent default click action
event.preventDefault();
// Get command
var command = $(event.target).data('command');
switch (command) {
// Do stuff...
}
// Prevent default click action (IE 8)
return false;
});
However it has just become an issue when trying to get it to work on data loaded via AJAX.
This obviously works..
$('#existing_element').on('click', '[data-command]', function(event) {
...but since it is supposed to work on many different pages in that section of the site the above wouldn't work on all pages.
I could just make sure to give a specific id to the parent wrapper where I load all my ajax data, but that would mean making two separate binding events with a bunch of the same code.
I also could do this to cover all bases..
$(document).on('click', '[data-command]', function(event) {
...but that's probably not such a wise idea binding an element to the document.
Edit: Html data is being loaded into the DOM via jQuery's html method.
Any clean way I can handle this or should I just create two different binding events to handle each situation?
Event delegation is the best approach to bind events on dynamically created elements. Since you don't want to use event delegation, use following approach to bind events.
$('[data-command]').off('click').on('click', clickHandler);
// Somewhere in the same scope
function clickHandler(e) {
// Handle click event here
}
Add this after the dynamically created elements are added using html().
off('click') will first unbind the click event handlers that are applied previously and then on('click', will bind the click handler on all the elements matching selector.
Edit
This seems to be repeating the same code again and again. Can't I keep it DRY?
Yes, you can keep the code DRY and clean by creating a function to bind events and call the same function when you want to bind event.
function clickHandler(e) {
// Handle click event here
}
function bindEvent() {
$('[data-command]').off('click').on('click', clickHandler);
}
$(document).ready(bindEvent);
...
$.ajax({
...
success: bindEvent
....
I have this bit of code that monitors clicks on <div class="selectable_item">
$(function(){
$("#matchres .selectable_item").on("click", function(){
console.log('Sending request')
$.post("/request", $.param({'crit_id': this.id}), function(){}).fail(function(){console.log("matchres error...");});
return true;});
});
What I'm noticing is when I use the chrome console, for example, to see if there are any $("#matchres .selectable_item"); it finds them, and if I define in the console $("#matchres .selectable_item").on("click", function(){console.log('hi')}); the action is as expected and the console logs correctly. But what I showed you above does not work. Any ideas why that is? Any help would be very much appreciated. As added information, I'm using jquery v1.10.2.
#Hanlet's idea is correct, at the time of document load those items don't exist because you're dynamically creating them, and they do exist by the time you interact with them in the developer console. What you want to do is bind the event handler to a delegate, or an object that will listen for events on child elements.
What you do not want to do is add delegate callbacks to the document when avoidable. Any click on the document will have to check against its event target to see if it should trigger this document delegate callback. You do this enough times and it becomes a performance concern. Instead, pick the closest ancestor element that isn't dynamically created.
For instance, if you're creating .selectable_item dynamically but not #matchres, then add this:
$('#matchres').on('click', '.selectable_item', function () { ... });
Because you add these dynamically, this could be an event delegation issue, very common. Try this instead:
$(document).on("click", "#matchres .selectable_item", function(){ ... }
The problem consists mainly in the fact that you bind these when the DOM is first built, and then you add more elements dynamically, but the event is not bound to these new elements.
Look at these two examples:
http://jsfiddle.net/hescano/aKfWf/
and
http://jsfiddle.net/hescano/aKfWf/1/
I've discovered a resource leak on a webpage I'm working on.
This webpage has two textfields, that upon click show a modal dialog, perform a data request to the backend, and then present that information in a table that the user can select an entry from for use in the textbox they originally clicked.
I'm binding the click events to the textboxes like so:
var $field = $('#one-of-the-text-fields');
$field.click(function () {
App.DialogClass.show();
App.DialogClass.PopulateTable();
App.DialogClass.GotoPageButtonAction(actionArgs); // Offender!
});
...Which calls...
App.DialogClass = (function($) {
var pub {},
$gotoPage = $('#pageNumberNavigationField'),
$gotoPageButton = $('#pageNumberNavigationButton');
// ...SNIP unimportant other details...
pub.GotoPageButtonAction = function (args) {
$gotoPageButton.click(function () {
var pageNumber = $gotoPage.val();
pub.PopulateTable(args); // Breakpoint inserted here...
});
};
return pub;
})(jQuery);
I noticed the leak because when I ran through using Chrome's JavaScript debugger, I'm always having one extra breakpoint hit every time I click a different button (e.g. the first time I click field A, the breakpoint is hit twice. When I hit field B after that, the break point is hit three times. If I click A after that, the breakpoint is hit four times. Extrapolate as necessary.)
Nowhere in my code am I doing anything about an existing click event for a given field. I suspect my leak stems from the fact that the events are not getting cleaned up. That being said, I am also not terribly familiar with JavaScript/jQuery. What are some techniques for removing click events from a control?
Sure. Just unbind them:
$field.unbind('click');
However, bear in mind that this will remove all event handlers for click, not just yours. For safety, you should use namespaces when binding handlers:
$field.bind('click.mynamespace', function(){
// do something
});
Then,
$field.unbind('click.mynamespace');
So, then, only your handler will be removed.
If you have used .bind() to bind them .unbind() removes the events
If you have used .on() to bind them .off() removes the events
JQuery offers the unbind function to unbind event listeners.
Note that you may also do it in vanilla JS using removeEventListener.
But instead of unbinding, you probably should not bind each time in GotoPageButtonAction : once is enough.
I am using Jquery to dynamically add some HTML into a page.
Now this new HTML code should trigger additional Jquery functions to enable more processing to be done but this new HTML code isnt recognized and thus the additional Jquery functions arent triggered.
How can I get the new HTMl code to be recognized and the additional functions triggered?
Thanx
It depends on what you want to do. The first thing to look into would be jQuery's .live() methods. You can associate events to matching elements that either exist or will exist in the future. For example, this click method will only bind to existing elements with the class of 'clickme'
$('.clickme').bind('click', function() {
// Bound handler called.
});
However, if you bind it using the.live() methods then it will work for existing elements and any new elements that are created:
$('.clickme').live('click', function() {
// Live handler called.
});
These examples are taken right off the API page for the live method. Check it out here: http://api.jquery.com/live/
There are 2 concerns normally, event handlers and plugins, which are two different things.
Part 1: Event Handlers
Event handlers are easy, because they act upon events, events behave identically no matter when the element as added. For this there's .live() and .delegate(), .live() listens for events on document and runs if an event comes from an element that matches the selector, let's take a table row for example:
$("tr").click(function() { ... });
This would find all current table rows, when it ran and bind a click event handler to them, the same as .bind('click', function). Then there's .live(), like this:
$("tr").live('click', function() { ... });
This listens for the click event to bubble up to document (this happens automatically, by default) and executes the handler...current and future elements behave the same way here. This means it works for both. Then there's .delegate() which is a local version of .live() like this:
$("#myTable").delegate('tr', 'click', function() { ... });
If you're just adding rows to #myTable but not removing/adding the table itself, the same type of listener for bubbling events can sit there, instead of all the way up on document, this means the event has to bubble fewer times before reaching the handler you want to execute.
Part 2: Plugins
Plugins are a bit trickier, because they take elements and do things with them (this is true for most plugins). You have two decent options here, either running the plugin when new elements yourself, for example loading via $.ajax() or a shorthand version would look like this:
$.ajax({
//options...
success: function(data) {
//add elements
$("tr", data).myPlugin();
}
});
This finds new <tr> elements, but only in a local context (in the returned HTML) and executes only on those elements. Alternatively, there's a plugin for this, less efficient, but usually not a noticeable difference on most pages. The .livequery() plugin actively looks for and acts up new elements, the same code would look like this:
$("tr").livequery(function() {
$(this).myPlugin();
});
Either of these are valid solutions, just see which fits your needs better.
More details might be helpful but it sounds like Jquery.live() might be what you need. Jquery.live() binds handlers to elements dynamically.