How to execute a callback after an #each is done?

2020-04-05 09:09发布

问题:

I'm having trouble with a callback after the #each has finished. I have a template named "content":

<template name="content">
{{#if Template.subscriptionsReady}}
    {{#each currentData}}
        <div data-cid="{{this._id}}"></div>
    {{/each}}
{{else}}
    Loading...
{{/if}}
</template>

At first I wait for a subscription, when this is available, I iterate through my Collection with {{#each}} and append the div. What I need is a sort of callback for when the for-each loop is done (in other words DOM ready).

Template.content.onRendered()

-> triggers to early

I also tried appending an image after the {{each}} and fire a function in its onload like this:

<img style="height:0;width:0" src="*mysource*" onload="callback()">

-> did work sometimes but not reliable somehow

Is there a way to get this callback? I do not fear to change the structure of this template, if that brings the solution.

回答1:

I had a similar problem and after a lot of searching found the following solution. I tried using Tracker, onRendered and other tricks, none of them worked. This could be considered more of a hack, but works. Unfortunately can't remember where I found this solution initially.

Start with your template, but add an template tag after your each.

<template name="content">
{{#if Template.subscriptionsReady}}
    {{#each currentData}}
        <div data-cid="{{this._id}}"></div>
    {{/each}}
    {{doneTrigger}}
{{else}}
    Loading...
{{/if}}
</template>

Then define a helper that returns null.

Template.content.helpers({
  doneTrigger: function() {
    Meteor.defer(function() {
      // do what you need to do
    });
    return null;
  }
});

You can read more about Meteor.defer() here, but it is equivalent to using a 0 millisecond setTimeout.



回答2:

There's no easy way to get notified when a Spacebars {{#each}} block has done rendering into the DOM every item getting iterated over.

The best solution is to use another reactive computation (Tracker.autorun) to observe your (reactive) current data.

Everytime your current data (which is likely a cursor) is modified, you can run arbitrary code after every other reactive computations are done performing whatever their job is, using Tracker.afterFlush.

The {{#each}} block is one of those computations, whose role is to listen to the reactive data source you give it as argument and rerender its Template.contentBlock as many times as items fetched from the source being iterated over, with the current item as current data context.

By listening to the exact same reactive data source as the {{#each}} block helper and running your code AFTER it has finished its own reactive computation, you can get the actual requested behavior without relying on some weird tricks.

Here is the full implementation of this pattern :

JS

Template.content.helpers({
  currentData: function(){
    return Template.currentData();
  }
});

Template.content.onRendered(function(){
  this.autorun(function(){
    var cursor = Template.currentData();
    // we need to register a dependency on the number of documents returned by the
    // cursor to actually make this computation rerun everytime the count is altered
    var count = cursor.count();
    //
    Tracker.afterFlush(function(){
      // assert that every items have been rendered
      console.log(this.$("[data-cid]") == count);
    }.bind(this));
  }.bind(this));
});


回答3:

You can also use sub-templates and count the number of sub-templates rendered. If this number is the number of items in the collection, then all are rendered.

<template name="content">
{{#if Template.subscriptionsReady}}
    {{#each currentData}}
        {{> showData}}
    {{/each}}
{{else}}
    Loading...
{{/if}}
</template>

<template name="currentData">
  <div data-cid="{{this._id}}"></div>
</template>

With that, initialize a reactive variable and track it:

var renderedCount = new ReactiveVar(0);

Tracker.autorun(function checkIfAllRendered() {
  if(renderedCount.get() === currentData.count() && renderedCount.get() !== 0) {
    //allDataRendered();
  }
});

When the currentData template is rendered, increment it, and decrement it when it is destroyed.

Template.currentData.onRendered(function() {
  renderedCount.set(++renderedCount.curValue);
});

Template.currentData.onDestroyed(function() {
  renderedCount.set(--renderedCount.curValue);
});


回答4:

A possibly simpler approach to consider - create a template around your #each block and then get an onRendered event afterwards:

html:

<template name="content">
{{#if Template.subscriptionsReady}}
    {{> eachBlock}}
{{else}}
    Loading...
{{/if}}
</template>

<template name="eachBlock">
{{#each currentData}}
    <div data-cid="{{this._id}}"></div>
{{/each}}
</template>

js:

Template.eachBlock.onRendered(function(){
  console.log("My each block should now be fully rendered!");
});