Marionette on adding an item to collection event is firing twice - backbone.js

I'm using Marionette 2.4 and have a layoutView which is listening to an event in the childView. When the event fires I search for an existing model within the collection and if it is not there I create a new model and add it to the collection. If it is found I remove the model from the collection. The problem is that the event seems to be firing twice. The first time it fires, it will create the model, but then as it is firing twice, it then finds the newly created model in the collection and then removes it.
var layout = Marionette.LayoutView.extend({
childEvents: {
'channel:selected': 'onChildviewChannelSelected'
},
onChildviewChannelSelected: function (childView, args) {
var linkCollection = this.getRegion('regionWithCollectionView').currentView.collection;
var modelToUpdate = linkCollection.where({channel: args.currentTarget.value});
if(modelToUpdate) {
this.removeModel(linkCollection, modelToUpdate);
} else {
this.addModel(linkCollection, args.currentTarget.value);
}
},
removeModel: function (collection, model) {
collection.remove(model);
},
addModel: function (collection, channel) {
var newEntity = new MyApp.Entities.Link();
newEntity.set('channel', channel);
collection.add(newEntity);
}
});
and here is the child view that fires the 'channel:selected' event....
var childView = Marionette.ItemView.extend({
events: {
'change input[type="checkbox"]': 'channelSelected'
},
channelSelected: function(args) {
this.triggerMethod('channel:selected', args);
}
});
Any idea why the childView fires the 'channel:selected' event twice?
It isn't the view that holds the collection that is being added to, but perhaps there is something that happens when a collection is added to that it will trigger the event again for some reason.

