Backbone.js View removing and unbinding - javascript

when my page opens, I call the collection and populate the view:
var pagColl = new pgCollection(e.models);
var pagView = new pgView({collection: pagColl});
Separately (via a Datepicker), I wish to want to populate the same collection with different models and instantiate the view again.
The problem I have is how to close the original pagView and empty the pagColl before I open the new one, as this "ghost view" is creating problems for me. The variables referred to above are local variables? is it that I need to create a global pagColl and reset() this?

well there has been many discussion on this topic actually,
backbone does nothing for you, you will have to do it yourself and this is what you have to take care of:
removing the view (this delegates to jQuery, and jquery removes it from the DOM)
// to be called from inside your view... otherwise its `view.remove();`
this.remove();
this removes the view from the DOM and removes all DOM events bound to it.
removing all backbone events
// to be called from inside the view... otherwise it's `view.unbind();`
this.unbind();
this removes all events bound to the view, if you have a certain event in your view (a button) which delegates to a function that calls this.trigger('myCustomEvent', params);
if you want some idea's on how to implement a system I suggest you read up on Derrick Bailey's blogpost on zombie views: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/.
another option
another option would be to reuse your current view, and have it re-render or append certain items in the view, bound to the collection's reset event

I was facing the same issue. I call the view.undelegateEvents() method.
Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.

I use the stopListening method to solve the problem, usually I don't want to remove the entire view from the DOM.
view.stopListening();
Tell an object to stop listening to events. Either call stopListening
with no arguments to have the object remove all of its registered
callbacks ... or be more precise by telling it to remove just the
events it's listening to on a specific object, or a specific event, or
just a specific callback.
http://backbonejs.org/#Events-stopListening

Here's one alternative I would suggest to use, by using Pub/Sub pattern.
You can set up the events bound to the View, and choose a condition for such events.
For example, PubSub.subscribe("EVENT NAME", EVENT ACTIONS, CONDITION); in the condition function, you can check if the view is still in the DOM.
i.e.
var unsubscribe = function() {
return (this.$el.closest("body").length === 0);
};
PubSub.subscribe("addSomething",_.bind(this.addSomething, this), unsubscribe);
Then, you can invoke pub/sub via PubSub.pub("addSomething"); in other places and not to worry about duplicating actions.
Of course, there are trade-offs, but this way not seems to be that difficult.

Related

how to log all events (use actions) an DOM elements that have event listeners?

