Backbone.js event handler triggers multiple times - backbone.js

I have a View called Form that renders either a form to edit a list, or the list itself, depending on what argument is passed to render. I've added event handlers so that the show/edit mode can be toggled. I've taken this out from the code below to keep it simple, but this just gives a bit of context to what the View does in context.
I can instantiate this Form view as a child in another view that requires a form, or the list to be rendered, which I've done in the New view, where it would be rendered as a form.
When I need to save, I call the form:save event, which triggers a routine in the Form view that saves the form, I've just made it call a console.log here to show it works. In my code, I call form:save through an $('a#submit').click binding which binds to navigation buttons that are inserted by an ApplicationView (but I don't think that matters for the purposes of this question.)
Lets say I navigate away from the New view, and I go back to it a number of times. When I hit save, the method runs the number of times I have instantiated and rendered a new Form view.
So far:
I've tried doing unbind() and remove() in a close method on the Form view from the New view with no luck.
I think I may have problems with scoping, but I'm unsure.
I know this isn't related to my navigation bindings.
I think this may be to do with zombie views.
Any pointers to make it only run once?
App.Views.New = Support.CompositeView.extend
initialize: (options) ->
_.bindAll this, 'render'
#model = new App.Models.Item()
render: ->
self = this
form = new App.Views.Form model: #model, collection: #collection
#$el.append form.render().el
setTimeout (->
$('a#submit').click (e) ->
e.preventDefault()
App.eventHandler.trigger 'form:save'
), 0
this
App.Views.Form = Support.CompositeView.extend
initialize: ->
_.bindAll this, 'render', 'save'
App.eventHandler.on 'form:save', #save
render: ->
self = this
# RENDER TEMPLATE HERE
this
save: ->
console.log 'form saved'

I believe your issue is that you are creating a new view each time you want to render the form, but you aren't getting rid of your old view. What you can do is either destroy your old view, or keep a reference to it and instead of creating a new view each time, just pass in the model to the existing view and refresh/rerender the display

Related

Repeatedly creating and destroying views in Backbone.js or Marionette.js without creating a "memory leak"

I suspect that the way I am handling views in backbone.js is flawed in such a way that it is creating a "memory leak".
There is a view that is constantly being overwritten with another copy of itself. The new copy is linked to a different model.
I am creating and adding the view to it's parent view by setting the el option when creating the child view.
The strange thing that is happening is that even though a new view is being rendered over top of the old view, when I click the "button" an alert pops up for every childView that was every rendered, even though the button they were listening to should be gone, they respond to the new button.
I've implemented a quick fix by calling a function on the old view to stop listening to events before the new view is added. But that this problem exists at all tells me that all of the old views are hanging around and will slow the application over time if the user does not refresh the page often enough.
var parent = new (Backbone.Marionette.ItemView.extend({
ui:{
child_container: '#child-container'
},
onRender: function(){
// Listen to outside event
...
}
on_Outside_Event: function(new_model){
// Quick fix prevents multiple alerts popping up for every child view when "button" is pressed
this.child_view.destroy_view();
// New child view is created and rendered on top of the one that was there before
this.child_view = childView({
el: this.ui.child_container, // <-- Is this the problem?
model: new_model
})
this.child_view.render();
}
}))();
var childView = Backbone.Marionette.ItemView.extend({
events:{
'click button': 'on_click_button'
},
on_click_button: function(){
// Alert pops up once for every view that was ever displayed.
alert('Button clicked');
},
// QUICK FIX
destroy_view: function(){
this.undelegateEvents();
}
})
In case this is helpful, here is a screen shot of the actual application. A calendar of appointments is on the right. The problem child view - a view of the individual appointment that the user wants to see is on the left.
When I click the "Cancel appointment" button, that function gets called for every appointment that was every displayed in that area, even though I am listening to the event using: events:{ 'click #cancel-button': 'on_button_click'}
None of the other buttons, interactions, and other controls have this same issue, I assume because all the others actually live views that are children of the child appointment view and not in the child appointment view itself
A possible fix?
Did a little searching around, does this fix look adequate?
Normally, I think the removeData().unbind(); and remove() functions are called directly on this.$el, but this did not work here, I think because I added the child view using the el option when it was created (el: this.ui.child_container)
var childView = Backbone.Marionette.ItemView.extend({
...
// REAL FIX
destroy_view: function(){
this.undelegateEvents();
this.$el.children().removeData().unbind();
this.$el.children().remove();
}
I think you should make your parent view a LayoutView (that's just an ItemView with added functionality to handle regions iirc), have a region defined for where you want this child view to appear, and then do:
Parent = Backbone.Marionette.LayoutView.extend
regions:
child: "#child-container"
on_Outside_Event: ->
childView = new ChildView(...)
#getRegion("child").show(childView)
(sorry I used coffeescript, it's faster to write, but you can translate easily).
Marionette will handle everything: closing your old child view, unbinding events, etc.

Passing a Backbone model from a collection to a new view keeps the collection in memory

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.

Collection create function firing add event too quickly

(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.

How do I design MarionetteJS ItemView to properly show loading message?

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.

Debugging events that are not loaded in view

My events are not loading automatically. When I added delegateEvents() at the end of the render() method, it worked for a while. I do not want to use delegateEvents, but now, even with delegateEvents the events are not loading.
I reckon the DOM is not known at the time, so the events aren't bound, but how do I check (debug) that?
View:
class EditGroup extends BaseView
initialize: ->
#render()
render: ->
html = _.template tpl, #model.toJSON()
#$el.html html
for own key, options of FormConfig[#model.type]
options.key = key
options.value = #model.get key
input = new Input options
input.on 'valuechanged', (key, value) => #model.set key, value
#$('section.'+key).html input.$el
#delegateEvents() # doesn't work
#
DOM:
h2 Edit Group
section.title
section.type
section.members
button.save.btn.btn-primary(onclick="return false") Save changes
In the sections type and member there are typeaheads and selects rendered (Backbone views). One works without delegateEvents and the other works with. The events in the parent view (shown above) don't work at all. Removing the for-loop doesn't make any difference.
Ok, I solved it, but I don't understand the workings.
I'm using a 'view manager', which registers all views and shows (read: attaches the html to the DOM) the parent view. The view manager's show function is triggered when a new route is fired, but the route was fired double, once as "route:edit" and once as "route". I catch them with router.on "all", (eventName) -> etc.. I reckon the events are bound to the html, but the html is overridden by the second router event without the bindings and attached to the DOM. Question remains why the route is fired twice.

Resources