I am using Backbone, Marionette, and Backbone.localStorage to persist a list of items. I have a remove button on the ItemViews which fires the model's destroy method. This removes the model from the collection successfully and fires the remove event.
However, the CollectionView does not remove its ItemView automatically (as I think? it should), nor does it remove the model from local storage, so on reloading the page, it's back in the list.
Edit:
The model is removed by this method on the view:
confirmRemove : function() {
this._setRemoveState(false);
this.model.destroy({success: function() { console.log('model destroyed!');}});
return false;
}
The success callback does fire.
The problems:
I overwrote the ItemView's remove method unintentionally, so it wasn't removed from the DOM.
destroy only runs its sync actions on models with ids, assuming that only models with ids are persisted to the server. However, while Backbone.localStorage creates internal ids, it does not persist them back to the models, so there was no id property and Backbone skipped removing it from the server.
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.
(Using Backbone 0.9.10)
I have a list view of sorts where the user can click a button to show a modal view. The list view has a counter that shows the amount of items contained in the list. Clicking a button in the modal view executes create on a collection that is passed into both views. This fires the add event in the list view, which in turn runs this function:
renderCount: function () {
// Container for model count
var $num = this.$el.find('.num');
if ($num.length > 0) {
// 'count' is a value returned from a service that
// always contains the total amount of models in the
// collection. This is necessary as I use a form of
// pagination. It's essentially the same as
// this.collection.length had it returned all models
// at once.
$num.html(this.collection.count);
}
}
However, add seems to be fired immediately (as it should be, according to the docs), before the collection has a chance to update the model count. I looked into sync but it didn't seem to do much of anything.
How can I make sure the collection is updated before calling renderCount?
Here's the list view initialize function, for reference:
initialize: function (options) {
this.collection = options.collection;
this.listenTo(this.collection, 'add remove reset', this.renderCount);
this.render();
}
EDIT:
Turns out I was forgetting to refetch the collection on success in the modal view.
$num.html(this.collection.count);
shoule be:
$num.html(this.collection.size());
Backbone.Collection uses methods imported from underscore, here is list: http://backbonejs.org/#Collection-Underscore-Methods
Turns out I was forgetting to refetch the collection on success in the modal view.
In the initialize function of my backbone View, I created the following listener:
this.listenTo(this.model.get('clusters'), 'add remove', this.saveChanges);
This successfully causes my saveChanges function to be called the first time a model is added/removed from the 'clusters' Collection. There's only one line in that saveChanges function:
this.model.save();
Once that is invoked, adding/removing clusters no longer invokes the "add" or "remove" event. Why would saving the model destroy the listener? Can that be prevented, or is there a way to re-establish the listener?
Or is there something fundamental I'm not understanding about Models and/or Collections...?
Let's break down your code:
You write
this.listenTo(this.model.get('clusters'), 'add remove', this.saveChanges);
Which is equal to
var clusters = this.model.get('clusters');
this.listenTo(clusters, 'add remove', this.saveChanges);
Now I only assume that after you get that event you set() a new clusters object inside your model.
The issue here is that your view still listens to events from that same old clusters object, which is not relevant anymore - your model deals with another object!
Another case could be that Backbone clears your view's event handlers from its model when it's being removed.. Could easily answer for sure if you'd share the whole code.
First off - I am a MarionetteJS noob.
I am having trouble making an ItemView display a loading message or throbber while it is being fetched. This is especially problematic when this ItemView is being displayed from a deep link in Backbone's history (i.e. the ItemView is the first page of the app being displayed since the user linked directly to it). I want to indicate that the page is loading (fetching), preferably with a simple view, and then show the real templated view with the fetched model.
I have seen other answers on SO like Marionette.async, (which has been deprecated) and changing the template during ItemView.initalize().
Anybody (Derrick?) got any suggestions or best practices here?
UPDATE:
I am getting the model from the collection using collection.get(id), not using model.fetch() directly.
After thinking about this, the real question is where should this be implemented:
I could change my controller to see if the model exists in the collection (and if the collection is loaded) and decide which view to show accordingly. this seems like a lot of boilerplate everywhere since this could happen with any ItemView and any controller.
I could change my ItemView initialize to test for existence of the model (and a loaded collection), but same comment here: every ItemView could have this problem.
UPDATE 2:
This is what I ended up with, in case anybody else want this solution:
app.ModelLayout = Backbone.Marionette.Layout.extend({
constructor: function(){
var args = Array.prototype.slice.apply(arguments);
Backbone.Marionette.Layout.prototype.constructor.apply(this, args);
// we want to know when the collection is loaded or changed significantly
this.listenTo(this.collection, "reset sync", this.resetHandler);
},
resetHandler: function () {
// whenever the collection is reset/sync'ed, we try to render the selected model
if (this.collection.length) {
// when there is no model, check to see if the collection is loaded and then try to get the
// specified id to render into this view
this.model = this.collection.get(this.id);
}
this.render();
},
getTemplate: function(){
// getTemplate will be called during render() processing.
// return a template based on state of collection, and state of model
if (this.model){
// normal case: we have a valid model, return the normal template
return this.template;
} else if (this.collection && this.collection.isSyncing) {
// collection is still syncing, tell the user that it is Loading
return this.loadingView;
} else {
// we're not syncing and we don't have a model, therefore, not found
return this.emptyView;
}
}
});
And here is how to use it:
// display a single model on a page
app.Access.Layout.CardLayout = app.ModelLayout.extend({
regions: {
detailsRegion:"#detailsRegion",
eventsRegion:"#eventsRegion"
},
template:"CardLayout", // this is the normal template with a loaded model
loadingView:"LoadingView", // this is a template to show while loading the collection
emptyView:"PageNotFoundView", // this is a template to show when the model is not found
onRender : function() {
this.detailsRegion.show( blah );
this.eventsRegion.show( blah );
}
});
thanks!
For the ItemView
I think you can add a spinner in your initialize function, I really like spin.js http://fgnass.github.io/spin.js/ because its pretty easy and simple to use, and you can hide the spinner in the onRender function of the Itemview
For The CollectionView
in the CollectionView you could handle it like this....
Take a look at the solution that Derick posted..
https://github.com/marionettejs/backbone.marionette/wiki/Displaying-A-%22loading-...%22-Message-For-A-Collection-Or-Composite-View
I'd suggest using jQuery deferreds:
Start fetching your data, and store the return value (which is a jQuery promise)
Instanciate your content view
Show your loading view
When the promise is done, show the view containing the content
I've talked about implementing this technique on my blog:
http://davidsulc.com/blog/2013/04/01/using-jquery-promises-to-render-backbone-views-after-fetching-data/
http://davidsulc.com/blog/2013/04/02/rendering-a-view-after-multiple-async-functions-return-using-promises/
The issue with the solution linked by Rayweb_on, is that your loading view will be displayed any time your collection is empty (i.e. not just when it's being fetched). Besides, the ItemView doesn't have an emptyView attribute, so it won't be applicable to your case anyway.
Update:
Based on your updated question, you should still be able to apply the concept by dynamically specifying which template to use: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#change-which-template-is-rendered-for-a-view
Then, when/if the data has been fetched successfully, trigger a rerender in the view.
I am converting my backbone app so that it starts communicating with the server, previously I had just been populating the collection with test data using .add()
I have tied some events to the collections add event. So every time an item is added to the collection I can render the view and update some statistics.
it appears that as soon as i add the .fetch() call to get data from the server the add events stop.
for example
var PayableCommitmentCollection = Backbone.Collection.extend({
model:PayableCommitment,
url:"/cc/account/contributions/",
initialize: function() {
this.bind("add",this.setInitialAmount,this);
}
}
this.SetInitialAmount() is never called after fetch creates the models in the collection.
I also have 2 views that are watching for items to be added to this collection that are now not updating.
My obvious work around is to write my own AJAX call so that I can add the items the same way I have been during development so far, however I'm sure backbone has the smarts to help me out here.
Can anyone suggest a way i can bind to the completion of fetch, or to make it stimulate the add event.
The fetch method accepts a hash of options. One of these options can be the "add" option, which calls add on the collection instead of reset.
collection.fetch({ add: true });
From the fine manual:
fetch collection.fetch([options])
Fetch the default set of models for this collection from the server, resetting the collection when they arrive.
And a reset:
reset collection.reset(models, [options])
Adding and removing models one at a time is all well and good, but sometimes you have so many models to change that you'd rather just update the collection in bulk. Use reset to replace a collection with a new list of models (or attribute hashes), triggering a single "reset" event at the end.
So a fetch will trigger a single "reset" event rather than a bunch of "add" events. You need a collection-wide version of setInitialAmount that you can bind to "reset".
In Backbone 1.0, Collection#fetch has this to say:
fetch collection.fetch([options])
Fetch the default set of models for this collection from the server, setting them on the collection when they arrive.
[...]
The behavior of fetch can be customized by using the available set options. For example, to fetch a collection, getting an "add" event for every new model, and a "change" event for every changed existing model, without removing anything: collection.fetch({remove: false})
So, if you're using 1.0+ then all you need to do is call your fetch with the remove: false option.