Backbone event being called multiple times - backbone.js

I'm just getting my feet wet with Backbone, and I think I have an easy problem to solve. I have the following view which is a simple tab that when clicked opens up a panel and when closed goes back to a tab:
myApp.views.Support = {
Form: Backbone.View.extend({
initialize: function () {
this.el = $('#support');
this._ensureElement();
},
render: function () {
if (this.$el.hasClass('support-panel')) {
// close panel
this.$el.empty();
this.$el.removeClass('support-panel');
this.$el.addClass('support-button');
}
else {
// open and populate panel
var template = _.template(myApp.utils.RenderTemplate('support/default'), {});
this.$el.removeClass('support-button');
this.$el.addClass('support-panel');
this.$el.html(template);
}
return this;
},
closePanel: function () {
alert('close event fired');
},
events: {
'click #SubmitFormButton': 'submitForm',
'click #CloseSupportPanel': 'closePanel'
},
submitForm: function (event) {
alert('form submitted: ' + $('#message'));
}
})
}
Everything is working fine except that "closePanel" gets fired +2 times every time the click event happens. I assume it's some sort of cleanup I'm missing but I don't know what.

Likely its because the event is bubbling up. Try returning false.

I know this is an old question but it helped me realize what my issue was. Returning false as Daniel said works, but the root cause of my issue was having the jQuery selector twice in my markup, resulting in two jQuery objects being created thus the click event fires twice.

Related

Undelegate view events when browser back button pressed

Could anyone explain please how to remove events in order to prevent triggering duplication when clicking browser back button. Or is there any way to undelegate events when initalizing view again. Really stuck how to deal with it.
Pressing back button and then back again causes firing events for multiple times. When saving model form data for instance. Thank you.
var App = {};
// extending models, collections etc.
App.SamplesCollectionView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click a.sample-item': 'onEdit'
},
render: function(){
this.$el.append(this.template());
var $sample_list = this.$el.find('ul#sample-list');
this.collection.each(function(sample) {
var rendered = new App.CategoryView({model: sample}).render().el;
$sample_list.append(rendered);
});
},
onEdit: function(e) {
this.undelegateEvents();
// go to edit view
Backbone.history.navigate(e.target.getAttribute('href'), {trigger: true});
return false;
}
});
App.SampleEditView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click button.save': 'onSave',
'click button.cancel': 'onCancel',
},
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this;
},
onSave: function() {
this.undelegateEvents();
var data = Helpers.getFormData(this.$el.find('form'));
this.model.save(data);
// go back to index view
Backbone.history.navigate('/samples', {trigger: true});
return false;
}
});
App.SamplesRouter = Backbone.Router.extend({
routes: {
'samples': 'index',
'samples/edit/:id': 'edit'
},
index: function() {
App.samples = new App.SamplesCollection;
App.samplessView = new App.SamplesCollectionView({collection: App.samples});
},
edit: function(id) {
App.sampleEdit = new App.SampleEdit({id: id});
App.sampleEditView = new App.SampleEditView({model: App.sampleEdit})
}
});
App.samplesRouter = new App.SamplesRouter;
Backbone.history.start({pushState: true, hashChange: false});
The problem is that you have many views pointing to same element #samples. You can't remove one view because if you call view.remove() your other view's element is gone.
And as long as that that element exists in DOM, the view you thought to be gone will exist in memory since the shared element has event handlers referring the view instance.
If you want to delegate display functionality and edit functionality under same element, do it in same view using something like show/hide techniques without creating a new view instance.
Otherwise they should have it's own elements, you shouldn't have two view instances pointing to same element. While switching to a different view, make sure you call it's remove() method which removes the element from DOM and invokes undelegateEvents so that it get's garbage collected properly.

How to test if an event handler is set in a Marionette.js view?

There is a Marionette.js view which acts as a login form. The following code sample shows the relevant code parts including an error I already fixed:
MyApp.module("User", function(User, App, Backbone, Marionette, $, _) {
User.LoginView = Marionette.ItemView.extend({
className: "reveal-modal",
template: "user/login",
ui: {
signInForm: "#signin-form"
},
events: {
"submit #signin-form": "onSignInFormSubmit"
},
onRender: function() {
var self = this;
var $el = this.$el;
// [...] Render schema
_.defer(function(){
$el.reveal({
closeOnBackgroundClick: false,
closed: function(){
self.close(); // <-- This is incorrect. Do not close the ItemView directly!
}
});
});
},
onSignInFormSubmit: function(event) {
event.preventDefault();
var errors = this.signInForm.validate();
var data = this.signInForm.getValue();
// [...] Notify that data has been submitted.
},
hideForm: function() {
this.$el.trigger("reveal:close");
}
});
});
I noticed a major mistake in my implementation. In the callback function closed of Reveal I decided to close the ItemView directly which is wrong as you can read in the documentation of Marionette.js:
View implements a close method, which is called by the region managers automatically.
Bugfix: Instead close() should be called on the region. I fixed this error.
Now I ask myself how can I actually write a test which covers the problem. I use Jasmine for testing. I noticed that the event handler onSignInFormSubmit is no longer called after I have incorrectly closed the ItemView and try to re-submit the form.
Here is a first draft of the test which unfortunately does fail also with the bugfix:
it("should call the submit handler for the sign-in form", function() {
spyOn(userController.loginView, "onSignInFormSubmit");
spyOn(userController.loginView.signInForm, "validate").andCallFake(function(params) {
return null;
});
userController.loginView.hideForm();
userController.loginView.ui.signInForm.trigger("submit");
expect(userController.loginView.onSignInFormSubmit).toHaveBeenCalled();
});
Maybe one could also test if the event handler is registered such as:
expect(userController.loginView.events["submit #signin-form"]).toEqual("onSignInFormSubmit");

