Bug: onRendered executes too early

All I wanted to do was scroll to a location on page load:

Template.myTemplate.onRendered(function () {
    $('html, body').animate({ scrollTop: $('#r7yafNuDd8g2wLceW').offset().top }, "500")
});

But $('#r7yafNuDd8g2wLceW').offset() was undefined most of the time.

What worked was wrapping the code using Meteor.setInterval() so that the code is regularly retried until the element is found:

Template.myTemplate.onRendered(function () {
    var attemptCount = 0
    var intervalId = Meteor.setInterval(function () {
        attemptCount++
        if ($('#r7yafNuDd8g2wLceW').length > 0) { //found element in DOM
            $('html, body').animate({ scrollTop: $('#r7yafNuDd8g2wLceW').offset().top }, "500")
            Meteor.clearInterval(intervalId)
            console.log(attemptCount) //to see how many attempts were made until successful
        }
    }, 100)
});

It would take 1, 2 or 3 attempts for the element to be eventually found.

After reading some other forum threads it appears that other people have worked around the problem using Meteor.setTimeout(), which is similar but there’s no retry.

This alternative code also seems to fix the problem:

Template.myTemplate.onRendered(function () {
    this.autorun(() => {
        if (this.subscriptionsReady()) {
            Tracker.afterFlush(() => {
                $('html, body').animate({ scrollTop: $('#r7yafNuDd8g2wLceW').offset().top }, "500")
            })
        }
    });

All of the above workarounds are ugly. The problem should be fixed so that all DOM elements are definitely available as assumed or expected when onRendered executes.

5 Likes

What is the proper way to know that all HTML of a page has been fully loaded (since onRendered does not reliably tell you without writing all the extra code)?

Your last code snippet is the proper way to do it. If you ever have an {{#each}} block that’s waiting for a subscription, then the DOM will be ready before your data is loaded in that block (so onRendered works as intended).

The usual pattern to avoid the Tracker.afterFlush “workaround” is to refactor whatever is inside your {{#each}} block into a separate template and use that template’s onRendered function instead.

9 Likes

Since “Rendered” is past tense, we should expect that the page has been completely loaded (all DOM elements have been rendered) once code in onRendered begins execution. My experience above shows that a page is not completely loaded when onRendered starts to execute. Therefore either this is a bug and should be fixed or “onRendered” should be renamed to “onRendering” or “onAlmostRendered” in order to avoid confusion.

All that code with autorun and this.subscriptionsReady() makes sense to have in onCreated but not in onRendered.

2 Likes

Data source in Meteor is reactive, i.e. async. So if you full page rendering depends on data, it could and usually happens after onRendered. Usual practice is to check subscriptonsReady in template instead of in code to avoid the confusion:

<template name="abc">
    {{#if Template.subscriptionsReady}}
     ... do what you need
    {{else}}
     ... show loading message ..
    {{/if}}
</template>

See here: https://www.discovermeteor.com/blog/template-level-subscriptions/

I understand your frustration. I think everyone goes through a little bit of this when they start with Meteor. This is a side-effect of reactive data. You have to stop thinking like pages are rendered server-side and sent to the client. Your template will not have initial data when it is rendered. Therefore, any DOM elements that are scoped under an {{#each}} block (which depends on data) will not exist. It is not a bug, I assure you.

In the old days, we used iron:router's waitOn feature to wait for initial data before rendering the page (which turns out isn’t great for performance at all), and onRendered would work as you are describing.

2 Likes

I think the terminology here is OK because a template can be rendered with data or without data and still be considered rendered.

Having said that I do think there should be something closer to first class support for determining when the current data has been fully rendered into the DOM. Combining subscriptionsReady with afterFlush is presumably the most correct way to do this but that is a lot of boilerplate for what might be the most commonly desired hook point.

1 Like

Technically, to be 100% sure that you call your callback at the right time, you typically want your callback to be

Subscription Ready -> After Flush -> Request Animation Frame -> callback.

This is because while offset() will force that element to calculate it’s layout, it doesn’t force the entire dom to finish pending layout calculations which may or may not be relevant to your scroll position. Request Animation Frame will cause a callback to fire after layout calculations are done but before paint is fired, giving you a chance to do your scroll then.

But why should that require a bunch of “boilerplate?” One helper function automagically handles this for all use cases:

function whenLayoutReady(subs, callback) {
    //ducktyping to force to an array
    if (!subs.length) {
        subs = [subs];
    }
    //autorun to detect when all subs are ready
    Tracker.autorun((c) => {
        if (_.all(subs, sub => sub.ready()) {
            c.stop();
            Tracker.afterFlush(() => {
                window.requestAnimationFrame(callback);
            });
        }
    });
}
4 Likes

Any help on this? Does onRendered executes only after the template is fully loaded? We have lots of dropdown in a page that uses materialize and lots of data too to be loaded from collections, the materialize feature disappeared without any error.

Hi, I’m wondering on where to put this codes in some js files I have on my meteor app, can you guide me please?

Thanks!

I didn’t go through the whole post, but we use _.defer or a timeout inside onRendered to give DOM engine the time to properly create all elements.

2 Likes

jQuery is very often a pain in the ass when using Blaze and reactivity. Because Blaze takes the next render event to update the UI, you need to take the one after, or the element that you want to hook into with jQuery isn’t available yet.

My solution, just hook any jquery init into the next render event. Never fails.

Meteor.setTimeout ->
  # Your jquery stuff in here
, 0

I have the same problem too.
Could you full example @ramez, @satya ?

This is actually due to the DOM sending ready event when the elements have not fully renderd. We also successfully used _.render – not just timeout

Please detail

Template.myTemplate.onRendered(function () {
   // How to use _.render???    
});
Template.myTemplate.onRendered(function () {
   var self = this;
   _.defer(function() {
      $("#" + self.data.id).val(self.data.value);
   });
});

I tried _.defer but still not working. We have lots of materialize dropdown in the page (because the form fields are extandable). :frowning:

Try adding a timeout with an extreme delay (say 5000) to see if that is the problem. I don’t know materialize but it’s possible that there is delay in its own rendering of the DOM. In which case, you are not waiting for the template’s onRendered, you are waiting for another library to process the DOM for you.

We use Semantic-UI and it has almost always processed in reasonable time.

This is really a bad idea.

Instead, an autorun should exist which monitors for the data subscriptions to be ready.

Template.myTemplate.onRendered(function () {
   var self = this;
   this.autorun((c) => {
     if (this.criticalSub.ready()) {
       //do key thing now with dom
       c.stop();
     }
   });
});

We usually subscribe in onCreated, onRendered relates to actual DOM rendering.