Reputation: 2565
I'm working on a Backbone.View
which should render a collection as a scrollable list.
Part of the initial rendering, I need access to some layout properties (e.g. clientWidth
) which are only available after the view has been rendered.
My problem is, how do I know when a view has been added to the DOM?
Using a Backbone.View
there are typically 2 ways to attach a view to the DOM:
create the view > render it > attach it:
view = new MyList().render()
$('#dummy').append(view.$el)
create the view and render it in-place:
new MyList({el: '#dummy'}).render()
Note: I know (1) and (2) are not completely equivalent, that's not the point.
Let's consider my list is defined something like this:
class MyList extends Backbone.View
render: ->
@$el->html( ... )
@
layout: ->
max = $el.scrollWidth - $el.clientWidth
# ... disable / enable scrolling arrows based on max ...
How would you make sure layout() is called after MyList is attached to DOM?
Upvotes: 1
Views: 2495
Reputation: 100010
I am surprised at how bad some of these answers are. JQuery methods are synchronous.
So in the Backbone.View render function you have a few good options:
//just invoke a function
render:function(){
//first, use Backbone/jQuery to append to DOM here
//second
this.callSomeFunctionToSignifyRenderIsDone();
return this;
}
//trigger an event
render:function(){
//use Backbone/jQuery to append to DOM here
Backbone.Events.trigger('viewX-rendered'.); //trigger an event when render is complete
return this;
}
or, in the unlikely event that your render method has async code you could pass an optional callback to the render function:
render: function(callback){
//use Backbone/jQuery to append to DOM here
if(typeof callback === 'function'){
callback(null,'viewX-rendered'); //invoke the error-first callback
}
}
considering that returning 'this' from render in order to chain Backbone calls is the preferred pattern then the first two choices are probably the much better options.
lastly, if indeed your current render method is asynchronous, that is to say, the jQuery code that appends stuff to the DOM is happening async, then you can use jQuery to attach a listener to the DOM element like so
https://gist.github.com/azat-co/5898111
Upvotes: 0
Reputation: 10241
I've faced this very issue and there are patterns that can help. Backbone gives you tools but doesn't tell you how to use them. It's very important to establish good patterns as you work with it. There are two different patterns I have used that help with this.
1) Depending on how your application works, it can be easy or hard to make sure you always call an attached()
method or trigger an attached
event on a view after you have rendered it and attached it to the DOM. In my case, it was easy because I created an architecture where I could handle that in one central place.
2) If you always attach your element to the DOM immediately following rendering - specifically any time after you render without deferring attaching by doing so inside an asynchronous callback, then within your render() function you can defer the call to layout()
or whatever needs to run after being attached. Like so:
var MyList = Backbone.View.extend({
render: function() {
/* rendering logic goes here */
// Notice that the timeout is 0 - you can actually not even pass this argument
// but I wanted to emphasize that there is no delay given.
setTimeout(this.layout, 0);
return this;
},
layout: function() {
// This code won't be run until after this.el is attached to the DOM
}
});
var view = new MyList().render();
$('#dummy').append(view.$el);
Javascript is and always will be single threaded. I/O can be done asynchronously, but anything that actually requires time from the JS engine cannot run in parallel with other JS code. When you call setTimeout()
without the last argument or with a value of 0
it will essentially queue it up to run as soon as possible after the code that is currently executing has finished.
You can take advantage of this fact if you follow a pattern of always attaching to the DOM soon after calling render()
- any time before the currently executing code finishes running and returns control back to the JS engine to run whatever is queued up next. By the time the function queued up by setTimeout(func, 0)
is run, you then can know that your view has been attached to the DOM. Note that this is the same thing as what _.defer()
does, but I wanted to explain it for you.
Once again, patterns are very important to establish. If you are never consistent and don't establish patterns within your application, you don't have any guarantees and you lose the ability to count on this mechanism - and others you might otherwise be able to establish. Hope this helps!
Upvotes: 9
Reputation: 546035
That's a deceptively difficult problem. There are DOM Mutation Events, but AFAIK these aren't implemented cross-browser and are deprecated anyway. Mutation Observers seem to be the new way to do it, but again, I'm not sure of the compatibility.
A surefire, but expensive/messy way to do it is to poll to see if the body is somewhere in the ancestor chain.
whenInserted = function ($el, callback) {
check = function () {
if ($el.closest('body').length) { // assuming a jQuery object here
callback();
} else {
setTimeout(check, 100);
}
};
check();
};
Upvotes: 3