Meteor: Call function after template is rendered with data
Asked Answered
L

2

3

I have a number of posts that I want to display inside of a carousel. For the carousel, I use OwlCarousel .

    <div class="owl-carousel" id="featured-carousel">
        {{#each featuredPosts}}
        <div>
            <h2>
                {{postTitle}}
            </h2>
        </div>
        {{/each}}
    </div>

I call my carousel like so:

Template.featuredCarousel.rendered = function(){
$('#featured-carousel').owlCarousel({
    loop:true,
    autoplay:true,
    autoplayTimeout:3000,
    items:1,
    smartSpeed:1080,
    padding:80
});
this.rendered = true;
};

The result I get is that Owl basically thinks that I just have one item to display in the carousel which are multiple divs. What apparently happens is that the function inside Template.featuredCarousel.rendered is called before the #each-part of the template is completed or before the data has arrived.

How can I make the function instantiating the carousel only be called after the template is fully rendered including all data?

Thank you very much for your help.

P.S.: I use iron-router for routing like so:

Router.map(function(){
this.route('home', {
    path: '/',
    waitOn: function(){
        return Meteor.subscribe('featured');
    },
    data: function(){
        return {featuredPosts: Featured.find({})};
    }
});
});

P.P.S.: I have also tried using a loading template but that doesn't help either.

Livery answered 17/8, 2014 at 22:2 Comment(0)
H
6

You have identified your problem correctly by stating that :

What apparently happens is that the function inside Template.featuredCarousel.rendered is called before the #each-part of the template is completed or before the data has arrived.

The rendered callback of a Template is only called once when your template instance is first inserted in the DOM, so if your data is not ready (fetched from server) yet the #each block won't generate any HTML elements and when you instantiate your carousel it will appear empty.

What you can do is make sure your data is ready before your rendered callback fires. Apparently you've tried to setup this solution with no luck, are you sure you added the default loading hook like so ?

Router.onBeforeAction("loading");

An even better solution is to listen for database changes in your rendered callback and reinitialize your carousel accordingly when items are first fetched, and then dynamically added and/or removed.

HTML

<template name="carousel">
  <div class="owl-carousel">
    {{#each featuredPosts}}
      {{! item declaration}}
    {{/each}}
  </div>
</template>

JS

function featuredPosts(){
  return Featured.find();
}

Template.carousel.helpers({
  // feed the #each with our cursor
  featuredPosts:featuredPosts
});

Template.carousel.rendered=function(){
  var options={...};
  // first initialization
  this.$(".owl-carousel").owlCarousel(options);
  this.autorun(_.bind(function(){
    // this is how we "listen" for databases change : we setup a reactive computation
    // and inside we reference a cursor (query) from the database and register
    // dependencies on it by calling cursor.forEach, so whenever the documents found
    // by the cursor are modified on the server, the computation is rerun with
    // updated content, note that we use the SAME CURSOR that we fed our #each with
    var posts=featuredPosts();
    // forEach registers dependencies on the cursor content exactly like #each does
    posts.forEach(function(post){...});
    // finally we need to reinit the carousel so it take into account our newly added
    // HTML elements as carousel items, but we can only do that AFTER the #each block
    // has finished inserting in the DOM, this is why we have to use Deps.afterFlush
    // the #each block itself setup another computation and Deps.afterFlush makes sure
    // this computation is done before executing our callback
    Tracker.afterFlush(_.bind(function(){
      this.$(".owl-carousel").data("owlCarousel").reinit(options);
    },this));
  },this));
};

I'm not familiar with owl-carousel so I'm not sure if reinit will act properly (I've quickly checked the documentation and is seems OK though).

For similar JS plugins where a reinit method is not available (bootstrap carousel for example), you can first check if the plugin has been instantiated then destroy it and recreate it like this :

var carousel=this.$(".carousel").data("bs.carousel");
if(carousel){
  // bootstrap carousel has no destroy either so we simulate it by pausing it
  // and resetting the data attribute to null (it's a bit messy)
  this.$(".carousel").carousel("pause");
  this.$(".carousel").data("bs.carousel",null);
}
// initialize normally because previous instance was killed
this.$(".carousel").carousel({...});
Hylotheism answered 17/8, 2014 at 22:54 Comment(5)
Thank you very much for your answer, it really has helped me a lot. I haven't set my hook exactly like you suggested, but I don't really understand your way. Is this a general router setting applying to all routes or am I supposed to add this as an option inside of the Router.map part for that specific route?Livery
"loading" is a special default hook provided by the iron-router package, you can look at its definition here : github.com/EventedMind/iron-router/blob/devel/lib/client/… It is not activated by default so you need to add it to onBeforeAction hooks used by your Router as a global router setting. You may also use the exclude syntax to deactivate it on certain routes. I was unable to find these explanations on current iron-router DOCS.md, this used to be clearer.Hylotheism
with {{! item declaration}} you mean some div,h1 etc?Materialist
Yes, just put here any tags that compose your carousel items.Hylotheism
right know im having really big issues seems like the helpers its not workingMaterialist
A
0

Its better we call the helper handler from the template exactly after the #each ends other than the onRender.

As soon as the loop ends and the DOM loads for the same the handler will call the function.

Like in your case After each place the handler as {{initializeCarousel}}.

{{#each featuredPosts}} {{! item declaration}} {{/each}} {{initializeCarousel}}

Now define this in the helper as:

Template.carousel.helpers({ // feed the #each with our cursor
    initializeCarousel: function(){
        $('#featured-carousel').owlCarousel({
            loop:true, autoplay:true, autoplayTimeout:3000, items:1, smartSpeed:1080, padding:80 
        });
    });

This will make the carousel to load once the data is loaded. Hope this will help.

Absalom answered 27/1, 2016 at 12:42 Comment(1)
I read in the docs that doing "setup" or "data manipulations" like this in helpers is not recommended. I think because helpers can get called many times over in a row which can lead to unwanted side effects.Thallium

© 2022 - 2024 — McMap. All rights reserved.