Backbone Layout Manager subview's events doesn't work after reload

I'm using the Backbone Layout Manager Boilerplate. Unfortunately, a quite frustrating bug occurred. I like render a list of items as subviews inserted by insertView function. At the first load everthing works fine. But after a reload the the click events doesn't work anymore :(. I already tried to call delegateEvents() on the TableItem View manually but nothing changed. I hope anyone can give me a clue.
App.Views.Item = Backbone.View.extend({
template: "templates/item",
tagName: "li",
events: {
"click .applyButton" : "apply",
"click .viewDetailsButton" : "showDetail"
},
serialize: function() {
return { table : this.model.toJSON() };
},
apply: function(ev) {
ev.preventDefault();
alert("apply button clicked");
},
showDetail: function(ev) {
ev.preventDefault();
var id = this.model.get("_id");
app.router.navigate("#events/"+ id, {trigger : true})
}
});
/*
* List View
*/
App.Views.List = Backbone.View.extend({
template: "templates/list",
tagNam: "ul",
className: "tableList",
beforeRender: function() {
var events = this.model.get("userEvents").get("hosting");
events.each(function(model) {
this.insertView(new App.Views.Item({ model : model }));
}, this);
},
serialize: function() {
return {};
}
});
I think you might want to add a cleanup function on your Item view to undelegate the events when layoutmanager removes the view. I don't know if this will fix your problem, but it seems like good practise.
When you say after a reload, do you mean reloading the page with the browser reload button? if so, how do you get it to work in the first place?
It would help if you could provide a jsfiddle of your setup, or point us to a repo so we can test it on our machines. Make sure you include the router so that we can have a look at how the view and the layout that contains it are initialised.

Backbone.js:"Maximum call stack size exceeded" error

suppose I have a model and a view ,ths view have two method:one is bind the document mousemove event and the other is unbind method,defalut I give the document mousemove event, once the model's enable value changed I will call the view's unbind method:
window.ConfigModel = Backbone.Model.extend({
defaults: {
'enable':0
},
initialize: function(){
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
},
change:function () {
this.set('enable', 9);
}
})
window.PortView = Backbone.View.extend({
viewOn: function () {
$(document).on('mousemove', function () {
console.log('move')
})
},
viewOff: function () {
$(document).off('mousemove');
}
})
then I put an input on the document to call the model changed:
$('input').click(function () {
var configModel = new ConfigModel();
configModel.change();
})
the boot script is :
var portView1 = new PortView();
portView1.viewOn();
The problem is once I call the click the input button ,the chrome would tell me an error:Maximum call stack size exceeded it seems the change be invoke many times.So what's the problem with my problem ,how can I solve this problem
Backbone models already have a change method:
change model.change()
Manually trigger the "change" event and a "change:attribute" event for each attribute that has changed. If you've been passing {silent: true} to the set function in order to aggregate rapid changes to a model, you'll want to call model.change() when you're all finished.
Presumably something inside Backbone is trying to call configModel.change() and getting your version of change which triggers another change() call inside Backbone which runs your change which ... until the stack blows up.
You should use a different name for your change method.
That said, your code structure is somewhat bizarre. A model listening to events on itself is well and good but a model creating a view is odd:
initialize: function() {
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
}
And instantiating a view simply to call a single method and then throw it away is strange as is creating a new model just to trigger an event.
I think you probably want to have a single ConfigModel instance as part of your application state, say app.config. Then your click handler would talk to that model:
$('input').click(function () {
app.config.enable_level_9(); // or whatever your 'change' gets renamed to
});
Then you'd have some other part of your application (not necessarily a view) that listens for changes to app.config and acts appropriately:
app.viewOn = function() {
$(document).on('mousemove', function() {
console.log('move')
});
};
app.viewOff = function() {
$(document).off('mousemove');
};
app.init = function() {
app.config = new ConfigModel();
app.viewOn();
$('input').click(function () {
app.config.enable_level_9();
});
// ...
};
And then start the application with a single app.init() call:
$(function() {
app.init();
});

Listen to body click inside a view with backbone.js

I'm creating a modal dialog like this
window.NewPageModalView = Backbone.View.extend({
template: _.template($('#view-template-new-page-dialog').html()),
el: $('div#main'),
events: {
'click input[type=radio]': 'newPage'
},
newPage: function (event) {
$(event.currentTarget).closest('form').submit();
},
initialize: function () { },
render: function () {
$(this.el).append(this.template());
return this;
}
});
and then I create it inside another view like this
addPage: function (event) {
event.preventDefault();
var modal = new NewPageModalView();
modal.render();
}
this works just great but what is the best way if I want to close the dialog on body click or when pressing escape?
Generally speaking when you bind events in backbone using the events hash they are delegated to the view's el, however you can still bind events to something else in the initialize method (in your case the body).
initialize: function() {
$('body').bind('click', yourfunction);
}
Edit:
As #muistooshort mentions, you will want to make sure to also unbind the event.

Resources