I would like to record all events that are fired through user action on DOM elements. A feature like recording user actions (macro) on my website so the app can later re-generate the current state by executing use actions sequentially. How to do it?
Is there any API or solution to find all events that are processed by event listeners?
Or should I gather events by myself? If so, what would be your approach/solution/design?
this OP says no: https://stackoverflow.com/a/63346192/5078847
It would be technically possible to record such a thing by
(1) using addEventListener exclusively in your site's code (if you don't, you'll have to also iterate through all on- properties and scan for inline handlers too, which is quite a pain)
(2) Overwrite the addEventListener prototype with a custom hander that, when fired, stores information uniquely identifying the click in an array (for example, save the name of the event fired, and a full selector string to the element the event is dispatched to, and if you need it, also the amount of time since the page was loaded)
(3) When needed, save the array somewhere
(4) To emulate the user's prior actions, retrieve the array, then iterate through it. For each action, create and dispatch an event to the unique selector at the time required.
But this is really, really convoluted. It would make a lot more sense for there to be a single source of truth for what's being displayed on your page. To save a state, just serialize the object that holds the data. To resume a state, retrieve the object and render according to its contents.
There isn't anything built in, like the other post said. Chrome devtools has a function getEventListeners that gets all the handlers for a given element (singular). You also can't use this outside of Chrome's devtools.
You could (but shouldn't) hijack addEventListener from the prototype chain.
Based off of this old forum here
/** !! Foot-Gun !! **/
/** Proof of Concept/Probably Doesn't Work **/
HTMLElement.prototype._addEventListener = HTMLElement.prototype.addEventListener;
HTMLElement.prototype.addEventListener = function(eventName, handlerFunction, eventOptions) {
// reportFunction you would have to write yourself
this._addEventListener(eventName, reportFunction, eventOptions);
this._addEventListener(eventName, handlerFunction, eventOptions);
// Store an array of them on the element
if ('currentListeners' in this === false) {
this.currentListeners = [
{ eventName, handlerFunction, eventOptions }
];
} else {
this.currentListeners.push({ eventName, handlerFunction, eventOptions });
}
}
Granted, I just handed out a loaded foot-gun for anyone who wants one. It's an anti-pattern at best, it doesn't control/track state, emulate user interactions, etc. etc.
I wouldn't recommend this for "regenerating" or rerendering UI as it's gonna be more trouble than it's worth.
If you're trying to use this for debugging, there are a couple of SAAS whose whole business models are based off of this, like HotJar and Sentry.io. I'd recommend checking them out first if you're looking for a solution.

How to delete old backbone views

I have searched the internet for ways to trigger the destruction of old views.
There are functions to do this, however, I don't know how to trigger them. Ideally, there would be a way to trigger the destruction on the event of closing a view.
I can't find a way how to trigger that particular event.
You should call view.remove() to trigger its destruction as specified in the documentation http://backbonejs.org/#View-remove
For example, if you had:
var myView = Backbone.View.extend({
initialize: function() {
...
},
render: function() {
...
}
});
You can later call myView.remove() provided you have a reference to myView available.
This method should also remove any event listeners tied to the view if you are using the listenTo (recommended) method as opposed to the on listener. You could also add view.off() to ensure that the events are removed.
Additionally, you will need to add a way for views to listen to a close event so you can call the remove and off methods. You should refer to 1 and 2.
This old but fantastic piece by Derick Bailey does a great job at explaining the issue and how to solve it. As Monica rightly suggested this relies on view.remove() but you can update your router to destroy your existing view -
Try something similar to
if (currentView) {
currentView.remove();
currentView = newView();
}

How to properly destroy view on backbone?

So Im new at backbone, and Im trying to make a single page app, Im using routes to manage certain things, and I want to remove a view when the user gets to another route
Im using this method to destroy the view
destroy_view: function() {
// COMPLETELY UNBIND THE VIEW
this.undelegateEvents();
this.$el.removeData().unbind();
// Remove view from DOM
this.remove();
Backbone.View.prototype.remove.call(this);
}
also this is my route element
Router = Backbone.Router.extend({
routes: {
'':'index',
'#':'index',
'events/*event' : 'events'
},
index: function(){
this.indexView = new VistaIndex();
},
events: function(params) {
if( this.indexView )
this.indexView.destroy_view()
this.eventView = new EventView({currentEvent: params})
}
});
the problem with this is that if I do this the app crashes, so what do you recommend me to do :)
Here’s how I do it:
Backbone.View.extend({
//some other view stuff here...
destroy: function () {
this.undelegateEvents();
this.$el.removeData().unbind();
this.remove();
//OR
this.$el.empty();
}
});
First we want to make sure we’re removing all delegated events (the ones in the events:{"event selector": "callback"} hash). We do this so we can avoid memory leaks and not have mystery bindings that will fire unexpectedly later on. undelegateEvents() is a Backbone.View prototype function that removes the view’s delegated events. Simple.
Next we want to cleanup any data in the view object that is hanging around and unbind any events that we bound outside the events hash. jQuery provides a removeData() function that allows us to to do that.
You may also have bound event listeners to your view chain unbind() with no arguments to remove all previously-attached event handlers from your $el. this.$el.removeData().unbind();
Now you may want to do one of two things here. You may want to remove your view element completely OR you just want to remove any child elements you’ve appended to it during it’s life. The latter would be appropriate if, for example, you’ve set the $el of your view to be some DOM element that should remain after your view behavior is complete
In the former case, this.remove() will obliterate your view element and it’s children from the DOM.
In the later case, this.$el.empty() will remove all child elements.
Check out this fiddle if you want to fool around with my solution.
http://jsfiddle.net/oakley349/caqLx10x/

backbone.js - how to communicate between views?