It looks like your function is getting fired twice because of Marionette's "childview* event bubbling". From the documentation:
When a child view within a collection view triggers an event, that
event will bubble up through the parent collection view with
"childview:" prepended to the event name.
That is, if a child view triggers "do:something", the parent
collection view will then trigger "childview:do:something".
This means that "childview:channel:selected" is already being triggered on your layoutview (which means that the onChildviewChannelSelected function is automatically executed on the parent view if it exists http://marionettejs.com/docs/v2.4.7/marionette.functions.html#marionettetriggermethod).
It seems there are a couple potential workarounds. 1 - don't specify a childEvents handler if your handler/function name follows Marionette conventions.
var LayoutView = Marionette.LayoutView.extend({
template: false,
el: '.container',
regions: {
'regionWithCollectionView': '.collection-view-container'
},
onChildviewChannelSelected: function (childView, args) {
console.log("layoutview::channelSelected - child " + childView.model.get('channel') + " selected");
}
});
Fiddle showing workaround #1: https://jsfiddle.net/kjftf919/
2 - Rename your LayoutView's childview function handler to something that doesn't conflict with Marionette's automatic event bubbling.
var LayoutView = Marionette.LayoutView.extend({
template: false,
el: '.container',
regions: {
'regionWithCollectionView': '.collection-view-container'
},
childEvents: {
'channel:selected':'channelSelected'
},
channelSelected: function (childView, args) {
console.log("layoutview::channelSelected - child " + childView.model.get('channel') + " selected");
}
});
Fiddle showing workaround #2: https://jsfiddle.net/kac0rw6j/

Related

Backbone: getting view object from the element

Let's say I have some items to show in a list. The list has a view that aggregates all the items as item views. Now I want to handle the click events on the item views and I delegate the handling to the list view.
Let's see some example code:
ItemView = Backbone.View.extend({
className: 'item',
initialize: function() {
this.$el.data('backbone-view', this);
}
});
Note that I am attaching the view object itself as a property of the root element, which essentially creates a circular reference situation for the view and the element.
ListView = Backbone.View.extend({
initialize: function() {
// contains the item views
this.items = [];
// click event delegation
this.$el.click(_.bind(this._onClick, this));
},
addItem: function(v) {
if ( !(v instanceof ItemView) ) return;
this.items.push(v);
this.$el.append(v.el);
},
_onClick: function(e) {
var el = $(e.target).closest('.item'),
view = el.data('backbone-view');
// do something with the view
}
});
This is a very general pattern whenever one has to deal with any kind of list views.
I am getting the item view back in the handler via the data property that I set on the item on the initialization time. I need to get item view because anything that I want to do on the item as part of handling the click event is based on the view.
Also note that I am using closest because item view may be complex and the actual target of the click event may be a descendant of the root element.
So the question: is this way to binding the view to it's root element via data properties the right approach -- in particular when considering garbage collection and memory leaks? Can there be something better than this?
You should catch the events in the child view. In my opinion, any Backbone view should only handle the DOM events of its element and its children. If views are nested, as yours are, the most specific view should handle the events.
If you want to delegate handling to the parent view, you can trigger a backbone event in the ItemView, and listen to those in the ListView.
ItemView = Backbone.View.extend({
events: {
"click":"onClick"
},
onClick: function() {
//trigger a custom event, passing the view as first argument
this.trigger('click', this);
}
});
ListView = Backbone.View.extend({
addItem: function(v) {
if ( !(v instanceof ItemView) ) return;
//listen to custom event
this.listenTo(v, 'click', this._onClick);
this.items.push(v);
this.$el.append(v.el);
},
_onClick:function(itemView) {
//...
}
});
If the click event represents some "higher level" action, such as select or activate, you should name your custom events as such. This way you can create a logical, robust interface between your views without concerning the parent ListView with the implementation details of its child. Only ItemView should know that whether it's been clicked, hovered, double clicked etc.

backbone events triggered when binding

I need to bind click events to certain amount of special divs, which divs should be binded are only known at runtime
so I was thinking simply set a class for all these special divs and bind them in "events", but then click on one of these divs would trigger all divs to fire
then I tried to use variables in events, but these variables are only know at runtime, so it turns out they are undefined when binding events
now I am using jQuery to bind the events inside Backbone at runtime, but whenever I initialize the view, the event fires right away
var RoomNumber = Backbone.View.extend({
el: $('#roomColumn' + this.roomNumber),
initialize: function () {
_.bindAll(this, 'render');
this.user = this.options.user;
this.roomNumber = this.options.roomNumber;
this.render();
//$('#roomNumber'+this.roomNumber).on('click', this.enterBooking());
},
render: function () {
$(this.el).append("<div class = 'roomNumber' id = 'roomNumber" + this.roomNumber + "'>" + this.roomNumber + "</div>");
},
enterBooking: function () {
var slideForm = new SlideForm({
user: this.user,
roomNumber: this.roomNumber,
state: 'book',
singleSchedule: new Schedule()
});
}
});
Would anyone kindly explain why these would happen? And how can I bind events to a dynamically generated divs?
(I know I probably should not have used a backbone view like this..but it's part of requirements )
Your code is having two problems:
Answering to your point, events are triggered when binding because you are calling the event handler while binding.
$('#roomNumber'+this.roomNumber).on('click', this.enterBooking()); should be
$('#roomNumber'+this.roomNumber).on('click', this.enterBooking); Notice the function call braces.
The way you have set the el is wrong
el: $('#roomColumn' + this.roomNumber), In backbone, el property of the view gets set before the initialize method gets called. This would mean that backbone would try to find for an element $('#roomColumnundefined') which is not expected. Instead, you can pass the el element as an option to the view
var roomNumber = 3;
var view = new RoomNumber({
roomNumber:roomNumber,
el:$('#roomColumn' + roomNumber)
});
......
//Pseudo code
render: function () {
$(this.el).append("<div class = 'roomNumber' id = 'roomNumber" + this.roomNumber + "'>" + this.roomNumber + "</div>");
//you can dynamic set up events like this:
this.events["click #roomNumber"+this.roomNumber] = "enterBooking";
this.delegateEvents(this.events);
},
......

Find a Backbone.js View if you know the Model?

Given a page that uses Backbone.js to have a Collection tied to a View (RowsView, creates a <ul>) which creates sub Views (RowView, creates <li>) for each Model in the collection, I've got an issue setting up inline editing for those models in the collection.
I created an edit() method on the RowView view that replaces the li contents with a text box, and if the user presses tab while in that text box, I'd like to trigger the edit() method of the next View in the list.
I can get the model of the next model in the collection:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
But the question is, how to find the View that is attached to that Model. The parent RowsView View doesn't keep a reference to all the children Views; it's render() method is just:
this.$el.html(''); // Clear
this.model.each(function (model) {
this.$el.append(new RowView({ model:model} ).render().el);
}, this);
Do I need to rewrite it to keep a separate array of pointers to all the RowViews it has under it? Or is there a clever way to find the View that's got a known Model attached to it?
Here's a jsFiddle of the whole problem: http://jsfiddle.net/midnightlightning/G4NeJ/
It is not elegant to store a reference to the View in your model, however you could link a View with a Model with events, do this:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
nextModel.trigger('prepareEdit');
In RowView listen to the event prepareEdit and in that listener call edit(), something like this:
this.model.on('prepareEdit', this.edit);
I'd say that your RowsView should keep track of its component RowViews. The individual RowViews really are parts of the RowsView and it makes sense that a view should keep track of its parts.
So, your RowsView would have a render method sort of like this:
render: function() {
this.child_views = this.collection.map(function(m) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
return v;
}, this);
return this;
}
Then you just need a way to convert a Tab to an index in this.child_views.
One way is to use events, Backbone views have Backbone.Events mixed in so views can trigger events on themselves and other things can listen to those events. In your RowView you could have this:
events: {
'keydown input': 'tab_next'
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
this.trigger('tab-next', this);
return false;
}
and your RowsView would v.on('tab-next', this.edit_next); in the this.collection.map and you could have an edit_next sort like this:
edit_next: function(v) {
var i = this.collection.indexOf(v.model) + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode(); // This method enables the <input>
}
Demo: http://jsfiddle.net/ambiguous/WeCRW/
A variant on this would be to add a reference to the RowsView to the RowViews and then tab_next could directly call this.parent_view.edit_next().
Another option is to put the keydown handler inside RowsView. This adds a bit of coupling between the RowView and RowsView but that's probably not a big problem in this case but it is a bit uglier than the event solution:
var RowsView = Backbone.View.extend({
//...
events: {
'keydown input': 'tab_next'
},
render: function() {
this.child_views = this.collection.map(function(m, i) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
v.$el.data('model-index', i); // You could look at the siblings instead...
return v;
}, this);
return this;
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
var i = $(e.target).closest('li').data('model-index') + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode();
return false;
}
});
Demo: http://jsfiddle.net/ambiguous/ZnxZv/

