This is a tricky problem to explain but I will try my best:
In Short: I have a Phonegap application that is using Backbone. When a touch event (on element A) is triggered on a view (lets say View A) and that event navigates to a new view (View B). An event is fired on an element (element B) on View B if element B is in the same position as element A.
Detailed: As mentioned above, the application makes use of Backbone. The problem only occurs on a mobile device and not on a browser on my machine. I have implemented jQuery Touch events to work with normal jQuery.
Snippet from my Router:
routes : {
"cart" : "cart",
"menu" : "menu"
},
cart: function (args, options) {
var options = options || {};
var view = App.Router.loadView(new App.Views.CartView(options), {access:true});
return view;
},
menu: function (args, options) {
var options = options || {};
var view = App.Router.loadView(new App.Views.MenuView(options));
return view;
},
loadView: function (view, options) {
var options = options || {},
returnview = view;
if (App.view) {
//Close views and subviews
_.each(App.view.subViews, function (subView) {
subView.close();
});
App.view.close();
}
App.view = returnview;
return $('#app-content').append(App.view.render().$el);
}
Snippet from MenuView
events: {
'tap #cart': function () {
App.Router.navigate('cart', {trigger:true});
}
},
Snippet from my CartView
'change #article-stock': function (e) {
alert('this should not happen!')
}
The scenario presents itself when I tap on an element on my menu (#cart), which in turn calls navigate, which creates the new view (CartView). CartView has a checkbox that is in the same position as where the #cart element was on the previsou view. When CartView is rendered the checkbox is toggled and I receive the alert, even though there was not event on that view. Its as if the event on the previous view bubbles through to the next view.
I obviously don't intent for this to happen. Does anyone know why this occurs, and how can this be prevented?
I hope I explained the issue well enough.
I have searched for a solution to my problem, but the only results I find are relating to events firing twice on the same view and not a single event firing on multiple views
Whilst going through my questions today I saw this one, and thought I would post my solution to this problem if anybody ever has the same issue.
I was never able to fully figure out what the actual cause of the problem was, my guess is that it is just the way the Android browser handles events, but by adding e.preventDefault(); to my events fixed it:
Snippet from MenuView
events: {
'tap #cart': function (e) {
e.preventDefault();
App.Router.navigate('cart', {trigger:true});
}
},
Snippet from my CartView
'change #article-stock': function (e) {
e.preventDefault();
alert('this should not happen!')
}
Related
I am working on Test cases for Marionette's View. By using events attribute, I have written a callback function on click of an HTML element. The functionality is working but I am struggling in writing test cases. I am not able to cover that click event using Jasmine test cases.
I have used the Marionette Region to render the view.
I have tried using spies but those are not working.
Please find code below for more details:
var TestView = Backbone.Marionette.CompositeView.extend({
tagName: 'div',
className: 'test-menu',
childView: testMenuView,
childViewOptions: function() {
return {
'componentId': this.cid
};
},
template: _.template(testTemplate),
initialize: function(options) {
this.collection = this.options.testData;
},
onShow: function(collectionView) {
collectionView.$el.show();
},
attachHtml: function(collectionView,itemView) {
collectionView.$("#testMenu").append(itemView.el);
},
events: {
'click #testBtn': function (event) {
alert('testBtn Clicked');
}
}
});
I like to use JQuery where I can when I write tests due to it being less verbose, and for most events triggering the handlers with JQuery will also work fine.
Given that you've got everything else set up for running a Jasmine suite I'd do
it('reacts to click events on its button', function() {
var view = new TestView();
view.render();
view.$('#testBtn').click(); // or view.$('#testBtn').trigger('click')
//(verify that the view did what was expected)
});
If testing the alert is the actual problem then use a spy for that, e.g. spyOn(window, 'alert') and expect(window.alert).ToHaveBeenCalledWith('testBtn Clicked')
JQuery won't always be able to trigger event handlers that are bound with addEventListener. Click is not one of those events, but there are situations where I have to trigger events using for examples
var event = document.createEvent('Event');
event.initEvent('keydown');
event.keyCode = 40;
event.altKey = true;
view.el.querySelector('input').dispatchEvent(event);
But most of the time just using JQuery's .trigger or corresponding shortcut-function directly on the element you want is fine.
I've been trying to debug my Backbone multi-page app for most of the day now to get rid of 'zombies', but unfortunately to no avail. Before today, I didn't even realize I have a zombie problem. What am I doing wrong?
This is my RegionManager:
var regionManager = (function() {
var currView = null;
var rm = {};
var closeView = function(view) {
if (view && view.close) {
view.close();
}
};
var openView = function(view) {
view.render();
if (view.onShow) {
view.onShow();
}
};
rm.show = function(view) {
closeView(currView);
currView = view;
openView(currView);
};
return rm;
})();
This is my View cleaning up function:
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
if (this.views) {
_.invoke(this.views, 'close');
}
// Unbind any view's events.
this.off();
// Unbind any model and collection events that the view is bound to.
if (this.model) {
this.model.off(null, null, this);
}
if (this.collection) {
this.collection.off(null, null, this);
}
// Clean up the HTML.
this.$el.empty();
};
I tried appending the View els directly to the body and using this.remove(); in the View clean-up function (instead of using a common el: $('#content') to which I am appending elements, then cleaning up by this.$el.empty()), but that didn't work either.
It might have something to do with my "global Events":
Backbone.Events.on('letterMouseDown', this.letterMouseDown, this);
But I take care of them with the onClose function:
onClose: function() {
Backbone.Events.off('letterMouseDown');
}
One problem I see is that your close function never removes the event delegator from the view's el. A view's events are handled by using the delegator form of jQuery's on to attach a single event handler to the view's el. Your close does:
this.$el.empty();
but that only removes the content and any event handlers attached to that content, it does nothing at all to the handlers attached directly to this.el. Consider this minimal example:
var V = Backbone.View.extend({
events: {
'click': 'clicked'
},
clicked: function() {
console.log('still here');
}
});
var v = new V({ el: '#el' });
v.close();
After that, clicking on #el will throw a 'still here' in the console even though you think that the view has been fully cleaned up. Demo: http://jsfiddle.net/ambiguous/aqdq7pwm/
Adding an undelegateEvents call to your close should take care of this problem.
General advice:
Don't use the old-school on and off functions for events, use listenTo and stopListening instead. listenTo keeps track of the events on the listener so it is easier to remove them all later.
Simplify your close to just this:
Backbone.View.prototype.close = function() {
if(this.onClose)
this.onClose();
if(this.views)
_.invoke(this.views, 'close');
this.remove();
};
Don't bind views to existing els. Let the view create (and own) its own el and let the caller place that el into a container with the usual:
var v = new View();
container.append(v.render().el);
pattern. If you must attach to an existing el then the view should override remove with a slightly modified version of the standard implementation:
remove: function() {
this.$el.empty(); // Instead of removing the element.
this.undelegateEvents(); // Manually detach the event delegator.
this.stopListening();
return this;
}
I'm pretty sure I found the root for my problem.
mu is too short was right, with the close() method I wasn't removing the events bound directly to my el (which I tried to do by this.off() - this.$el.off()/this.undelegateEvents() is the correct way). But for me, it only fixed the problem that events got called multiple times unnecessarily.
The reason I was plagued by 'zombie views' or unintended behavior was that I wasn't freeing up the memory in the View..
this.remove() only gets rid of the el and it's elements/events, but not the View's internal variables. To elaborate - in my View I have an array declared like so this.array: [] and I didn't have it freed in the onClose function.
All I had to do was empty it in the onClose function or initially declare the array as this.array: null so on recurrent View renderings it would at least free the previous array (it still should be freed on the onClose method though, because the array/object is still going to sit in the memory until browsing away from the page).
It was excruciating to debug, because it's a crossword game (at least my code is hard to read there) and sometimes the words didn't match up, but I didn't know where the problem was coming from.
Lessons learned.
I'm working on a chat client, and I've come across an issue with Backbone's event system.
When the client starts, I render the view as so:
var view = new ServerListView({collection: Chatter.servers});
Chatter.Views.servers = view;
$('#channels > ul').html(view.render().el);
Works fine, events are called.
Speaking of events those are:
events: {
"click .server": "server",
"click .server ul li": "channel",
"click .server .slider": "slide"
},
The render method in question:
render: function(){
var self = this;
this.$el.html("");
this.collection.each(function(server){
self.$el.append(self.template(server.toJSON()));
var header = self.$el.find('li.server[data-id="' + server.id + '"]')
var connection = Chatter.Connections[server.id];
if (connection) {
if (connection.channels.length > 0) {
connection.channels.each(function(channel) {
$(header).find("ul").append("<li data-channel-id=\"" + channel.id + "\">" + channel.get('name') + "</li>");
}, self);
}
}
}, self);
this.delegateEvents();
return self;
},
Everytime this is called, it should re-render the view with all the Servers and Channels correctly. Everything renders correctly, but the events are not being called.
I re-render it the same way I render it:
$('#channels > ul').html(Chatter.Views.servers.render().el);
For whatever reason the above events are not being called when rendering it, any ideas?
Thanks
EDIT: I forgot to mention something:
If I re-render it with:
var view = Chatter.Views.servers;
$('#channels > ul').html(view.render().el);
view.delegateEvents();
Then it works just fine, which I know that would be okay, but I'm 99% sure I shouldn't have to do that, and I don't want to have to do that for every time that I re render the view.
Thanks again.
This stackoverflow question might help. Do you have an initialize function for the view? Maybe the initialize function could accept an argument (the jQuery object that specifies where the view should render into), so that it renders itself into the proper place on the page, then you can delegate the events within the initialize function if the events aren't delegated automatically.
In any case, I'd be intrigued to hear from others.
I am still kind of new to backbone and I have a question. I am currently working on a previously created backbone view. This view is responsible for all the saving within the application. The class is becoming exceedingly large and I wanted to split out some of the events/methods/function to their own "sub class", if that is possible. For example, I have a group of events and functions that are responsible for a specific task. I would like to move those event and functions to their own js page, in a sub folder. The problem is that the events are being called. Here is a general overview of how the class is setup:
var myClass= myClassBaseClass.extend({
events: {
.... all my events here,
'click .eventOneButton': 'eventOne',
},
initialize: function (options) {
//initialize stuff here
},
postRender: function (renderOptions) {
//post render here
},
preRender: function (renderOptions, html) {
return html;
}, template: function (renderOptions) { //template stuff },
//...etc.
//my events
//eventOne was here, now its moved to its own file
});
My new file is in a subfolder, and looks like this:
var myClassSubClass= myClass.extend({
eventOne: function(e){
//event stuff here
}
});
So, is what am I doing wrong? Why wont the event get caught in the second file?
thanks
jason
I am new to Javascript and backbone.js, so hopefully I am missing something simple here. I am experimenting with some sample code I found which is supposed to check for unsaved changes before allowing a user to navigate away to another page. I have created a JSfiddle here:
http://jsfiddle.net/U43T5/4/
The code subscribes to the hashchange event like this:
$(window).on("hashchange", router.hashChange);
And the router.hashChange function checks a "dirty" flag to determine whether or not to allow the navigation, like this:
hashChange: function (evt) {
if (this.cancelNavigate) { // cancel out if just reverting the URL
evt.stopImmediatePropagation();
this.cancelNavigate = false;
return;
}
if (this.view && this.view.dirty) {
var dialog = confirm("You have unsaved changes. To stay on the page, press cancel. To discard changes and leave the page, press OK");
if (dialog == true) return;
else {
evt.stopImmediatePropagation();
this.cancelNavigate = true;
window.location.href = evt.originalEvent.oldURL;
}
}
},
The problem is that the code is not working because this.view is undefined, so the 2nd if block is never entered.
I would like the sample program to always ask for confirmation before navigating away from the page (in my sample program, I have set this.view.dirty to always be true, which is why it should always ask for confirmation). Or if there is a better approach, I am open to alternatives.
The main issue is the this context in the methods , this corresponds to the Window Object and not the Router. So it always remains undefined as you are defining view on the router. Declare a initialize method which binds the context inside the methods to router.
initialize: function() {
_.bindAll(this, 'loadView', 'hashChange');
},
Check Fiddle
I spent a lot of time to make at least something decent.
I ended up writing a wrapper for Backbone function:
var ignore = false;
Backbone.history.checkUrl = function() {
if (ignore) {
ignore = false;
return;
}
app.dirtyModelHandler.confirm(this, function () {
Backbone.History.prototype.checkUrl.call(Backbone.history);
},
function() {
ignore = true;
window.history.forward();
});
};
app.dirtyModelHandler.confirm is a function which shows confirmation (Ok, Cancel) view and takes success and cancel functions as arguments.