Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Meteor rendered callback and applying jQuery Plugins

Looking for a pattern when applying a jQuery plugin -- like a slider or isotope -- to a collection of DOM elements that are loaded with dynamic content from Meteor.

If you call the template.rendered (doc here) seems like a logical choice. When the template is rendered apply the jQuery.

According to the Blaze wiki template.rendered is now only called once. Sounds good. However it doesn't mentioned that template.rendered is called before the contents of the template are applied to the DOM.

So the recommended method is to put the inner elements into an {{#each}} sub-template and to then call jQuery on their rendered callback.

However: Most jQuery plugins don't work this way. They need to be called on the parent DOM element, and the child DOM elements need to already be in place.

Some goes as far as recommending a setTimeout on the parent element to approximate when the child elements will be rendered. This doesn't sound reliable enough for me.

Here is a common example:

page_slider.html

<template name="page_slider">
  <div class="slider">
    <ul class="slides">
      {{#each slide}}
        {{> slider_item}}
      {{/each}}
    </ul>
  </div>
</template>

page_slider.js

Template.page_slider.rendered = function() {
  /*
   * Can't initialise jQuery slider here as 
   * all inner DOM elements don't exist. 
   *
   */
};

Looking at the sub-templates.

slider_item.html

<template name="slider_item">
  <li class="slide"><img src="{{image}}"/></li>
</template>

slider_item.js

Template.slider_item.rendered = function() {
  /*
   * Can't initialise jQuery slider here either 
   * as I don't know if all slide elements have been loaded 
   *
   */
};

As you can see from the sample above, there's no opportunity to know when all slides have been loaded into the DOM and to therefore call a jQuery plugin.

Wanting to know if there is something I've overlooked, or if there is currently a common pattern others are using.

like image 863
onepixelsolid Avatar asked Aug 25 '14 13:08

onepixelsolid


1 Answers

The pattern I'm using for addressing this purpose is the following :

First, I externalize the cursor used to feed the #each block in a separate helper function because we are going to reuse it later.

// given a slider, return associated slides from the collection
function slides(slider){
  return Slides.find({
    sliderId:slider._id
  });
}

// assumes the slider template is called with a slider as data context
Template.slider.helpers({
  slides:function(){
    return slides(this);
  }
});

<template name="pageSlider">
  {{> slider mySlider}}
</template>

Template.pageSlider.helpers({
  mySlider:function(){
    return Sliders.findOne({
      name:"mySlider"
    });
  }
});

Now what we'd like to do is execute code after the #each block has finished inserting our items in the DOM, so we are going to "listen" to the same reactive data source that the #each block is listening to : this is why we declared the cursor as a separate function.

We'll setup a reactive computation to detect changes being made to the underlying Slides collection, and we'll execute some code that is going to wait until the #each block renders items in the DOM then reinitialize the jQuery slider.

Template.slider.rendered=function(){
  this.autorun(_.bind(function(){
    // we assume that the data context (this.data) is the slider doc itself
    // this line of code makes our computation depend on changes done to
    // the Slides collection
    var slidesCursor=slides(this.data);
    // we wait until the #each block invalidation has finished inserting items
    // in the DOM
    Deps.afterFlush(function(){
      // here it is safe to initialize your jQuery plugin because DOM is ready
    });
  }, this));
};

Deps.afterFlush is assuring us that we run our plugin initialization code AFTER the DOM rendering process implied by the #each block is done.

The setTimeout hack works by assuming that it will trigger after the flush cycle has finished, but is is ugly insofar as the Deps API provides a method specifically designed to run code after the current flush cycle is over.

As a quick recap, this is what is happening with this code under the hood :

  • Template.slider.rendered is called but the #each block has not yet rendered your slider items.
  • Our reactive computation is setup and we listen to updates from the Slides collection (just like the #each block is doing in its own distinct computation).
  • Some time later, the Slides collection is updated and both the #each block computation as well as our custom computation are invalidated - because they depend on the SAME cursor - and thus will rerun. Now the tricky part is that we can't tell which computation is going to rerun first, it could be one or the other, in an nondeterministic way. This is why we need to run the plugin initialization in a Deps.afterFlush callback.
  • The #each block logic is executed and items get inserted in the DOM.
  • The flush cycle (rerunning every invalidated computations) is done and our Deps.afterFlush callback is thus executed. 

This pattern allows us to reinitialize our jQuery plugins (carousels, masonry-like stuff, etc...) whenever new items are being added to the model and subsequently rendered in the DOM by Blaze.

The reinitialization process is plugin dependent, most of the time you will have either to call a reinit method if present or manually destroy and recreate the plugin.

The Meteor manual provides great explanations and examples of Deps and Blaze internals, this is definitely a recommended reading :

http://manual.meteor.com/

like image 137
saimeunt Avatar answered Oct 02 '22 12:10

saimeunt