Knockout binding handler teardown function? - javascript

I have a knockout binding handler that uses plupload for drag and drop and ajax uploads.
To use the plupload script I create an instance of plupload which in turn is binding event listeners to DOM elements.
That works fine.
However, I have a list of "folders" and when I click a folder I display a list of files in that folder. I reuse the same DOM elements for this by binding selectedFolder().documents using foreach.
The problem I have is that in my binding handler I do all my plupload stuff in the init function and since I reuse the DOM elements they get multiple event handlers bound to them. This causes the drag and drop events to be sent to alla handlers. This means that if I drop a file on the rendered file list, the drop event fires on all previously rendered file lists too.
What I am looking for is some sort of teardown or cleanup function in the binding handler, so that I can unregister all of the events whenever a file list get unrendered (is that a word?).
Maybe we cannot detect unrendering? How would I then handle this? I would prefer not to have a global instance, since that would prevent me from using the binding on multiple places at the same time.
Sorry about not giving you any code. I'm on my cell phone atm.
Cheers!

You can register a handler that will be executed whenever KO removes elements (like when a template is re-rendered). It looks like:
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$(element).datepicker("destroy");
});
So, in your "init" function you would register a dispose callback for the element that is being bound and you would have an opportunity to run whatever clean-up code that you would like.

I believe the solution provided here will only work if Knockout is the one that removes the DOM node (ie when it rejigs templates). I had a hard time getting it to trigger under certain conditions. There might be scenarios where you need a callback to be executed regardless of how your element got removed; whether it be with Knockout, or via jQuery.html(), etc (especially in a single page application).
I brewed a different approach for adding such a hook with a little help from jQuery. Using the special events API (which are well described here), you can add a method that gets execute when a particular event is removed from a DOM node (something that happens on teardown).
If you are using Knockout in conjunction with jQuery, you can wrap this into a knockout binding to look something like this:
ko.bindingHandlers.unload = {
init: function (element, valueAccessor) {
var eventName = 'your_unique_unLoad_event'; // Make sure this name does not collide
if (!$.event.special[eventName]) {
$.event.special[eventName] = {
remove: function (o) {
o.data.onUnload()
}
};
}
$(element).on(eventName, { onUnload: valueAccessor()}, $.noop);
}
};
You can then use this on any element like this:
<div id="withViewModelMethod" data-bind="unload: aMethodOnMyViewModel" />
<div id="withInLineMethod" data-bind="unload: function() { /* ... */ }" />
I owe credits to this SO post.

Related

jQuery click event handler attached to dom element

