How to get active Backbone view correctly? - backbone.js

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.

Related

Is there a way to re-render child view on any custom JQuery event in Backbone/Marionette?

I want to re-render my child view that is being rendered in a composite view on $(window).resize() event which I have subscribed in onShow() of my composite view. Can we do do that? if yes, is there a preferred way? I want to something like this:
define([
"app",
"views/list-item",
], function(App, ListItem) {
var List= App.CompositeView.extend({
template: "list",
childViewContainer: ".list-items",
childView: ListItem,
onShow: function() {
$(window).on('resize', function() {
//re-render child View(ListItem)
});
}
});
return List;
});
According to this PR CompositeView has a renderChildren method, depending n your version of marionette. If it is old it might be private _renderChildren.
Side note: having global selectors and events inside a view is bad practice. I'd put that logic in a separate script outside of the view.
You can implement 'modelEvents' in parent view. On change of 'modelEvents' call render method which will re-render the view. For example:
"modelEvents" :{
"change:updateView": "render"
}
And toggle the 'updateView' model to call this event.

backbone js view event binding only to views elements

Hi I'm learning backbone and I am having trouble with binding events to views. My problem is that I have a view constructor that when called, binds all views to a button press event that is only part of one view. I would like the button press event to be bound to only the 1 view that contains the button.
http://jsbin.com/tunazatu/6/edit?js,console,output
click on all of the view buttons
then click back to view 1
click the red button (all view's models console.log their names)
So I've looked at the code from this post mutliple event firing which shows that you can have multiple views that have the same el thru tagName but map events only to their html elements. This is also what is done in the localtodos example from Jérôme Gravel-Niquet
I have also tried not declaring el /tunazatu/7/edit?js,console,output but then it seems like no event gets bound.
var AppView = Backbone.View.extend({
tagName:"div", //tagName defined
getName:function(){
console.log(this.model.get('name'));
},
initialize:function(options){
this.listenTo(this.model, 'change', this.render);
var temp_mapper = {appView1:'#route1',appView2:'#route2',appView3:'#route3'};
var m_name = this.model.get('name');
this.template = _.template($(temp_mapper[m_name]).html()); //choose the correct template
},
render:function(){
var temp = this.template(this.model.toJSON()); //populate the template with model data
var newElement = this.$el.html(temp); //put it in the view's tagName
$('#content').html(newElement);
},
events:{
"click button":"log"
},
log:function(){
this.getName();
}
});
Your problem is that your AppView really looks like this:
var AppView = Backbone.View.extend({
el: "#content",
//...
Every time you create a new AppView, you bind another event delegator to #content but you never remove those delegations. If you create three AppViews, you end up with three views listening to click button inside #content.
I would recommend two things:
Avoid trying to re-use views, create and destroy them (via View#remove) as needed. Views should be lightweight enough that putting them together and tearing them down should be cheap.
Don't bind multiple views to the same el. Instead, let each view create its own el and then let the caller put that el inside some container.
If you do both of those things then your problem will go away. Your AppView would look more like this:
var AppView = Backbone.View.extend({
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this; // Common practise, you'll see why shortly.
},
// As you already have things...
});
Then your router methods would look more like this:
view1: function() {
if(this.appView)
this.appView.remove();
this.appView = this.createView('appView1');
$('#content').html(this.appView.render().el);
// that `return this` is handy ----------^^
},
If you must stick with your current approach then you'll have to call undelegateEvents on the current AppView before you render another one and delegateEvents on the new AppView after you render it.
But really, don't be afraid to destroy views that you don't need right at this moment: destroy any view that you don't need on the page right now and create new instances when you need them. There are cases where you don't want to destroy your views but you can usually avoid it.

How to remove the events of child views and master views

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;
}
});

rerender Backbone views without losing references to dom

I have the following problem with backbone and I'd like to know what strategy is the more appropriated
I have a select control, implemented as a Backbone view, that initially loads with a single option saying "loading options". So I load an array with only one element and I render the view.
The options will be loaded from a collection, so I fire a fetch collection.
Then I initialize a component that is in charge of displaying in line errors for every field. So I save a reference of the dom element of the combo.
When the fetch operation is finally ready, I rerender the control with all the options loaded from the collection.
To render the view I user something like this:
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
pretty standard backbone stuff
the problem is that after rendering the view for the second time the reference of the dom is no longer valid,
perhaps this case is a bit strange, but I can think of lots of cases in which I have to re-render a view without losing their doms references (a combo that depends on another combo, for example)
So I wonder what is the best approach to re-render a view without losing all the references to the dom elements inside the view...
The purpose of Backbone.View is to encapsulate the access to a certain DOM subtree to a single, well-defined class. It's a poor Backbone practice to pass around references to DOM elements, those should be considered internal implementation details of the view.
Instead you should have your views communicate directly, or indirectly via a mediator.
Direct communication might look something like:
var ViewA = Backbone.View.extend({
getSelectedValue: function() {
return this.$(".combo").val()
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
this.viewA = options.viewA;
},
doSomething: function() {
var val = this.viewA.getSelectedValue();
}
});
var a = new ViewA();
var b = new ViewB({viewA:a});
And indirect, using the root Backbone object as a mediator:
var ViewA = Backbone.View.extend({
events: {
"change .combo" : "selectedValueChanged"
},
selectedValueChanged: function() {
//publish
Backbone.trigger('ViewA:changed', this.$('.combo').val());
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
//subscribe
this.listenTo(Backbone, 'ViewA:changed', this.doSomething);
},
doSomething: function(val) {
//handle
}
});
var a = new ViewA();
var b = new ViewB();
The above is very generic, of course, but the point I'm trying to illustrate here is that you shouldn't have to worry whether the DOM elements are swapped, because no other view should be aware of the element's existence. If you define interfaces between views (either via method calls or mediated message passing), your application will be more maintainable and less brittle.

marionette simple event delegation

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!

Resources