cached view losing its events

I render a collection of models, which is associated with a collectionView where when rendered each element in the collection has its own 'itemview' which is rendered.
When a collection is sorted and the listView re-rendered based on the new order, I had been creating a totally new view for each item, and as I was not clearing up any previous instances of views associated with that model, I believe zombies being left around.
So initially rendering my collection I would do...
render : function() {
$(this.el).empty();
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view = new TB_BB.RequestItemView({model : model});
els.push(view.render().el);
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So whenever the render function was called a second/third etc time, I had not cleared up the TB_BB.RequestItemView (hence the zombies)
To overcome this I tried to add some simple caching in the collections view, so that instead of creating a new itemview if it had already been created use that instead. My code
initialize : function(){
_.bindAll(this,"render");
this.collection.bind("add", this.render);
this.collection.bind("remove", this.render);
this.template = $("#request-list-template");
this.views = {};
},
events : {
"change #sort" : "changesort",
"click #add-offer" : "addoffer",
"click #alert-button" : "addalert"
},
render : function() {
$(this.el).empty();
outerthis = this;
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view;
if(outerthis.views[model.get('id')]) {
view = outerthis.views[model.get('id')];
} else {
view = new TB_BB.RequestItemView({model : model});
outerthis.views[model.get('id')] = view;
}
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So this works in so much as the views are re-used - however what I have noticed is that if I use a cached view (e.g. the collection has been sorted and the render function finds a cached view) that all of the events on the sub itemview stop working? why is that?
Also could anyone suggest a better way of doing this?
You can use delegateEvents ( http://documentcloud.github.com/backbone/#View-delegateEvents ) to bind the events again.
As OlliM mentioned the reason is because the events are bound to the dom element, but instead of rebinding the element you can also just detach them instead of removing them (detach keeps the event bindings http://api.jquery.com/detach/)
something like
var $sortContainer = $('#sorting-container');
$('li', $sortContainer).detach();
And then just reattach the element
$cnt.append(view.el);
I would also consider using a document fragment while rebuilding/sorting the list and then attaching appending that instead.

Passing functions to the `events` hash

Inside Backbone.View instances one can set an events hash of callbacks:
events: { 'click #element' : 'myFunction' }
When the function I try to access is not a direct function of the view instance (e.g. this.model.myFunction) I cannot pass the function right in the events hash. I have tried:
events: { 'click #element' : 'model.myFunction' }
and
events: { 'click #element' : this.model.myFunction }
How can I tell my backbone view to use this.model.myFunction as a callback right from the events hash?
No, you can't do that. The relevant chunk of Backbone looks like this:
delegateEvents : function(events) {
if (!(events || (events = getValue(this, 'events')))) return;
this.undelegateEvents();
for (var key in events) {
var method = this[events[key]];
if (!method) throw new Error('Event "' + events[key] + '" does not exist');
//...
So the values in events have to be the names of methods in your view object. You could route the events yourself though:
events: { 'click #element': 'myFunction' },
// ...
myFunction: function(e) {
this.model.myFunction(e);
}

Resources