I have a single page web app with multiple backbone.js views. The views must sometimes communicate with each other. Two examples:
When there are two ways views presenting a collection in different ways simultaneously and a click on an item in one view must be relayed to the other view.
When a user transitions to the next stage of the process and the first view passes data to the second.
To decouple the views as much as possible I currently use custom events to pass the data ($(document).trigger('customEvent', data)). Is there a better way to do this?
One widely used technique is extending the Backbone.Events -object to create your personal global events aggregator.
var vent = {}; // or App.vent depending how you want to do this
_.extend(vent, Backbone.Events);
Depending if you're using requirejs or something else, you might want to separate this into its own module or make it an attribute of your Application object. Now you can trigger and listen to events anywhere in your app.
// View1
vent.trigger('some_event', data1, data2, data3, ...);
// View2
vent.on('some_event', this.reaction_to_some_event);
This also allows you to use the event aggregator to communicate between models, collections, the router etc. Here is Martin Fowler's concept for the event aggregator (not in javascript). And here is a more backboney implementation and reflection on the subject more in the vein of Backbone.Marionette, but most of it is applicable to vanilla Backbone.
Hope this helped!
I agree with #jakee at first part
var vent = {};
_.extend(vent, Backbone.Events);
however, listening a global event with "on" may cause a memory leak and zombie view problem and that also causes multiple action handler calls etc.
Instead of "on", you should use "listenTo" in your view
this.listenTo(vent, "someEvent", yourHandlerFunction);
thus, when you remove your view by view.remove(), this handler will be also removed, because handler is bound to your view.
When triggering your global event, just use
vent.trigger("someEvent",parameters);
jakee's answer suggests a fine approach that I myself have used, but there is another interesting way, and that is to inject a reference to an object into each view instance, with the injected object in turn containing references to as many views as you want to aggregate.
In essence the view-aggregator is a sort of "App" object, and things beside views could be attached, e.g. collections. It does involve extending the view(s) and so might not be to everybody's taste, but on the other hand the extending serves as a simple example for doing so.
I used the code at http://arturadib.com/hello-backbonejs/docs/1.html as the basis for my ListView and then I got the following to work:
define(
['./listView'],
function (ListView) {
var APP = {
VIEWS : {}
}
ListView.instantiator = ListView.extend({
initialize : function() {
this.app = APP;
ListView.prototype.initialize.apply(this, arguments);
}
});
APP.VIEWS.ListView = new ListView.instantiator();
console.log(APP.VIEWS.ListView.app);
}
);

How to attach an event listener to an object in JS

This seems possible as http://www.knockoutjs.com appears to be doing it. I haven't been able to make enough sense of their code-base to get a similar pattern working though.
Effectively I have a MVVM style application with the UI based on jQuery tabs. Each tab is represented by a view model that I want to be able to validate and fire events based on changes in the model.
I create a representation of my data similar to the following on page load:
$(document).ready(function(){
thisTab = new ThisTab();
});
function ThisTab(){
Load: {Load from my model}
Save: {Save/Persist model to the db (via web service call)}
Validate: {
this.Item1 = function(){Validate item 1, do work, refresh fields, whatever.}
}
}
The model itself is a complex global object and changes to the DOM (inputs, etc.) immediately update the object. Changes to some of those properties should call their associated validate items thisTab.Validate.Item1. I have no issue raising events from the changes. If I bind that event listener to a random DOM element I can call my routines without issue and everything works beautifully. It does seem strange, however, to attach the event to a non-related DOM object.
So the question is: how can I do something like thisTab.addEventListner("someEvent") or $(thisTab).bind("someEvent"), where thisTab is not a DOM element, but instead is a native object. Trying to do it, I always get an error that "this method is not supported".
Attaching an event to a standard object does not use the same methods; basically, you would implement your own eventing like so:
function ThisTab()
{
listeners: [],
addListener: function(callback) { this.listeners.push(callback); },
load: { // Finds DOM elements and such, and attaches to their events. The callback from the DOM event should be a method on your object },
yourDomEventCallback: function()
{
for(var j = 0; j < this.listeners.length; j++)
this.listeners[j]();
}
}
The above code should be used as a starting point, since I just cobbled it together and there are likely syntax errors. Basically, you have taken your object and mapped onto events you want to capture, and then expose methods to attach callback methods that you will call when the hidden DOM events occur. You wont be able to use jQuery's abstractions for DOM events, because such events have no meaning on your custom object.
Bind the event to your regular JS object as you would do for a DOM object.
$(thisTab).bind("someEvent", function() {
// handler's code here
});
See this example. Using this has one side-effect that jQuery will add a housekeeping identifier as a property on the object - it looks something like
jQuery1510587397349299863.
This property named jQuery<timestamp> is added to all DOM objects that have events or data associated with them, and regular objects behave similarly. If you are uncomfortable with your model objects being modified, then use your own callback mechanism which should be fairly easy to add.

Categories