Parent template's onRendered called before child's


#1

Hey @sashko, I found an issue with Blaze and I wondered how to go about it.

The short version is that Blaze.View.prototype.onViewReady uses Tracker.afterFlush to queue the rendered callbacks. This opens up the possibility of a parent’s rendered callback to run before a child’s one, even though the child already rendered.

This creates issues if you’re processing elements on the onRendered functions. The typical example is “locate all unprocessed elements and do something with them, then flag them as processed”. If onViewReady uses Tracker.afterFlush then a parent template can process an element before a child template can do it.

Here’s a repro: https://github.com/ManuelDeLeon/blaze-rendering-issue

The hierarchy is:

body 
  -> editContact 
    -> card
    -> editContactForm

editContact only displays card if editContactForm has rendered.

When you run it the output is:

editContact.onCreated
editContactForm.onCreated
editContactForm.onRendered
card.onCreated
editContact.onRendered
============= editContact rendered with a card! =============
card.onRendered

In other words, card renders on the page but card.onRendered is called after editContact.onRendered (the parent).

The problem is that Tracker.afterFlush sets functions to run in the next cycle, but it allows other computations to be injected into the current queue while the queue is being processed .

The problem is fixed if you use a setTimeout instead of Tracker.afterFlush. The onRendered functions are still set to run on the next cycle but they don’t allow other functions to be injected in the queue before they’re processed.

So instead of:

Blaze.View.prototype.onViewReady = function (cb) {
  var self = this;
  var fire = function () {
    Tracker.afterFlush(function () {
      if (! self.isDestroyed) {
        Blaze._withCurrentView(self, function () {
          cb.call(self);
        });
      }
    });
  };
  self._onViewRendered(function onViewRendered() {
    if (self.isDestroyed)
      return;
    if (! self._domrange.attached)
      self._domrange.onAttached(fire);
    else
      fire();
  });
};

You would have:

Blaze.View.prototype.onViewReady = function (cb) {
  var self = this;
  var fire = function () {
    setTimeout(function () {
      if (! self.isDestroyed) {
        Blaze._withCurrentView(self, function () {
          cb.call(self);
        });
      }
    }, 0);
  };
  self._onViewRendered(function onViewRendered() {
    if (self.isDestroyed)
      return;
    if (! self._domrange.attached)
      self._domrange.onAttached(fire);
    else
      fire();
  });
};

(the only change is that Tracker.afterFlush becomes setTimeout)

What do you think?


Order of onRendered execution in parent-children templates
#2

btw, the onCreated and onDestroyed could get the same treatment.


#3

Good find. This explains some weirdness I’ve seen around onRendered()


#4

I’ve had a similar issue where onRendered gets called multiple times (for no reason – data only comes in once). Added a setTimeout to onRendered fixed this issue and made code only get called once.


#5

Hey @manuel I’m having similar issues with parent-child onRendered which I makes me crazy. I ad an message (child) to my message list (parent) and the parent onRendered is called multiple times.

I’m not clear how to “solve” this with the workarond you proposed. Am I supposed to put a setTimeout in my parent template? If so, could you elaborate a little more on this?

Did @sashko provide any hints/answers?


#6

@manuel: Is there a ticket with bug report for this?

I opened this one some time ago: https://github.com/meteor/meteor/issues/4166 But it is for a different thing, the order of onDestroyed callbacks. I have not observed the issue you are mentioning. Can you explain why in your case you have this strange order, but example here does not?


#7

@massimosgrelli

I solved it by monkey patching Meteor. I put the following js on a file in client/lib

Blaze.View.prototype.onViewReady = function (cb) {
  var self = this;
  var fire = function () {
    setTimeout(function () {
      if (! self.isDestroyed) {
        Blaze._withCurrentView(self, function () {
          cb.call(self);
        });
      }
    }, 0);
  };
  self._onViewRendered(function onViewRendered() {
    if (self.isDestroyed)
      return;
    if (! self._domrange.attached)
      self._domrange.onAttached(fire);
    else
      fire();
  });
};

Did @sashko provide any hints/answers?

Not this time.

@mitar

Is there a ticket with bug report for this?

Yes but it was closed with “that’s not our problem”.

Can you explain why in your case you have this strange order, but example here does not?

Blaze calls onRendered in a predictable way almost all the time. However there’s the case when you can mess up the order which Blaze calls the onRendered:

<body>

{{> parent }}

</body>

<template name="parent">
  {{#if showChild1}}
    {{> child1 showChild1}}
  {{/if }}

  {{> child2 }}
</template>

<template name="child1">
  <div id="child1">

  </div>
</template>

<template name="child2">

</template>
if (Meteor.isClient) {

  Template.parent.helpers({
    showChild1: function () {
      return Session.get("child2-onRendered-called");
    }
  });

  Template.parent.onRendered(function () {
    console.log("parent - onRendered");
    if (this.$("#child1").length > 0) {
      console.log("parent's onRendered called before child1's onRendered (even though child1 is already on the page)");
    }
  });

  Template.child1.onRendered(function () {
    console.log("child1 - onRendered");
  });


  Session.set("child2-onRendered-called", false);
  Template.child2.onRendered(function () {
    console.log("child2 - onRendered");
    Session.set("child2-onRendered-called", true)
  });

}

In this case it starts on the right foot (child2 onRendered called before the parent) but then child2 says “I also need my brother child1”. Blaze jumps child1 in front of the queue and puts it on the page. At this point one would think that child1’s onRendered would be called (because it’s on the page) but Blaze calls parent’s onRendered instead, then child1’s onRendered.

I explained it and got: “That’s not our problem because each individual template’s onRendered is called at some point after it’s on the page.” to which I said please reconsider, this causes problems when you’re processing elements on the page. To that I got: “We understood you very well the first time. You were the one who didn’t understand us when we said that’s your problem, not ours.”

I gave up.


#8

In a universe where there are architecture changes required to make onRendered happen in a certain order, it might actually be more effective to work around it on the application level.


#9

The thing is that I didn’t get “Yeah, it’s a problem but your solution isn’t viable because X/Y/Z and coming up with a proper solution would take too much time.” Instead I got “There’s no problem here.”

Which is fine (I have to accept whatever decision is made) but there’s a difference between the two.


#10

I am linking it here for the documentation sake: https://github.com/meteor/meteor/issues/4410


#11

@sashko: I see that in https://github.com/meteor/meteor/issues/4410 ticket it was not the problem of any “architecture changes required”, but that @glasser simply requested a better reproduction. I have just made one so I would request that ticket is reopened and reevaluated.

I personally think that it is really a bug and problematic behavior. But maybe that ticket is a better place to discuss it further.


#12

Ticket has been reopened. :slight_smile:


#13

Now I have the same problem for config jquery on template rendered.

Template.updateTpl.onRendered(function () {
    $............ // init jquery
});

But don’t work, and then I tried open the browser console and type the config init jquery, it work fine.
Please example how to use

Blaze.View.prototype.onViewReady = function (cb) {
  var self = this;
.....................................