I am using BackBone relational as very useful extension of Backbone.js.
I have an issue however after saving a backbone relational model with a 1 - M relationship.
The problem I am seeing is that, after model.save(), the add: is fired once again for every related model in the collection. This causes an issue with my view as the models are rendered a second time in the master view.
My master view is set up as follows:
//master view
initialize: function(){
_.bindAll(this, 'render', 'renderRelated');
this.model.bind('change', this.render);
this.model.bind('add:related', this.renderRelated);
}
This works great on first load and when adding new related models. The problem is that when save is called on the parent model the add:related is fired again for every nested model even when the models have IDs etc. It seems that the save fully repopulates the related collection.
I'm wondering how have other people gotten around this? I was considering:
attaching the nested view to each related model
bind rm:related to a method that will remove the view from the master view
This seems pretty hacky to me, is very inefficient and will causes a flashing effect for the end user as the sub views are removed and added again.
As part of this I was also trying to bind to the reset:related but that does not seem to fire after the save.
I use Backbone.Marionette views, and I had a similar problem. This is how solved it:
initialize: function(){
this.bindTo(this.model, 'change', this.render);
this.bindTo(this.model.get('myrelated'), 'relational:add', this.render);
}
I think in plain Backbone this translates to (untested):
initialize: function(){
_.bindAll(this, 'render', 'renderRelated');
this.model.bind('change', this.render);
this.model.get('myrelated').bind('relational:add', this.render);
}
Note that if you're pass {wait: true} when you save, then the relational:add will be fired twice: once when the temporary model is created by backbone before the save, and once when the save succeeds and the actual model is created. This is a bug with Relational that hasn't been resolved yet.
Related
I have a problem with Backbone and Marionette. I have a CompositeView with a collection where people can a comment, this all works nicely, the comment is added and saved to the server but I don't want the view to update and to show the newly added comment. I have tried this:
App.Views.CommentsView = Backbone.Marionette.CompositeView.extend({
template: '#article-comment-container',
itemViewContainer: 'ul',
itemView: App.Views.CommentView,
collectionEvents: {
"add": "modelAdded"
},
modelAdded: function(){
console.log('Please do nothing!');
}
});
But the item is still rendered into the page on top of my modelAdded function being called. Can I prevent that from happening at some point?
In a different scenario I would like new items to be added to the top of the list and not the bottom. Do I have to override the entire appendHtml method achieve this?
Setting the collection event add simply adds another handler to the queue for that event; it doesn't replace any other events so the default marionette behaviour will still occur.
I assume you're calling the create method on the collection to create your new comment model. If this is the case you simply need to set the silent option to true. Now the add event will not fire and Marionette will not create and render the view for that model. You can do it like this:
commentCollection.create(commentModel, {silent: true});
As for you second question about prepending, yes I would override appendHtml method. Or to keep the method names consistent with what actually happens, create a method called prependHtml and then override the renderItemView method to call prependHtml.
What is the best way to remove a model from a collection that has been removed in the DOM. Let me ask a better question, how do I keep views in sync with a collection?
remove the view first, while removing execute
this.model.collection.remove(this.model);
you can check with conditions to whether current view has a model, and that model has a collection etc before you execute the same.
I've followed the backbone Todos example application. This keeps view state up to date with collection.
Pass models to any view created like so:
var someView = new SomeItemView({ model: modelFromCollection });
Then listen to events on that model and react from the view:
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
// listen to other events ...
}
I have a view that takes a collection like below:
MyView = Backbone.View.extend({
initialize: function() {
this.collection.on("change://an attribute of a model in aCollectionToRender", someAction);
}
});
var MyViewInstance = new MyView({
collection: aCollectionToRender
});
Basically, I don't want MyView to listen for changes on the entire collection, only a single attribute of the model that the collection contains.
I realize there are a number of alternatives to this:
Create views for each model of aCollectionToRender and attach .on("change") events in those views. I don't want to do this because it's not the right thing to do for my situation
Just have a this.collection.on("change") event and have the event handler filter based on my requirements. This is less elegant, and if Backbone already allows me to write event filters as I asked above, this is duplicate code
I was just wondering if there's any way to write an event listener that already does the filtering. This question may also be a duplicate; I looked but I didn't find anything exactly like this, however, there are a lot of Backbone questions
Maybe I am misunderstanding your question, but you can do exactly what you show. Check out the Backbone documentation.
MyView = Backbone.View.extend({
initialize: function() {
this.collection.on("change:attributeName", this.someAction /*function to call*/, this);
},
someAction: function(model, value, options){
}
});
I've been learning a lot about backbone, backbone-relational and building web apps from reading through stackexchange - so first a thank you to the community.
Now, I am stuck at trying to understand this current issue involving nested models and sub views in what seems to me to be is a pretty common usecase.
I am trying to extend this tutorial to learn about backbone-relational, views/subviews and event handling by keepin track of "CheckIns" for each wine.
I have extended the server side to return appropriate JSON and backbone-relational model for checkIns like so:
window.CheckInModel = Backbone.RelationalModel.extend({
defaults:{
"_id":null,
"Did":"true",
"dateOf":"",
}
});
window.CheckInCollection = Backbone.Collection.extend({
model : CheckInModel
});
And the Wine Model like so:
relations: [{
type: Backbone.HasMany,
key:'CheckIn',
relatedModel: 'CheckInModel',
collectionType: CheckInCollection,
}]
I've created a CheckInListView and CheckInItemView (same as WineListView and WineListItemView) and use the WineView Render function to render the CheckIns like so:
render:function (eventName) {
console.log("wine View Render");
$(this.el).html(this.template(this.model.toJSON()));
this.myCheckInListView = new CheckInListView({model: this.model.attributes.CheckIn});
this.$el.append(this.myCheckInListView.render().el);
return this;
},
I've also created a new function within wineview that creates a checkin and associated with the given event:
logcheckin: function (event){
var todate = new Date();
newCheckIn = new CheckInModel ({'Did':"true", 'dateOf': todate.toISOString()});
console.log ("Logcheckin - About to push newCheckIn onto Model.");
this.model.attributes.CheckIn.push (newCheckIn);
console.log ("Just pushed newCheckIn onto Model.");
this.saveWine();
}
Ok - if you haven't TL/DRed yet - This all seems to work fine from a UI perspective -ie. everything renders correctly and saves to the Db.
But I notice in the console that when I push a new CheckIn (between the console.logs above) the CheckInListView's Add binding gets called multiple times for wach button press - Which makes me think something is wrong with how I'm doing views or that I am not understanding something fundamental about event propagation.
Why is this happening ? Is it expected behavior ? Am I approaching what I am trying to do correctly ?
Thanks for reading if not your help.
==
Here are the relevant parts of the CheckinListView and CheckInList Item views that are bound to the add (and other) events.
window.CheckInListView = Backbone.View.extend({
initialize:function () {
this.model.bind("reset", this.render, this);
this.model.bind("change", this.render, this);
var self = this;
this.model.bind("add", function (CheckIn) {
console.log ("Adding to CheckInListView - a CheckIn List Item", CheckIn);
self.$el.append(new CheckInListItemView({model:CheckIn}).render().el);
});
},
close:function () {
$(this.el).unbind();
$(this.el).remove();
}
});
window.CheckInListItemView = Backbone.View.extend({
initialize:function () {
this.model.bind("change", this.render, this);
this.model.bind("destroy", this.close, this);
},
});
==============================================
The comment about event binding and closing views were the right hints for debugging this.
1) I was not closing and unbinding the nested views properly which left some ghost event consumers even though there was nothing in the DOM
2) You only need to bind events if we want to do something only in the subview.
For Example - If I have checkbox in a subview I can bind a subview change event in the main view and handle the event there since the mainview has model there anyway. I don't know if this is the "right" way but it works for what I need to do. (mm.. spaghetti code tastes so good)
3) Struggling with this helped me think through the UX some more and helped me simplify UI.
4) I was trying to "save" calls to the server by nesting all the data into on JSON call. And if I were to re-do this - I would not nest the data at all but handle it on the back end by associating the wine ID with checkIn ID and then having a separate collection that gets populated with the collection once a task is selected - I thought this would not be a the preferred way but it seems to be the way that a lot of people.
Still welcome any thoughts on the "right" way questions above or if anyone can point to a tutorial that goes beyond the "simple backbone app"
I'm not sure about everything that's happening, but, I've run into the problem of events firing multiple times before. If you're rendering multiple models using the same view, there's a chance that they're all being bound to the same event.
Perhaps this answer might apply:
Cleaning views with backbone.js?
If not, you should respond to Edward M Smith's comment and show how your events are being bound.
What is the best way to bind events to a Backbone boilerplate application? I've been trying to bind my events directly to the models associated with my views, in my views, but it doesn't seem to be working. I see within 'namespace.js', that there is an app key that extends Backbone.Events like so:
// Keep active application instances namespaced under an app object.
app: _.extend({}, Backbone.Events)
I don't fully understand how to use it...
I was able to get things working without the boilerplate, but it does provide some very cool functionality, so I'd love to be able to use it. Thanks!
ADDED
the code I was using was with the underscore bind method like so:
this.module.bind('change', this.render);
But then, I realized that 'this.model' is returning undefined, and so this doesn't work. I really am not sure how the boilerplate wants me to reference my model from the view.
I'm not sure if it is a typo that you copied from your code or a typo you only entered here, but I believe this.module (which IS undefined) should be this.model, which you also must be sure to pass in when you instantiate your view, of course, as so:
myView = new BBView({model: myModel});
then you can say this.model.bind('change', this.render); or this.model.on('change', this.render); for the most recent version of Backbone
I frequently bind my views to change events on my models in this way.
As for extending Backbone.Events, the way I have used it is to create an independent "event aggregator" that will help connect events between different views on your page. Let's say for example you have some action on one view that needs to set off an action on another view. In this case, you can pass your event aggregator object as an option to each of your views when you instantiate them, so that they can both trigger events or bind to events on a common object (i.e. your event aggregator).
whatsUp = _.extend({}, Backbone.Events) // the aggregator
myFirstView = new FirstBBView ({whatsUp: whatsUp});
(the aggregator shows up as this.options.whatsUp inside the view)
mySecondView = new SecondBBView2 ({whatsUp: whatsUp});
inside FirstBBView:
this.options.whatsUp.bind('specialEvent', function(arg1,arg2) {
// do stuff
});
inside SecondBBView, when something important happens:
this.options.whatsUp.trigger('specialEvent', {arg1: 'some data', arg2: 'more data'});
For a great explanation, see this great article by Derick Bailey