6

I have the following template code

<template name="home">
    <div class="mainBox">
        <ul class="itemList">
            {{#each this}}
                {{> listItem}}
            {{/each}}
        </ul>
    </div>
</template>

<template name="listItem">
    <li class="item">
        {{username}}
    </li>
</template>

And I'd like to execute a code once ALL of the "listItem" are rendered. There are about 100 of them. I tried the following

Template.home.rendered = function() {
   // is this called once all of its 'subviews' are rendered?
};

But it doesn't wait until all views are loaded.

What's the best way of knowing when all sub-view templates are loaded?

ericbae
  • 9,604
  • 25
  • 75
  • 108
  • You somewhere subscribe to the data? Are you using iron router? – Peppe L-G Sep 10 '14 at 14:25
  • `listItem` templates get data from a presumably reactive collection, in which case you can never be sure that *all* of them are rendered. You can use a few tricks to get a notification when the initial set is drawn, though. Still, you are probably trying to do something not in the "Meteor way", so I'd look for a better solution. What are you trying to achieve? – Hubert OG Sep 10 '14 at 14:26

5 Answers5

3

This is how I proceed :

client/views/home/home.html

<template name="home">
  {{#if itemsReady}}
    {{> itemsList}}
  {{/if}}
</template>

<template name="itemsList">
  <ul>
    {{#each items}}
      {{> item}}
    {{/each}}
  </ul>
</template>

<template name="item">
  <li>{{value}}</li>
</template>

client/views/home/home.js

Template.home.helpers({
  itemsReady:function(){
    return Meteor.subscribe("items").ready();
  }
});

Template.itemsList.helpers({
  items:function(){
    return Items.find();
  }
});

Template.itemsList.rendered=function(){
  // will output 100, once
  console.log(this.$("li").length);
};

lib/collections/items.js

Items=new Mongo.Collection("items");

server/collections/items.js

insertItems=function(){
  var range=_.range(100);
  _.each(range,function(index){
    Items.insert({value:"Item "+index});
  });
};

Meteor.publish("items",function(){
  return Items.find();
});

server/startup.js

Meteor.startup(function(){
  Items.remove({});
  if(Items.find().count()===0){
    insertItems();
  }
});

We specify that we want to render our list of items only when the publication is ready, so by that time data is available and the correct number of li elements will get displayed in the list rendered callback.

Now the same using iron:router waitOn feature :

client/views/home/controller.js

HomeController=RouteController.extend({
  template:"home",
  waitOn:function(){
    return Meteor.subscribe("items");
  }
});

client/lib/router.js

Router.configure({
  loadingTemplate:"loading"
});

Router.onBeforeAction("loading");

Router.map(function(){
  this.route("home",{
    path:"/",
    controller:"HomeController"
  });
});

client/views/loading/loading.html

<template name="loading">
  <p>LOADING...</p>
</template>

Using iron:router is probably better because it solves a common pattern elegantly : we don't need the itemsReady helper anymore, the home template will get rendered only when the WaitList returned by waitOn will be globally ready.

One must not forget to add both a loading template and setup the default "loading" hook otherwise it won't work.

saimeunt
  • 22,666
  • 2
  • 56
  • 61
  • Just adding to @saimeunt's answer - in my route, I had "data:" function, but data function itself runs before "subscription" is finished, so, you need to add "this.ready()" conditional-check to make sure all the data is returned - https://github.com/EventedMind/iron-router/issues/265 – ericbae Sep 11 '14 at 07:41
1

I had this same problem with needing to wait on all my subtemplates to load before calling a slick JavaScript carousel plugin (or any cool JavaScript plugin like charts or graphs that need your whole data set loaded in the DOM before calling it).

I solved it by simply comparing the rank of the subtemplate to the overall count that should be returned for whatever query I was doing. Once the rank is equal to the count, you can call your plugin from the subtemplate.rendered helper because all the subtemplates have been inserted into the DOM. So in your example:

Template.listItem.rendered = function() {

    if(this.data.rank === ListItems.find({/* whatever query */}).count()) {
       console.log("Last item has been inserted into DOM!");
       //  Call your plugin
       $("#carousel").owlCarousel({
          //  plugin options, etc.
       });
    }
}

Then you just need your helper for listItems to return a rank, which is easy enough:

Template.home.helpers({

    listItems: function() {         
        return ListItems.find({/* whatever query */}).map(function(listItem, index) {
           listItem.rank = index + 1;  //  Starts at 1 versus 0, just a preference
        });
    }
}
evolross
  • 533
  • 1
  • 5
  • 15
0

the method rendered works of this way

This callback is called once when an instance of Template.myTemplate is rendered into DOM nodes and put into the document for the first time.

so, when is rendered you doesn't have variable reactive in this case.

// this would sufficient
Template.listItem.helpers = function() {
   username:function(){
      return ...
   }
};
Walter Zalazar
  • 541
  • 4
  • 11
0

I'd suggest something like:

var unrendered = [];

Template.listItem.created = function () {
  var newId = Random.id();
  this._id = newId;
  unrendered.push(newId);
};

Template.listItem.rendered = function () {
  unrendered = _.without(unrendered, this._id);
  if (!unrendered.length) {
    // WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
  }
};

CAVEAT

This works on the assumption that essentially all template instances will be created before they first ones have been rendered, otherwise your code will run before it should. I think this should be the case, but you'll have to try it out as I don't really have time to run a 100+ sub-template test. If it's not the case, then I can't see how you can achieve this behavior without knowing in advance exactly how many sub-templates will be created.

If you do know how many there will be then the code above can be simplified to a counter that decrements every time rendered runs, and it's easy.

unrendered = [number of listitems];

Template.listItem.rendered = function () {
  unrendered--;
  if (!unrendered) {
    // WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
  }
};

Also, you may need to meteor add random, but I think this package is now included in core.

richsilv
  • 7,993
  • 1
  • 23
  • 29
0

Apparently there are various ways to handle your situation. You could easily use template subscriptions.

Template.myView.onCreated(function() {
    var self = this;

    self.autorun(function(){
        self.mySub = self.subscribe('mySubscription');
    });

    if(self.mySub.ready()) {
        // my sweet fancy code...
    }
});


<template name="myTemplate">
    <ul>
        {{#if Template.subscriptionsReady}}
           {{#each items}}
               <li>{{item}}</li>
           {{/each}}
        {{else}}
          <div class="loading">Loading...</div>
        {{/if}}
    </ul>
</template>