I wonder is there any way to know if #each loop is "ready". By "ready" I mean it rendered all nodes and inserted into the DOM. I don't even speak about onRendered
callback (old rendered
). I tried
<template name="myTemplate">
<div class="some-class">
{{#if Template.subscriptionsReady}}
{{#each messages}}
<div>{{text}}</div>
{{/each}}
<script>
$(".some-class").trigger("LOOP_READY")
</script>
{{/if}}
</div>
</template>
Template.myTemplate.onRendered(function(){
this.$(".some-class").on("LOOP_READY", doSomething)
})
But it does not work either. I don't want to use timers.
UPDATE
messages
is a collection of text messages for current dialog, dialogId
is a reactive var stored in the Session
.
Template.myTemplate.onCreated(function(){
var self = this;
self.autorun(function(){
self.subscribe("messages", Session.get("dialogId"))
})
})
So if someone changes dialogId, myTemplate loads another dialog messages, and I want to know when this messages are ready, to scroll to a specific message. Simple onReady
does not work, since I can not call scrollTop accurately before all messages are rendered and got its own height.
There's no easy way to get notified when a Spacebars {{#each}}
block has done rendering into the DOM every item its spanning.
The best solution is to use another reactive computation (Tracker.autorun
) to observe your messages cursor changes.
Everytime your messages list 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 setTimeout
tricks.
Here is the full implementation of this pattern :
HTML
<template name="myTemplate">
<div class="some-class">
{{#if Template.subscriptionsReady}}
{{#each messages}}
<div class="message">{{text}}</div>
{{/each}}
{{/if}}
</div>
</template>
JS
// declare your reactive data source once to reuse the same in multiple places
function messagesCursor(){
return Messages.find();
}
Template.myTemplate.helpers({
messages: messagesCursor
});
Template.myTemplate.onRendered(function(){
this.autorun(function(){
// 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 messagesCount = messagesCursor().count();
//
Tracker.afterFlush(function(){
// assert that every messages have been rendered
console.log(this.$(".messages") == messagesCount);
}.bind(this));
}.bind(this));
});
@saimeunt provided elegant solution, but I implemented it other way, maybe someone find it helpful too.
Since I don't want my code been executed everytime a new message arrives, I can not trigger scrolling after autorun is finished. (You would say, OK, do it in autorun that depends only on dialogId
. - then I can not get exact message count of this dialog since subscription should take some time)
HTML
<template name="myTemplate">
<div class="chat">
{{#if Template.subscriptionsReady}}
<div class="messages">
{{#each messages}}
<div class="message">{{text}}</div>
{{/each}}
</div>
<script>$(".chat").trigger("MESSAGES_READY_EVENT")</script>
{{/if}}
</div>
</template>
JS
Template.chat.onCreated(function () {
var self = this;
self.messages = function() {
return Messages.find({dialogId:Session.get("dialogId")})
}
self.autorun(function(){
self.subscribe("messages", Session.get("dialogId"));
})
});
//helpers skipped
Template.chat.onRendered(function () {
var self = this;
self.$(".chat").on("MESSAGES_READY_EVENT", function () {
var computationNumber = 0;
var $messages = self.$(".messages");
//how many massages we have to render
var total = self.messages().count();
//max tries
var maxTries = 10;
var intervalId = Meteor.setInterval(function () {
var totalNodes = $messages.children().length;
if (computationNumber++ >= maxTries || total <= totalNodes) {
Meteor.clearInterval(intervalId);
$messages.scrollTop(someValue);
}
}, 100);
});
});