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.
Related
I have a CompositeView that shows a list of models that I requested from a server, something like (in CoffeeScript):
class List.Stories extends Marionette.CompositeView
template: "stories-list-body"
itemView: List.Story
itemViewContainer: "#stories-list"
class List.Story extends Marionette.ItemView
template: "stories-list-story"
triggers:
"click .js-show-button": "show:button:clicked"
The views are correctly created passing the collection as an argument for the constructor, I can see the elements and when I click on the button it triggers the appropriate event and it's handled. The thing is, when the handler creates a new view showing the model and closes the old one, the collection is still referenced in model.collection taking up some memory.
What would be the correct way to eliminate this reference? Simply using delete model.collection in the handler before replacing the view?
Try doing something like
var model = myCollection.remove(viewModel, { silent: true })
// create new view using `model`
In the example above, viewModel would refer to the view's model (so it would be this.model from within the view).
By removing the model from the collection, it should be garbage collected (assuming it's not referenced anywhere else...).
If it all happens within same controller, i.e. the controller is still open and will be responsible for the event and no Application.vent will be triggered, I think this situation is acceptable if memory leak won't be big. The reason is the controller will be finally closed so no need in a hurry.
If App level vent/request/command will be triggered, you need to take it seriously. Assume you have such code in controller:
#listenTo storiesView, 'itemview:show:button:clicked', (itemView) ->
App.vent.trigger 'show:another:view:with:this:model', itemView.model
Stop here. The model is the old model and won't be garbage collected.
I will use below code instead:
#listenTo storiesView, 'itemview:show:button:clicked', (itemView) ->
model = _.clone itemView.model
App.vent.trigger 'show:another:view:with:this:model', model
The new model is a totally new object and then has nothing related to current view/model/controller.
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've binded window's scroll event to a view's method like:
MyView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, 'handleScrolling');
$(window).off('scroll', this.handleScrolling).on('scroll', this.handleScrolling);
}
})
I see this is not working. If this callback is triggered as many times as this view is instantiated. However, if I remove handler from off, then it is correctly unbinding and triggers only once per scrolling. Like:
$(window).off('scroll').on('scroll', this.handleScrolling);
Any idea why this is happening? I dont want to remove all callbacks from this event as other views/codes may bind event to it which will make app behaving unexpected.
Is there any better way of binding events to window/document or other element outside the scope of current view?
Your problem is right here:
_.bindAll(this, 'handleScrolling');
That's equivalent to:
this.handleScrolling = _.bind(this.handleScrolling, this);
so each time you instantiate your view, you're working with a brand new function in this.handleScrolling. Then you do this:
$(window).off('scroll', this.handleScrolling)
But that won't do anything since the this.handleScrolling function that you attached with on:
.on('scroll', this.handleScrolling);
isn't the same function as the this.handleScrolling function that you're trying to .off. The result is that each time you create a new instance of your view, you're leaving the old scroll handler in place and adding a new one.
The proper solution (IMO) is to add a remove method to properly clean things up:
remove: function() {
$(window).off('scroll', this.handleScrolling);
return Backbone.View.prototype.remove.apply(this);
}
and then call view.remove() before creating the new view.
It looks like you have a new instance of the handler this.handleScrolling in each call.
so when jQuery tries to remove the specific handler it will not find the handler in the event registry, so it will not be able to remove it.
Problem: Demo
I would suggest using event namespaces here
$(window).off('scroll.myview').on('scroll.myview', this.handleScrolling);
Demo: Fiddle
Another solution is to use a shared handler like this
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've got a backbonejs application with two view. It kind of looks like this:
<body>
<header></header>
<div id="content"></div>
</body>
Every time a view is loaded the app overwrites the current view by completely overwriting the contents of #content.
// Like this...
$('#content').html( new primaryView().render() );
// ...and this.
$('#content').html( new secondaryView().render() );
The application has a global collection.
App.Collection();
The secondary view modifies itself depending on the global collection. Therefor it binds a function to the 'add' event' on App.Collection in the views initialize function;
App.Collection.bind('add', function(){
console.log('Item added');
});
Which result in my problem. Every time the secondary view is loaded a new function is binded to App.Collection's add event. If I go from the primary view to the secondary view three times, the function will fire three times everytime an item is added to App.Collection.
What am I doing wrong?
I can see how I would do it if there was an uninitialize function on views.
I can see how I could do it if I never removed a view once it was loaded.
I can see how I would do it if I could namespace events like in Jquery. (by unbinding before binding).
You can generalize your problem quite a bit. Basically, you are writing an event-driven app, and in such app events should be taken care of.
Check out this post to see a recommended way to work with event handlers in backbone.
Depending on the situation, you can use initialize and render methods to handle different aspects of creating a view. For instance, you can put your binding inside the initialize
initialize: function() {
App.Collection.bind('add', function(){
this.view.render()
});
}
which only fires when the view is created. This binds your render method to the add event. Then in your render method you can actually create the html.
This prevents the binding from happening every time you need to re-render.