I have been playing with backbone and trying to learn it. I'm stuck at this point for a while. Not able to figure out what's wrong with following code?
render: function() {
this.$el.empty();
// render each subview, appending to our root element
_.each(this._views, function(sub_view) {
this.$el.append(sub_view.render().el); // Error on this line
});
You have got context issue. this you are refering to doesn't contain the $el you are looking for. You can fix this by declaring a self variable that points to appropriate this. Following code should work for you.
render: function() {
var self = this; //Added this line to declare variable (self) that point to 'this'
this.$el.empty();
_.each(this._views, function(sub_view) {
self.$el.append(sub_view.render().el); //Used 'self' here instead 'this'
});
Side Note: As you are leaning backbone also you should know about a very commong JavaScript problem with document reflow. You are render a view for every single model in the collection. It can lead to performance issues and especially on old computers and mobile devices. You can optimise your code by rendering everything in container and appending it once, rather than updating DOM each time. Here is an example:
render: function() {
this.$el.empty();
var container = document.createDocumentFragment();
_.each(this._views, function(sub_view) {
container.appendChild(sub_view.render().el)
});
this.$el.append(container);
}
Related
I am working on a Backbone demo app that shows a list of tweets. As I am replacing all the "tweets" with different data I clear the list using $.html()
render: function() {
$("#item-table").html('');
this.collection.each(this.addItem);
}
I was wondering if anyone could give me a hint with what can I replace this $.html() for better performance, because by using $.html() I am causing reflows and which gives bad layout process times.
There are two other places in the code where I use $.html() and it would be really great if someone could give me advice on how to change those too if those other places are even possible.
Create a new DocumentFragment to pre-render all the items, then update the DOM once.
Also, favor this.$(...) over the global jQuery selector $(...).
this.$ is a proxy to this.$el.find(...) which is more efficient, and less prone to select something outside of the view.
Using jQuery's core function ($()) inside a view can fail if the view wasn't rendered yet. So it's better to always manipulate through this.$el so you can make changes even before the view is actually put in the DOM.
Keep all the sub views created in an array to cleanly remove them later.
initialize: function() {
this.childViews = [];
},
render: function() {
// cache the list jQuery object
this.$list = this.$("#item-table");
// Make sure to destroy every child view explicitely
// to avoid memory leaks
this.cleanup();
this.renderCollection();
return this;
},
The real optimization starts here, with a temporary container.
renderCollection: function() {
var container = document.createDocumentFragment();
this.collection.each(function(model) {
// this appends to a in memory document
container.appendChild(this.renderItem(model, false).el);
}, this);
// Update the DOM only once every child view was rendered.
this.$list.html(container);
return this;
},
Our renderItem function can still be used to render a single item view and immediatly put it in the DOM. But it also provides an option to postpone the DOM manipulation and it just returns the view.
renderItem: function(model, render) {
var view = new Item({ model: model });
this.childViews.push(view);
view.render();
if (render !== false) this.$list.append(view.el);
return view;
},
To avoid memory leaks with dangling listeners, it's important to call remove on each view before forgetting about it.
I use an additional optimization by deferring the actual call to remove so we don't waste time now while the user waits.
cleanup: function() {
var _childViewsDump = [].concat(this.childViews);
this.childViews = [];
while (_childViewsDump.length > 0) {
var currentView = _childViewsDump.shift();
// defer the removal as it's less important for now than rendering.
_.defer(currentView.remove.bind(currentView), options);
}
}
I'm struggling with getting the concept of memory management with single page applications. This is my code:
var FilterModel = Backbone.Model.extend({});
var taskView = Backbone.View.extend({
template: _.template('<h1><%= title %></h1>'),
initialize: function(){
this.render();
this.listenTo(this.model, 'destroy', this.remove);
console.log(this.model)
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
},
events:{
'click h1': 'removeView'
},
removeView: function(){
this.model.destroy();
console.log('removed');
}
});
var filterModel = new FilterModel({title: 'Test'});
var taskview = new taskView({model:filterModel});
// I make heap snapshot before and after the change!
setTimeout(function(){
$("h1").click()}, 3000
)
$('body').append(taskview.$el);
I was told by numerous articles that using "remove" and "destroy" would clean up any memory leaks when removing the DOM tree.
But Chrome profile utility tells otherwise. I get detached DOM elements no matter what I do.
UPDATE!!!
After trying a few things in the responses I still get this in Google Chrome:
Here is jsfiddle: http://jsfiddle.net/HUVHX/
taskview is still holding a strong reference to this.el, although it is not connected to the dom. This is not a memory leak because taskview is held strongly also by it's variable
To test my assumption just add:
removeView: function(){
this.model.destroy();
this.el = undefined;
this.$el = undefined;
}
Another approach is to undef taskview var
EDIT:
When I change: "click h1" : "removeView" To "click": "removeView" it solves the detached dom node leak.
I suspect this has something to do with jquery selector caching.
You can see in backbone code, the difference is in calling jquery on function with a selector:
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
I tried to trace the cache deep into jquery code, with no luck.
So Janck, you can fin your answer here:
Backbone remove view and DOM nodes
The problems is that you have to do more than just remove you model and view.
You need to properly destroy all of the events and other bindings that are hanging around when you try to close your views.
I don't know if you know about Marionette.js (Backbone.Marionette), but it's a great extension to Backbone to handle this Zombie Views and to create robust JS applications.
You can read some articles about this as well, they were pointed in the Stackoverflow link that I posted.
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
http://lostechies.com/derickbailey/2012/03/19/backbone-js-and-javascript-garbage-collection/
But the logic is this: If a View is listening a model, then the contrary also occurs, so you'll always get a instance of your View in your DOM.
I have a sample single-page-application in Backbone that I am messing around with. The idea is that I have a button that will trigger a refresh on a view. I am trying to get the event handler to remember 'this' as the backbone view, not the element that it was called on. No matter how much docs I read, I cant seem to make it past this mental hump.
in my view, I have
initialize: function() {'
//i am trying to say, call render on this view when the button is clicked. I have tried all of these calls below.
//this.$("#add-tweet-button").click(this.render);
//this.$("#add-button").bind("click", this.render);
}
When the render function is called, the 'this' element is the button. I know what im missing is pretty easy, can someone help me out with it? Also, is this sound as coding conventions go?
If you use the View's 'delegateEvents' functionality, the scoping is taken care of for you:
var yourView = Backbone.View.extend({
events: {
"click #add-tweet-button" : "render"
},
render: function() {
// your render function
return this;
}
});
This only works with elements that are 'under' the View's El. But, your example shows this.$(...), so I'm assuming this is the case.
#Edward M Smith is right, although if you need to handle a jquery event of element outside the scope of your View you might write it that way :
var yourView = Backbone.View.extend({
initialize: function() {
var self = this;
$("button").click(function () {
self.render.apply(self, arguments);
});
},
render: function(event) {
// this is your View here...
}
});
I'm using Backbone.js, and in one of my main views I've encountered a very strange bug that I can't for the life of me figure out how to solve.
The view looks a look like the new Twitter layout. It receives an array of objects, each of which describes a collection and views elements that act on that collection. Each collection is represented by one tab in the view. The render() method on my view takes this array of collection objects, clears out the tabContainer DOM element if it isn't already empty, renders the tabs and then binds events to each of those tabs.
Now in my code I have the method to render the tabs and the method to bind the click handlers to those tabs sequentially. This works fine the first time I execute render(), but on subsequent calls of render(), the click handlers are not bound. Here's the relevant code snippet:
initialize: function() {
// Context on render(), _addAllTabs and _bindTabEvents is set correctly to 'this'
_.bindAll(this, 'render', 'openModel', 'closeModel', 'isOpen', 'addAllModels', '_switchTab',
'addOneModel', '_addTab', '_removeTab', '_addAllTabs', '_loadCollection',
'_renderControls', '_setCurrentCollection', '_loadModels', '_bindTabEvents');
this.template = JST['ui/viewer'];
$(this.el).html(this.template({}));
// The tabContainer is cached and always available
this.tabContainer = this.$("ul.tabs");
this.collectionContainer = this.$("#collection_container");
this.controlsContainer = this.$("#controls");
this.showMoreButton = this.$("#show_more_button");
},
render: function(collections, dashboard) {
// If _bindTabEvents has been called before, then this.tab exists. I
// intentionally destroy this.tabs and all previously bound click handlers.
if (this.tabs) this.tabContainer.html("");
if (collections) this.collections = collections;
if (dashboard) this.$("#dashboard").html(dashboard.render().el);
// _addAllTabs redraws each of the tabs in my view from scratch using _addTab
this._addAllTabs();
// All tabs *are* present in the DOM before my _bindTabEvents function is called
// However these events are only bound on the first render and not subsequent renders
this._bindTabEvents();
var first_tab = this.collections[0].id;
this.openTab(first_tab);
return this;
},
openTab: function (collectionId, e) {
// If I move _bindTabEvents to here, (per my more thorough explanation below)
// my bug is somehow magically fixed. This makes no friggin sense.
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
_addAllTabs: function() {
_.each(this.collections, this._addTab );
},
_bindTabEvents: function() {
this.tabs = _.reduce(_.pluck(this.collections, "id"), _.bind(function (tabEvents, collectionId) {
var tabId = this.$("#" + collectionId + "_tab");
tabEvents[collectionId] = tabId.click(_.bind(this._switchTab, this, collectionId));
return tabEvents
}, this), {});
},
_addTab: function(content) {
this.tabContainer.append(
$('<li/>')
.attr("id", content.id + "_tab")
.addClass("tab")
.append($('<span/>')
.addClass('label')
.text(content.name)));
//this._loadCollection(content.id);
this.bind("tab:" + content.id, this._loadCollection);
pg.account.bind("change:location", this._loadCollection); // TODO: Should this be here?
},
etc..
As I said, the render() method here does work, but only the first time around. The strange part is that if I move the line this._bindTabEvents(); and make it the first line of the openTab() method like in the following snippet, then the whole thing works perfectly:
openTab: function (collectionId, e) {
this._bindTabEvents();
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
Of course, that line of code has no business being in that method, but it does make the whole thing work fine, which leads me to ask why it works there, but doesn't work sequentially like so:
this._addAllTabs();
this._bindTabEvents();
This makes no sense to me since, it also doesn't work if I put it after this line:
var first_tab = this.collections[0].id;
even though that is essentially the same as what does work insofar as execution order is concerned.
Does anyone have any idea what I'm doing wrong and what I should be doing to make this correct (in terms of both behavior and coding style)?
In your view's render function, return this.delegateEvents(); I think you are losing your event bindings across your renderings and you need to re-establish them.
See this link for the backbone.js documentation for that function:
backbone.js - delegateEvents
When you switch tabs you are not simply showing/hiding content you are destroying and rebuild dom element so you are also destroying event liseners attached to them. that is why the events only work once and why adding _bindTabEvents into render works, because you are re-attaching the events each time.
when this line executes : this.tabContainer.html(""); poof... no more tabs and no more tab events.
I'm working on a plugin that causes something to happen in selected elements when the window is resized. The logic on what happens is perfectly fine, but I'm running into a conundrum when it comes to where to hook on the event listeners.
Basically, the code looks something like this:
$.fn.beAwesome = function() {
return this.each(function(){
// ???
});
}
While I've completed the rest of the plugin (and have it working just fine on, say, a click even on this), I can't put it "all together" without solving this central issue.
I considered just adding an extra bind resize to $(window) for each awesome element, but there'd be no way to access the element from within the closure:
$.fn.beAwesome = function() {
$window = $(window);
return this.each(function(){
$window.resize(function(){
// can't access initial awesome element
});
});
}
The other solution that sprung to mind was to instantiate a global data store (probably in $(document).data('beAwesome') or something like that. This, however, doesn't seem Javascript-like, blocking off access once the function runs its course, so I'd have to come up with some roundabout ways to do things like removing the hook. I've definitely seen approaches like these in Javascript libraries I've used in the past, but I don't know whether that's due to necessity or laziness on the authors' parts.
So is there an effective way to accomplish what I'm looking for?
You shouldn't bind to the resize event multiple times, will just end up with bloat and slowness.
What you could do is create a var to store all the elements that are to beAwesome. Bind to the resize event once. And then on resize do something will all the elements that are in the var.
(function($){ // closure to keep your pluggin contained.
var elems = $([]);
$(window).bind('resize.beAwesome', function(){
if (!elems.length) return; //no need to continue if elems in empty.
// Do something with elems here.
});
$.fn.beAwesome = function() {
elems = elems.add(this);
return this;
}
})(jQuery);
You also might want to namespace your resize event.