I'm working on a Backbone app that renders several instances of the same view. Each of the views has a fairly large node tree and I can see that the user agent can't render the views instantaneously.
The issue I'm running into is that when my render callbacks fire, the height is coming through as 0 because the user agent's rendering engine hasn't actually finished rendering the entire view. If I set a timeout, the correct final height comes through:
var ChildView = window.Backbone.View.extend({
render: function() {
var template = require('templates/ChildTemplate');
this.$el.html(template());
this.afterRender();
},
afterRender: function() {
console.log(this.$el.outerHeight()); // 0
var _this = this;
setTimeout(function(){
console.log(_this.$el.outerHeight()); // 51 (or some non-zero integer)
}, 100);
setTimeout(function(){
console.log(_this.$el.outerHeight()); // 240, full correct height
}, 300);
}
});
How can I account for this rendering engine delay?
Architecture:
jquery 1.7.2
backbone 0.9.9
If the template contains images then i suggest you use the standard jquery load function
_this.$el.children('img').load(function(){
//the element should exist now
});
Otherwise im not sure what the problem is. Im fairly certain that Underscore's template function is synchronous so i wouldnt have suspected any problems there. One thing that might work, and probably makes more sense from a design perspective, would be to make use of initialize
var ChildView = window.Backbone.View.extend({
initialize: function(){
var template = require('templates/ChildTemplate');
this.renderedTemplate = template();
},
render: function() {
this.$el.html(this.renderedTemplate);
this.afterRender();
},
});
If you dont want to use the name renderedTemplate then thats fine, just make sure not to name it template as that is already in use
Related
I understand that when a view is removed through .remove(), .stopListening() is called on that view to remove any event listeners associated with that view in Backbone. From the Backbone docs:
remove view.remove()
Removes a view from the DOM, and calls stopListening to remove any bound events that the view has listenTo'd.
I have views that are appended to a container that only have events related to dom actions on themselves through Backbone's events hook.
var View = Backbone.View.extend({
events : {
'input keyup' : 'searchDropdown'
},
searchDropdown: function () {
$('dropdown').empty();
//Appends views based on search
}
});
My question is really whether or not I'm leaking any memory (significant or not) when calling $.empty() on a container that effectively removes the view(s) appended inside of it. And if I am, is there any good convention for accessing and calling .remove() on those views?
You don't need any special framework for this but it's a good idea to implement removal properly and not depend on the browser being smart enough to do this. Sometimes in a large app you will find you specifically need to override the remove method to do some special cleanup - for instance you are using a library in that view which has a destroy method.
A modern browser tends to have a GC which is smart enough for most cases but I still prefer not to rely on that. Recently I came on to a project in Backbone which had no concept of subviews and I reduced the leaking nodes by 50% by changing to remove from empty (in Chrome 43). It's very hard to have a large javascript app not leak memory, my advice is to monitor it early on: If a DOM Element is removed, are its listeners also removed from memory?
Watch out for things which leak a lot of memory - like images. I had some code on a project that did something like this:
var image = new Image();
image.onLoad(.. reference `image` ..)
image.src = ...
Basically a pre-loader. And because we weren't explicitly doing image = null the GC never kicked in because the callback was referencing the image variable. On an image heavy site we were leaking 1-2mb with every page transition which was crashing phones. Setting the variable to null in a remove override fixed this.
Calling remove on subviews is as easy as doing something like this:
remove: function() {
this.removeSubviews();
Backbone.View.prototype.remove.call(this);
},
removeSubviews: function() {
if (!_.isEmpty(this.subViews)) {
_.invoke(this.subViews, 'remove');
this.subViews = [];
}
}
You just need to add your subview instances to an array. For example when you create a subview you could have an option like parentView: this and add it to the array of the parent. I have done more intricate subview systems in the past but that would work fine. On initialize of the views you could do something like:
var parentView = this.options.parentView;
if (parentView) {
(parentView.subViews = parentView.subViews || []).push(this);
}
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.
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.
So I have two views here, their structure relies on $(window).height(). So when I re-size the browser or mess with the console, I need the height to be calculated. The below works for me, but I haven't seen any one mention doing it like this.
So I want to know if this code is awful or okay or great. Obviously all I am doing is telling my program when to instantiate my views.
// Play
$(document).ready(function() {
intro = new App.Views.Intro();
flight = new App.Views.Flight();
});
$(window).on('resize', function() {
intro = new App.Views.Intro();
flight = new App.Views.Flight();
});
Not sure if this fits the guidelines, but I do not want this to come back to hurt me later on.
Edit: Not sure why I didn't think of this, but below seems pretty practical. The reason I didn't want to re-render inside the view is because it seems I would have to add a remove() method and "bind" the resize. Not very pretty and organized if you ask me. example How do I add a resize event to the window in a view using Backbone?
// Play
$(document).ready(function() {
intro = new App.Views.Intro();
flight = new App.Views.Flight();
});
$(window).on('resize', function() {
intro.render();
flight.render();
});
You can create a base view class to encapsulate this behavior. This can be useful for a few reasons. One benefit is you can separate the resize behavior from the render code. Sometimes render does alot more work, such as serializing a model and rewriting large parts of the dom, which you don't want to re-run.
Here is a pen demonstrating a pattern I've used so that after the initial render only the width/height are adjusted and you don't need to re-render any of the views simply to resize them. The code is copied below for convenience.
This is just a sample. In a real app where I might be resizing a few views I typically refactor the window.resize event out so I only wire up one listener.
var ResizeView = Backbone.View.extend({
template: '#view-tpl',
initialize: function() {
$(window).on('resize.resizeview', this.onResize.bind(this));
},
remove: function() {
$(window).off('resize.resizeview');
Backbone.View.prototype.remove.call(this);
},
render: function () {
var $tmpl = $(this.template);
var tmpl = _.template($tmpl.html());
this.$el.append(tmpl());
this.onResize();
return this;
},
onResize: function () {
var w = $(window).width()
, h = $(window).height();
console.log('resize', w, h);
this.resize(w, h);
},
resize: function (w, h) {
this.$el.css({
'width': w,
'height': h
});
}
});
var view = new ResizeView();
$('body').append(view.render().$el);
I'm working on a application with Backbone.js.
When I'm switching of page, the global view of the current page is replaced with the view of the new page. Inside that view, I have a typical "render" function which replace the body html by the new template.
My problem is, after changing the html, so the DOM, how can I execute a function when the DOM is ready again ?
I have to use it because I need to scroll to an element and just executing my scrollTo() function after the render() don't work ($(myElement).offset().top always return me 0).
Here is my code :
module.exports = BaseView.extend({
initialize: function () {
this.render();
$('html, body').scrollTop(this.$("#toScroll").offset().top);
},
template: require('./templates/home'),
render: function () {
this.$el.html(this.template());
return this;
}
});
Thanks.
If your view's el is in the DOM and your #toScroll is inside the view's el, then #toScroll will be in the DOM as soon as you say this:
this.$el.html(this.template());
However, the HTML won't be rendered until after the browser gets control again (i.e. after your view has done all its work). The elements inside your el won't have any size or position information until the browser has rendered all of it (unless of course you're positioning and sizing everything by hand). The sequence of events goes like this:
v = new View.
initialize is called and that calls render.
render sets up everything in the DOM.
The view calls this.$('#toScroll').offset() and gets zeros.
The view tries to scrollTop to zero.
The browser gets control back.
The browser renders everything, this process computes the positions and offsets you want.
You just need to get 4 and 5 to happen after 7.
An easy way to do that is by putting your position/size dependent things inside a _.defer callback:
initialize: function () {
this.render();
var _this = this;
_(function() {
$('html, body').scrollTop(_this.$("#toScroll").offset().top);
}).defer();
}
You can also use a setTimeout with a delay of zero but _.defer makes your intent clearer.
Demo: http://jsfiddle.net/ambiguous/tWSBE/