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.
Related
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 am trying to implement endless scrolling with Backbonejs. My view initializes a collection and calls fetch fetch function.
My view
var app = app || {};
app.PostListView = Backbone.View.extend({
el: '#posts',
initialize: function( ) {
this.collection = new app.PostList();
this.collection.on("sync", this.render, this);
this.collection.fetch();
this.render();
},
render: function() {
/*render posts*/
}
});
In my page I added the following code. It checks if the the user at the bottom of the page. If yes then it checks if the view is initialized. If yes then call that view fetch function of the view's collection object.
var app = app || {};
$(function() {
var post_view;
$(window).scroll(function() {
if(($(window).scrollTop() + $(window).height() == getDocHeight()) && busy==0) {
if(!post_view){
post_view = new app.PostListView();
} else {
post_view.collection.fetch();
}
}
});
});
So far this code is working. I am not sure if this is the right approach or not?
It's not a bad option; it works, and Backbone is making that collection available for you. But there's a couple of other options to consider:
Move that collection.fetch into a method getMoreItems() inside your PostListView, and call it within your else block. That way you're encapsulating your logic inside the view. Your app is more modular that way, and you can make your scrolling smarter without updating the rest of your app.
Move the scroll listener inside your PostListView. I'd probably put this within your PostListView's initialize function. Again, this reduces dependencies between the various parts of your app - you don't have to remember "Whenever I create a PostListView, I must remember to update it on scroll." By setting that event listener within the PostListView itself, all you have to do is create it. Backbone's general philosophy is to have small, independent components that manage their own state; moving the event listener inside would fit with that.
I'm building a website with the Javascript Backbone framework and the standard underscore templating engine. I've got a listview which I load upon the first page load. Since the contents of this list may change server side, I want to update this list every two seconds. I tried doing this by adding a setInterval call:
var OpenTicketListView = Backbone.View.extend({
el: '#the-id-in-the-template',
render: function() {
var that = this;
var tickets = new TicketColection();
tickets.fetch({
success: function(openTickets){
console.log('WE GOT A RESPONSE!');
var template = _.template($('#my-template').html(), {tickets: tickets.models});
that.$el.html(template);
}
});
}
});
var openTicketListView = new OpenTicketListView();
router.on("route:home", function() {
openTicketListView.render();
assignedTicketListView.render();
});
setInterval(openTicketListView.render, 2000);
This setup seems to ALMOST work. The listview renders perfectly fine the first time. The setInterval also seems to work, since it gets updated lists from the server, which look good (with updated content) in the console. I also see the "WE GOT A RESPONSE!" in the console. BUT, the only thing that it refuses to do, is update the view visually.
Does anybody know what I might be doing wrong here? What is the stupidity that I'm on here? How can I possibly debug this?
All tips are welcome!
The problem is, the value of this inside the render function is being lost when you call the function from setInterval. One way of solving that would be to bind the context to the function:
setInterval(openTicketListView.render.bind(openTicketListView), 2000);
Its worth noting, there is no need to call the render function directly. Instead, you can call fetch from the polling method, and have the view bind to the collection's sync event, and re-render itself. Something like this:
var OpenTicketListView = Backbone.View.extend({
el: '#the-id-in-the-template',
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
this.collection.fetch();
},
render: function() {
console.log('WE GOT A RESPONSE!');
var template = _.template($('#my-template').html(), {tickets: this.collection.models});
that.$el.html(template);
}
});
var tickets = new TicketColection();
var openTicketListView = new OpenTicketListView({ collection: tickets });
setInterval(tickets.fetch.bind(tickets), 2000);
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!')
}
From what I understand of the way Backbone.js is intended to be used, Views are supposed to be rendered in their own $el element, which is not necessarily attached to the page. If it is so, the higher level view they depend on usually takes care of inserting the $el in the page.
I am making this statement after having read the Todo sample application. In this case, the TodoView renders element in a default div element that is not attached to the page.
var TodoView = Backbone.View.extend({
// [...]
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('done', this.model.get('done'));
this.input = this.$('.edit');
return this;
},
The AppView, upon a Todo creation, takes care of creating the TodoView and appending its $el to the page after rendering.
var AppView = Backbone.View.extend({
// [...]
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().$el);
},
My question is: If a view not attached to the page needs adjustments after being inserted (e.g. calculating its position in the viewport and performing DOM manipulation accordingly), where would you place the corresponding code?
Would you create a afterInsertion() method that the sublevel View should call after inserting, would you put the code at the same emplacement that where the insertion takes place (i.e. in the sublevel View) or would you change the way the view works to have it rendering directly in the page? I'm sure there are other solutions I can't think of right now. I would like to know what you consider being a best practice/optimized in the way Backbone should work, or if this question doesn't make sense to explain why.
I keep track of my sub-views. In a base view definition, create an add method:
var BaseView = Backbone.View.extend({
// It is really simplified... add the details you find necessary
add: function (name, viewDef, options) {
options = options || {};
if (!this.children)
this.children = {};
var view = new viewDef(options);
this.children[name] = view;
this.listenToOnce(view, 'ready', this.onSubViewReady);
view.render(options);
view.$el.appendTo(options.appendTo || this.$el);
}
});
With this, you can keep track of your subviews and make anything you want with them later.
If you feel like making things "automatic", you can trigger a ready event after your render method doing this:
var extend = BaseView.extend;
BaseView.extend = function (protoProps, staticProps) {
var child = extend.apply(this, arguments);
child.prototype.__render__ = protoProps['render'] || this.prototype.__render__ || function() {};
child.prototype.render = function () {
this.__render__.apply(this, arguments);
this.trigger('ready', this);
}
};
With this you can do a lot already.
Just remember that the DOM won't be drawn by the time that ready is triggered. So, if you are willling to do any calculations with the subview height or anything that needs the DOM to be drawn, use setTimeout(function () { ... }, 0) to put your code to the end of queue.
Hope I've helped.