I am having hard time while building e-commerce cart module with jquery.
Lets say that if i write a tags in html like this:
<div class="add-to-cart">+</div>
and then target it in my app:
this.$products,
this.$pa,
this.$ip,
this.$products = $('.shopperProducts'),
this.$pa = this.$products.find('.shopperAdd');
var self = this;
this.$ip = function() {
var init = function(action, product) {
/.../
};
self.$pa.on('click', function(event) {
event.preventDefault();
init('add', this);
});
};
This method is possible while im displaying products because they are displayed by php on page refresh so i have all the + links generated by php on html.
The problem is on the checkout file, this is the page when i display entire cart filled with products, cart must be generated and handled in jQuery and AJAX.
And code that i showed you doesnt work in cart page beacuse those links are appended for each product via jQuery into the DOM.
I have been study possible methods and there are few, the most in favour is to do this:
$(document).on('click', self.$pa, function(event) {
The problem with that solution is that it also is considered practice to be avoided due to high resources drain, i can see the difference in execution time myselfe, it takes a lot longer on low end devices. Is there some neat trick that can be used or method that is considered good practice to do in that situation?
<--- EDIT (Solution) --->
Instead of calling:
this.$products = $('.shopperProducts'),
this.$pa = this.$products.find('.shopperAdd');
on the beginning, i have to call it after i load elements into DOM and then they became targetable, then i just have to use self.$ip(); and event handlers can be attached. Without using any sort of workarounds, the solution was just to change order of executing commands.
There are two main strategies that you can use for adding click handlers for elements that you dynamically add to the dom.
One, You can add click handlers to the DOM element each time you create one
var addToCartButton = $('<div class="add-to-cart">+</div>');
addToCartButton.on('click', function(){
init('add', this);
};
// then you add your DOM element to the page
$('.container').append(addToCartButton);
Two, you can have a master click event listener on the page listen for all clicks where your buttons fall, and in your click handler, figure out whether the user is clicking on your element or not. This is ultimately more efficient and you don't have to add or remove event handlers each time you add elements to your page. This pattern is called event delegation, and here's another post on Stack that probably explains it better than I can
What is DOM Event delegation?
$('.container').click(function(event){
if ($(event.target).is('.add-to-cart') || $(event.target).parents().is('.add-to-cart')) {
// handle add to cart
}
})
BTW, your use of the self variable doesn't actually do anything, and neither does declaring this.$pa. You're basically accessing the property "$pa" of your this object, but not doing anything it.

jQuery scripts on ajax content WITHOUT using $(document).on

I am using an infinite scroll plugin which uses ajax.
When the 'next page' is loaded via ajax, all other ajax related scripts that are on the next page do not work. I have been told that I have to use 'delegated events'(ie change $(id).click() to $(document).on) - problem is that means editing multiple plugins and changing dozens of function calls.
Is there any way I can avoid changing everything to $(document).on and do something cool with the infinite scroll?????
I'd much rather modify the infinite scroll plugin rather than modifying other ajax related plugins to make them fit.
Unfortunately you have very few options here, and switching to delegated events is by far the best of them.
The problem is that your old code was assigning behaviour to "particular elements" when what it should really have been doing is creating page-wide responses to "certain types of actions".
I see 3 possibilities, and only one of them is guaranteed to work.
Run any scripts that are needed on new pages each time a new page is loaded. The downside here being that unless you are careful about also "tearing down" between content loads you will have behaviours repeating or colliding with each other (eg: double popups, broken animations).
Encapsulate the dynamic areas in <iframe>s. Depending on your architecture this may or may not be possible, and certainly won't be easy to integrate with some kind of infinite scrolling plugin which already expects a certain page structure.
Bite the bullet and fix the crappy code.
Loading scripts inside your ajax loaded content is a bad way to start with anyway. What you need is event delegation to attach itself to any dynamically added elements.
$("body").on("click", ".yourclass", function() {
//This function will run for every element with `yourclass` class you load via ajax
});
If you must keep using .click() then you must have a function you can call on the new content to re-hook the events every time you add more content to the page.
e: though it is worth noting that a change from .click to .on can often be handled by a properly structured find/replace
Event delegation is the correct solution. The issue is that the HTML elements on the "next page" were not part of the DOM when the page loaded. Therefore, if you did something like:
$(function() {
$('#some-element-on-the-next-page').click(function() {
foo();
});
});
Your handler did not bind.
I wouldn't attach the events to $(document). I would attach them to the closest parent which is available when the DOM loads. For example, the body tag or the fixed width wrapper which is the first child of the body (assuming your layout uses this type of structure.)
Make sure that the element that you attach to is not emptied with .empty() or repopulated with .html() as that will break the binding. Attaching the delegated handlers lower down on the DOM tree will give you better performance since the events will not have to bubble all the way up to the document node to fire your methods.
You shouldn't need to rewrite all of your functions and plugins, just the bindings to the events that fire them.
I typically use the module pattern and de-couple my method definitions from the click handlers. All of my methods are defined in the outer closure. I'll have a "document ready" section where I bind user events like clicks.
For example:
var myModule = (function() {
var public = {};
public.foo = function() {
// do something cool here
};
// document ready
$(function () {
$('#site-container').on('click', '.js-foo', function() {
public.foo();
});
});
return public;
})();
If you need to change the bindings in the future you will only need to change the call inside the document ready section.

'click' event disappears in Backbone.View after using Quicksand

There is a 'View' in the model with the event click. After using the Quicksand effects plug-in for jQuery, the objects loose their event handlers. I have tried to add the listener for the event with standard methods in backbone.js:
events: {
"click .objContact" : "openChat"
}
and the same tools jQuery delegate:
var self=this;
this.$el.delegate('.objContact','click', function(){
self.openChat();
});
and live:
var self=this;
this.$el.find('.objContact').live('click', function(){
self.openChat();
});
but the click event disappears.
What could be the problem? And how do I solve it?
UPD: Calling 'Quicksand' is in Backbone.Router (subject to change is obtained directly by means of jQuery, not Backbone), so changes are not handled in Backbone.View
UPD 2: The problem is solved in the following way - by moving the handling of the click event from the View-model to View-collection. And treated with live (did not work in on)
Simple Answer: instead of linking the function to the link with the classic ajax method that is
$('a.oldJqueryClass').click(function(){....
you need to make that function standalone, declaring a new function
function myfunction(params) {alert(params);}
than in the link you call that with the old school way:
Click here
In this way the cloned element will contain itself the call to the function and you can forget about restoring the dom integrity broken by the cloning of quicksand.
I did it in my project, it works fine.
Do a call to delegateEvents() after the related DOM entries have changed or become overwritten. In a traditional Backbone app this is typically done in the render method, but you probably need for figure out when and where quicksand does it's magic (I do not know anything about it), and call delegateEvents that will reactivate the events for the current elements in the DOM.

First plugin, how can I make it live for dynamically generated content

http://blog.jeremymartin.name/2008/02/building-your-first-jquery-plugin-that.html
Based on this tutorial, I created my first plugin and made some modifications.
But, the plugin doesn't work for dynamically generated content when I load new content on the site.
I have no idea how to go about making it live. The events are already using .on but the building of the pluggin doesn't work.
Here is the jsFiddle: http://jsfiddle.net/bT4dH/13/
I know I could call the plugin everytime I add content dynamically, but as that gets messy very quickly, I would prefer to avoid it, and integrate it directly into the plugin.
I was starting to try this:
$(document).on('DOMNodeInserted', function (e) {
if (e.target.classname === 'JS_postShrinker') {
$(e.target).shrinker(options);
}
});
But it doesn't work
UPDATE
Please ignore the DOMNodeInserted, I was just trying it. How do people in general make plugins live? I have the same problem with:
http://code.google.com/p/jquery-watermark/
http://unwrongest.com/projects/elastic/
And many more...
You can simply make it live by using .on() in a bit different way, with delegation. The documentation explains it in detail.
$(closeParent).on(event, selectorForElement, handler);
All you have to do is to choose a parent that you surely know will be part of the DOM when you attach the handler. In the worst case, you can choose body.
Then you attach the handler on this element, and specify the selector for the child elements (that can be inserted into the DOM at any time). The handler will run whenever the event is fired on any of the child elements matching the selector. This method makes use of event bubbling in a very clever way.
EDIT: Of course this will only work for event handling. If you need to initialize your elements when they are added to the DOM (resizing, decorating, etc.), you better call the plugin every time this happens. This is how most plugins work. The DOMNodeInserted event is not cross-browser and deprecated, like all Mutation events, so you should not use it.
There is an extra parameter (selector) to delegate the event on it:
$(document).on('DOMNodeInserted',"." + opts.clickZoneClass, function (e) {
if (e.target.classname === 'JS_postShrinker') {
$(e.target).shrinker(options);
}
});

how to properly unbind events in Jquery-Mobile using on/off when a page is kept in the DOM?

As Jquery Mobile keeps some pages in the DOM when navigating around, a page may be visited multiple times when going back and forth.
If I'm binding to a page like below and inside this binding perform all my page logic, which includes "nested element bindings":
// listener for the page to show:
$(document).on('pagebeforeshow.register', '#register', function() {
// stuff
// page event bindings:
$(document).on('click.register', '.registerSubmitter', function(e) {
// do something
});
});
Going back and forth causes my nested binding to be attached multiple times.
Right now trying to work around this like so (doesn't work...):
$(document).on('click', '.registrySubmitter', function(e) {
if ( $(this).attr('val') != true ) {
$(this).attr('val') == true;
// do something
}
});
So I'm only allowing the first binding to pass and then I block every other binding attempt that comes along.
While this works, it's far from optimal.
Question:
How and when should event bindings be properly unbound/offed? Is there a general way (kill all) or do I have to do this binding per binding? Maybe more importantly: Is it better performance-wise to do a binding once and keep it or bind/unbind when the user comes to/leaves the page?
Thanks for input!
EDIT:
So I'm namespacing all my events and then listen for pageHide like so:
$(document).on('pagehide.register', '#register', function(){
$(document).off('.registryEvents');
});
While this seems to unbind, it also fires when ever I close a custom dialog/selectmenu on the page, so I'm loosing my bindings before leaving the page. So partial answer, I should use off(), but how to bind to the page really being left vs. opening and closing a select menu?
When you use .on() like that, you are delegating the event handling to the document element, meaning you can setup that delegated event binding anytime you want because the document element is always available.
I've got two suggestions:
Use the pageinit or pagecreate event to only run the page-specific bindings when pages are added to the DOM and initialized. Using this method I would not delegate the event bindings within the pageinit or pagecreate event handlers because when they fire, all the elements on the pseudo-page are in the DOM:
.
$(document).on('pageinit', '#register', function() {
//note that `this` refers to the `#register` element
$(this).find('.registerSubmitter').on('click', function(e) {
// do something
});
});
Bind the delegated event handlers once and don't worry about when pages are actually in the DOM:
.
//this can be run in the global scope
$(document).on('click.register', '.registerSubmitter', function(e) {
// do something
});
Basically when you bind an event using delegation like you are, the actual CPU hit of adding the event handler is less but each time an event is dispatched (of any kind that bubbles) it has to be checked if it matches the delegated event handler's selector.
When you bind directly to elements it generally takes more time to do the actual binding because each individual element has to be bound to rather than binding once to the document element like with event delegation. This however has the benefit that no code runs unless a specific element receives a specific event.
A quick blurb from the documentation:
Triggered on the page being initialized, after initialization occurs.
We recommend binding to this event instead of DOM ready() because this
will work regardless of whether the page is loaded directly or if the
content is pulled into another page as part of the Ajax navigation
system.
Source: http://jquerymobile.com/demos/1.1.0/docs/api/events.html

Categories