I am creating a backbone view with custom events. If I remove the backbone view, the remove process unsubscribe the events or I have to manually unsubscribe the events.
Similarly, I created a master view with some child views. If I remove the master view, will all my child events will unsubscribe or I have to unsubscribe the child events and unsubscribe the master view events.
Please suggest me an approach where I can remove the views in a proper order so that no memory leaks will happen.
You need call remove() method for each of your child views and then call native remove() method on your view. Native remove will stop listening to events and remove $el from the DOM.
Here is an example:
var View = Backbone.View.extend({
initialize: function() {
this.views.my_view_1 = new Backbone.View();
this.views.my_view_2 = new Backbone.View();
return this;
},
/*
* Remove child views and remove itself
*/
remove: function() {
// Remove the child views
Object.keys(this.views).forEach(function(view_name) {
if (is(this.views[view_name].remove, "function")) {
this.views[view_name].remove();
}
}, this);
// Call the native remove function.
Backbone.View.prototype.remove.apply(this, arguments);
// For old browsers we need convert arguments object to Array
// Backbone.View.prototype.remove.apply(this, Array.prototype.slice.call(arguments));
return this;
}
});
Related
What is a good method for sending a message from a Child view to its parent Collection view in Backbone.js or Marionettejs?
Normally I send the message through the collection:
ChildView = Backbone.Marionette.ItemView.extend({
send_message: function(){
this.model.collection.trigger('some-message');
}
})
ParentCollectionView = Backbone.Marionette.CollectionView.extend({
// ON RENDER
onRender: function(){
this.listenTo(this.collection, 'some-message', this.do_something);
}
// DO SOMETHING
do_something: function(){
alert('did something');
}
});
I think this is not right because:
I'm sending the message from the child view, through the data, back to the parent view
In this instance, the message does not relate to the data, its strictly message passing between views about view stuff
The model could belong to more than one collection
Instead, I would like to send a message directly from a child view to its parent collection view. (actually, I'm using a composite view, not sure if that matters, wanted to keep the example simple though).
Either have the child view directly emit the event and have the parent listen for it :
ChildView = Backbone.Marionette.ItemView.extend({
send_message: function(){
this.trigger('some-message');
}
})
ParentCollectionView = Backbone.Marionette.CollectionView.extend({
// ON RENDER
onRender: function(){
// no idea how Marionette references its children views
// let's say this.subview is a reference to your child view
this.listenTo(this.subview, 'some-message', this.do_something);
}
// DO SOMETHING
do_something: function(){
alert('did something');
}
});
Or use a dedicated event emitter you inject into your subview(s)
ChildView = Backbone.Marionette.ItemView.extend({
send_message: function(){
this.channel.trigger('some-message');
}
})
ParentCollectionView = Backbone.Marionette.CollectionView.extend({
initialize: function(){
this.channel = _.extend({}, Backbone.Events);
this.listenTo(this.channel, 'some-message', this.do_something);
},
// ON RENDER
onRender: function(){
// pass the channel to the child
// that probably should be done when the child is created
this.subview.channel = this.channel;
},
// DO SOMETHING
do_something: function(){
alert('did something');
}
});
Marionette has a handy function called triggerMethod that can send events from the child to the parent.
this.triggerMethod("someMethod", data1...)
That event is caught by the parent using an onChildview listener
onChildviewSomeMethod(childView, data1param, ...) {}
I have Backbone view that has sub-views and each of the could stay "active" (just click or contextmenu). And I need to get view reference to that active sub-view from parent view. What is the correct way to do it?
My view hierarchy looks like the following:
var OuterView = Backbone.View.extend({
initialize: function() {
this.children = {};
this.child = new Backbone.View();
this.children[this.child.cid] = this.child;
},
render: function() {
this.$el.html('<div data-view-cid="' + this.child.cid + '"></div>');
_.each(this.children, function(view, cid) {
this.$('[data-view-cid="' + cid + '"]').replaceWith(view.el);
}, this);
}
};
The approach I prefer is not to have active and inactive views, but to only render the view that is active, and to remove them when not needed.
In other words, the easiest way to handle state is to make things stateless.
Simplest solution would be to delegate the parent view to listen to 'click div' and in the callback get the child view by $(event.currentTarget).closest('[data-view-cid]')
The child view should not be aware of the parent view to avoid creating zombies, otherwise you might have to clean up the references.
I am facing a problem while trying to click submit after re-render.
This is my view:
ShareHolderInfoView = Backbone.View.extend( {
template : 'shareholderinfo',
initialize: function() {
this.model = new ShareHolderInfoModel();
},
render : function() {
$.get("shareholderinfo.html", function(template) {
var html = $(template);
that.$el.html(html);
});
//context.loadViews.call(this);
return this;
},
events:{
"change input":"inputChanged",
"change select":"selectionChanged",
"click input[type=submit]":"showModel"
},
inputChanged:function(event){
var field = $(event.currentTarget);
var data ={};
data[field.attr('id')] = field.val();
this.model.set(data);
},
showModel:function(){
console.log(this.model.attributes);
alert(JSON.stringify(this.model.toJSON()));
}
});
This is my Router
var shareholderInfo, accountOwnerInfo;
App.Router = Backbone.Router.extend({
routes:{
'share':'share',
'joint':'joint'
},
share:function(){
$("#subSection").empty();
if(!shareholderInfo){
shareholderInfo = new ShareHolderInfoView();
$("#subSection").append(shareholderInfo.render().el);
} else{
$("#subSection").append(shareholderInfo.$el);
}
},
joint:function(random){
$("#subSection").empty();
if(!accountOwnerInfo){
accountOwnerInfo = new AccountOwnerInfoView();
$("#subSection").append(accountOwnerInfo.render().el);
} else{
$("#subSection").append(accountOwnerInfo.$el);
}
}
});
This is my HTML a div with id='subSection'.
if I check in console, I can able to see the events bound to that view.
Object {change input: "inputChanged", change select: "selectionChanged", click input[type=submit]: "showModel"}
But its not calling that showModel function afer i click submit. Please help.
Your fundamental problem is that you're improperly reusing views.
From the fine manual:
.empty()
Description: Remove all child nodes of the set of matched elements from the DOM.
[...]
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So when you say:
$("#subSection").empty();
you're not just clearing out the contents of #subSection, you're also removing all event handlers attached to anything inside #subSection. In particular, you'll remove any event handlers bound to accountOwnerInfo.el or shareholderInfo.el (depending on which one is already inside #subSection).
Reusing views is usually more trouble than it is worth, your views should be lightweight enough that you can destroy and recreate them as needed. The proper way to destroy a view is to call remove on it. You could rewrite your router to look more like this:
App.Router = Backbone.Router.extend({
routes: {
'share':'share',
'joint':'joint'
},
share: function() {
this._setView(ShareHolderInfoView);
},
joint: function(random){
this._setView(AccountOwnerInfoView);
},
_setView: function(view) {
if(this.currentView)
this.currentView.remove();
this.currentView = new view();
$('#subSection').append(this.currentView.render().el);
}
});
If your views need any extra cleanup then you can override remove on them to clean up the extras and then chain to Backbone.View.prototype.remove.call(this) to call the default remove.
If for some reason you need to keep your views around, you could call delegateEvents on them:
delegateEvents delegateEvents([events])
Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
and you'd say things like:
$("#subSection").append(shareholderInfo.$el);
shareholderInfo.delegateEvents();
instead of just:
$("#subSection").append(shareholderInfo.$el);
I'd strongly recommend that you treat your views and cheap ephemeral objects: destroy them to remove them from the page, create new ones when they need to go on the page.
In my router object, I created an event object to share among my views
I pass the event object to my views
I register events to this shared object like this
var productCatalogView = Backbone.View.extend({
initialize: function (options) {
//bind alert event to sharedEvents
options.sharedEvents.bind("alert", this.alert,this);
},
alert: function () {
alert('alerted');
}
});
//The following view triggers the alert event
var testView = Backbone.View.extend({
initialize: function (options) {
this.sharedEvents = options.sharedEvents;
},
events: {
'click #test': 'triggerAlert'
},
triggerAlert: function (e) {
this.sharedEvents.trigger("alert", null);
}
});
THE PROBLEM:
The problem I experience is that the first time I click on the button which triggers the alert event (second view), the alert event gets called once (good), this causes the first view to be re-rendered by triggering the route passing search parameters, therefore creating the first view and binding the sharedEvents again, hence when I trigger the alert event a second time, it gets triggered twice (bad), the next time I repeat the same process, it gets triggered 3 times, and so on and so forth. I guess it has to do with the event binding in the first view, it occurs more than once, i.e each time the view is initialized (if I am correct)
please how can I make the binding of the event occur once.
Here is my router which shows how I initilze the views:
var Router = Backbone.Router.extend({
sharedEvents:_.extend({},Backbone.Events),
catalog: function (id) {
//....unecessary code left out
var productView = new ProductView({sharedEvents:this.sharedEvents});
this.renderView(productView);
this.renderView(new testView({sharedEvents: this.sharedEvents }));
}
renderView: function (view) {
if (null != this.currentView) {
this.currentView.undelegateEvents();
// this.currentView.remove();
}
this.currentView = view;
this.currentView.render();
}
});
I have tried this solution but problem persists, thanks
Try using Backbone.Events' listenTo method instead of the bind method. Then, in your renderView(), call this.currentView.remove instead of this.currentView.undelegateEvents.
Rationale:
I believe in your renderView() method, you are using undelegateEvents() thinking it releases all event listeners created by your view. It only releases events bound on to your view's $el element. However, using remove() on the view releases events bound to the $el as well as events created using this.listenTo() (and this.listenOnce()).
Now once you render another view, the old currentView will be properly released and you'll only get one alert.
I am trying to add a simple event to the children under my compositeview but it is not triggering at all..and frankly I am not sure why, it seems so simple, I could do this just fine with normal backbone.view.
In the example below, the alert is not triggered at all, however when I purposefully change the function name the event binds to , to something else that doesnt exist, it complaints that the function doesnt exist, so I think it's something else...help?
App.View.ContentContainer = Backbone.Marionette.CollectionView.extend({
className:'content_container',
itemView:App.View.ContentBrowseItem,
events:{
'click .browse_item':'focus_content'
},
initialize:function () {
//this.views = {} //indexed by id
//this.create_modal_container()
var coll = this.collection
coll.calculate_size()
coll.sort_by('title', -1)
},
focus_content:function (e) {
alert('here???')
var $modal_container = this.$modal_container
var content_id = $(e.currentTarget).data('content-id')
var $selected_view = this.views[content_id]
var $focused_content = new App.View.FocusedItem({model:$selected_view.model})
$modal_container.empty().show().append($focused_content.el).reveal().bind('reveal:close', function () {
$focused_content.close()
})
return false
},
onShow:function(){
this.$el.addClass('content_container').isotope({
selector:'.content_item',
resizable:true,
layoutMode:'masonry',
masonry:{ columnWidth:64 }
})
}
EDIT: this is the resulting HTML: http://pastebin.com/uW2X8iPp the div.content_container is the resulting el of App.View.ContentContainer
Is .browse_item a selector for the App.View.ContentBrowseItem itemView element? In that case, you need to bind the event in the ItemView definition, not in the CollectionView definition. The reason is that events are bound when a view is rendered. The CollectionView itself is rendered before any of its child itemViews.
Also, if you are opening up another modal view on this click event, I would let the app handle that, rather than your CollectionView
Try something like this:
App.View.ContentBrowseItem = Backbone.Marionette.ItemView.extend({
...
initialize: function() {
// Maintain context on event handlers
_.bindAll(this, "selectItem")
},
events: {
"click" : "selectItem"
}
selectItem: function() {
App.vent.trigger("item:select", this.model);
}
...
});
And to actually show the modal detail view:
App.vent.on("item:select", function(itemModel) {
var detailView = new App.View.FocusedItem({ model: itemModel });
// You may also want to create a region for your modal container.
// It might simplify some of your `$modal_container.empty().show().append(..).etc().etc()
App.modalRegion.show(detailView);
});
Allowing each of your views to handle their own events is part of what makes Backbone and Marionette so beautiful. You'll just want to avoid one view getting all up in another view's business (eg. a CollectionView trying to handle its ItemView's events, an ItemView creating event bindings to show and close a separate modal view, etc.)
Hope this helps!