Cristi Mihai
Cristi Mihai

Reputation: 2565

Get notified when a Backbone View has been rendered

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:

  1. create the view > render it > attach it:

    view = new MyList().render()
    $('#dummy').append(view.$el)
    
  2. 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

Answers (3)

Alexander Mills
Alexander Mills

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

Justin Warkentin
Justin Warkentin

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

nickf
nickf

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

Related Questions