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();
}
Related
I am having an issue in my Backbone Marionette application where my child views are not being destroyed completely. How do you properly destroy a nested layout view that you are replacing with another layout/item view?
I was under the impression from the Marionette documentation on destroying layout views, that when I set a region to display a new view, that the old view is destroyed. However, events that are triggered via vent are still visible by the old view that was supposedly destroyed.
I created a sample of this issue here: https://jsfiddle.net/dhardin/5j3x2unx/
I believe the issue stems from my router:
App.Router = Marionette.AppRouter.extend({
routes: {
'': 'showView1',
'view1': 'showView1',
'view2': 'showView2'
},
showView1: function() {
var view1 = new App.View1();
App.Layout.mainRegion.empty();
App.Layout.mainRegion.show(view1);
},
showView2: function() {
var view2 = new App.View2();
App.Layout.mainRegion.empty();
App.Layout.mainRegion.show(view2);
}
});
The App.Layout.mainRegion.empty() is not required to my understanding as that is taken care of when the view is destroyed in the Region Manager's show() function.
To see the issue, navigate to another view via navigation, and click the button. You will see that the alert is fired for both the old view and the new view.
Back in my pre-marionette applications, I followed a clean-up pattern to avoid these memory leaks discussed here.
Essentially, my displayed view would call the following function when my app changes to a new view:
Backbone.View.prototype.close = function(){
this.remove();
this.unbind();
}
Please let me know if you need any additional info. Thanks in advance!
For cases such as this you should take advantage of the onDestroy function to do additional clean-up work beyond what Marionette provides. Marionette automatically calls onDestroy when a view is replaced or removed.
onDestroy: function() {
App.vent.off('ButtonClicked', this.onButtonClicked, this);
}
From the Marionette documentation:
By providing an onDestroy method in your view definition, you can
run custom code for your view that is fired after your view has been
destroyed and cleaned up. The onDestroy method will be passed any arguments
that destroy was invoked with. This lets you handle any additional clean
up code without having to override the destroy method.
See the working fiddle here: https://jsfiddle.net/ocfn574a/
Note that I did update a typo in your routes config: 'showVeiw1' -> 'showView1'
You should be using this.listenTo(App.vent, 'ButtonClicked', this.onButtonClicked) instead of App.vent.on('ButtonClicked', this.onButtonClicked, this); this way marionette takes care to take off all the listeners when the view is destroyed and you do not need to explicitly handle onDestory event to take off the listener. see the updated fiddle here.
So basically there is no issue in your router but there is issue in registering the listener since the listener is not present in the view object it is not getting unregistered.
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/
So, I have a layoutView, lets call it MyLayoutView. In this layout view I
have two childViews:
return Marionette.LayoutView.extend({
template: templates.pages.layoutTpl,
regions: {
regionTest1: "#test1-view",
regionTest2: "#test2-view"
},
childEvents: {
'my:event': function (childView, param) {
console.log(param); //its ok
}})
and in the childView(CompositeView), I trigger an event, like this:
this.triggerMethod('my:event', [1,2,3]);
works fine. How can I listen to this event from the other childView?
So far i do it in the LayoutView:
'my:event': function (childView, params) {
this.getRegion('regionTest2').currentView.trigger('my:event2');
},
and in regionTest2 I listen to the event:
//in the secord childView
this.on('my:event2', function() {//do whatever...})
to catch the event in the other childView. OK it works, but I think the writing is a little ugly.. any suggestions to it better?
You're mostly on the right track if you want to use your parent layout view to mediate events between the child views(you can also use triggerMethod instead of triggers).
However, the reason it feels ugly is because of the use of a parent layout view as a mediator to connect its seemingly disjoint child views. There are many approaches and messaging and eventing patterns to solve this, as well as few plugins like Backbone.Radio(marionette is slated to replace wreqr with radio as dependency). If it is possible, I would suggest leveraging Backbone Radio's Channels to better namespace and manage your events(e.g. creating and managing channels based on their purpose), and also to help decouple your views.
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.
I'm pretty new to Backbone so excuse me if this question is a little obvious.
I am having problems with a collection inside of a model. When the collection changes it doesn't register as a change in the model (and doesn't save).
I have set up my model like so:
var Article = Backbone.Model.extend({
defaults: {
"emsID" : $('html').attr('id')
},
initialize: function() {
this.tags = new App.Collections.Tags();
},
url: '/editorial_dev.php/api/1/article/persist.json'
});
This works fine if I update the tags collection and manually save the model:
this.model.tags.add({p : "a"});
this.model.save();
But if the model is not saved the view doesn't notice the change. Can anyone see what I am doing wrong?
initialize: function() {
this.tags = new App.Collections.Tags();
var model = this;
this.tags.bind("change", function() {
model.save();
});
},
Bind to the change event on the inner collection and just manually call .save on your outer model.
This is actually an addendum to #Raynos answer, but it's long enough that I need answer-formatting instead of comment-formatting.
Clearly OP wants to bind to change and add here, but other people may want to bind to destroy as well. There may be other events (I'm not 100% familiar with all of them yet), so binding to all would cover all your bases.
remove also works instead of destroy. Note that both remove and destroy fire when a model is deleted--first destroy, then remove. This propagates up to the collection and reverses order: remove first, then destroy. E.g.
model event : destroy
model event : remove
collection event : destroy
collection event : remove
There's a gotcha with custom event handlers described in this blog post.
Summary: Model-level events should propagate up to their collection, but can be prevented if something handles the event first and calls event.stopPropagation. If the event handler returns false, this is a jQuery shortcut for event.stopPropagation();
event.preventDefault();
Calling this.bind in a model or collection refers to Underscore.js's bind, NOT jQuery/Zepto's. What's the difference? The biggest one I noticed is that you cannot specify multiple events in a single string with space-separation. E.g.
this.bind('event1 event2 ...', ...)
This code looks for the event called event1 event2 .... In jQuery, this would bind the callback to event1, event2, ... If you want to bind a function to multiple events, bind it to all or call bind once for each event. There is an issue filed on github for this, so this one will hopefully change. For now (11/17/2011), be wary of this.