Meteor #each loop ready - javascript

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);
});
});

Related

Show certain elements when using common template based on url in meteor

I am using on a common template in two pages. I have to show some lines in one page. So i have added an if function which will check the current url . It is working for me only once. After coming back to the same url i am getting previous url's value. Then this it is not changing . Below is a sample code. How to achieve this?
Below is my code
Url- /template1
Template1.html
<template name="template1">
{{>commontemplate}}
</template>
Url - /template2
Tempalate2.html
<template name="tempalte2">
{{>commonTemplate}}
</template>
CommonTemplate.html
<template name="commonTemplate">
{{#if isAdmin 'template1'}}
<div> hello</div>
{{else}}
<div> hai</div>
{{/if}}
</template>
CommonTemplate.js
Template.CommonTemplate.helpers({
isAdmin: function (val) {
var path = window.location.pathname;
var str = path.split("/");
return val === str[1];
}
})
The reason it's not rerunning is that your helper function has no reactive dependencies. Normal Javascript variables (like window.location.pathname) don't instruct reactive computations (like helper functions) to rerun when their values change.
The two easiest possibilities are:
Name your routes and use FlowRouter.getRouteName() in the helper function, which is reactive.
Add the line FlowRouter.watchPathChange() to your helper, which doesn't return anything, but does ensure the containing reactive function reruns whenever the path changes. FlowRouter API.
The other alternative is to simply use CSS. Have a look at meteor-london:body-class, which appends the route name as a class to the body, allowing you to selectively show or hide stuff based on route in your CSS.

Why is the document returning null even though I already subscribed to the collection (Meteor)?

I have a Books and Chapters collection. Self-explanatory: A book can have many chapters.
subscriptions.js:
Meteor.publish("singleChapter", function(id) {
return Chapters.find(id);
});
book_page.js:
Template.bookPage.helpers({
chapters: function() {
Chapters.find({
bookId: this._id
}, {
sort: {
position: 1
}
});
}
});
book_page.html:
<template name="bookPage">
<div class="chapter-list hidden">
<div class="chapter-items">
{{#each chapters}}
{{> chapterItem}}
{{/each}}
</div>
</div>
</template>
chapter_item.html:
<template name="chapterItem">
<div class="chapter clearfix">
<div class="chapter-arrows">
<a class="delete-current-chapter" href="javascript:;">X</a>
</div>
</div>
</template>
Right now, I'm trying to get the current chapter item in chapter_item.js:
Meteor.subscribe("singleChapter", this._id); // even tried this but didn't work
Template.chapterItem.events({
"click .delete-current-chapter": function(e) {
e.preventDefault();
var currentChapter = Chapters.find(this._id);
}
});
But when I do
console.log(currentChapter);
I get undefined. What am I doing wrong?
TL/DR - skip to 3 as it's probably most relevant, but I've included the rest for completeness.
I assume you're putting the console.log... line in the "click .delete-current-chapter" callback? The currentChapter variable is going to be local to that function, so you won't get anything by entering that in the console. Apologies if that's obvious, but it's not clear that you're not doing this from the question.
Even in the callback, currentChapter is going to be a cursor, not a document or array of documents. Use findOne to return a single doc (or null), or find(query).fetch() to return an array (which in this case should probably be just one doc).
Where and when are you trying to subscribe to singleChapter? If it's in the callback, you have to bear in mind that this isn't a reactive function. This means that you'll subscribe (once you know the _id to which to subscribe), but immediately return currentChapter before the collection is actually ready (and thus doesn't have anything in it). In this case, the callback won't rerun once the collection is ready as event handlers aren't reactive.
The easiest way to resolve this would be to use the onReady callback when you subscribe, and set currentChapter in there. The alternative would be a self-stopping Tracker.autorun in the event handler, but this seems like overkill.
As a final point, you need to be a bit careful about subscriptions with this sort of setup, as you can easily accumulate dozens and dozens of them per client, with none of the automatic subscription stopping that Iron Router provides. Given this use case, it's probably preferable to stop the subscription as soon as your callback has run and the item in question has been deleted.
Is your publish function working? Perhaps Mongo has a feature that I'm not aware of, but I'd expect you need to include {_id:id}, not just (id).
Meteor.publish('singleChapter', function(id){
return Chapters.find({_id: id});
});

Can't filter array returned from firebase in angular

This is the html:
<div ng-controller="FilterController as ctrl">
<div>
All entries:
<span ng-repeat="entry in ctrl.array">{{entry.from}} </span>
</div>
<div>
Entries that contain an "a":
<span ng-repeat="entry in ctrl.filteredArray">{{entry.from}} </span>
</div>
</div>
This is my script:
angular.module('myApp', ["firebase"]).
controller('FilterController', ['filterFilter', '$firebase', function(filterFilter, $firebase) {
var ref = new Firebase("https://******.firebaseio.com/");
this.array = $firebase(ref).$asArray();
this.filteredArray = filterFilter(this.array, 'a');
}]);
The result from filteredArray is just empty. What have I done wrong? Many thanks for any insight.
The data loads in an asynchronous manner. This is JavaScript 101 and you should probably spend a little time on asynchronous ops before you try to tackle Angular and Firebase.
Your filter call happens probably even before the request is sent to the server, thus the array is still empty.
You can use the $loaded method to wait for the initial data to be downloaded. However, your filtered results will still be static, circumventing the real-time capabilities.
this.array.$loaded(function() {
console.log(this.array.length); // current length at time of loading
});
console.log(this.array.length); // 0, hasn't loaded yet
Generally, you should not be doing things like this at the code level, but instead letting Angular and Firebase work their magic; move it into the view:
<li ng-repeat="item in array | filter:searchText" />
If you find yourself trying to manipulate arrays in your controller to transform data, check out $extendFactory instead. But mostly, just leave it alone and bind it to scope, then let Angular take care of the rest.

Meteor.js Template does not update after variable assignment

I'm building a free TV-tracking app with Meteor, and along the way I seem to have hit a wall. A part of my template contains this:
<template name="results">
<div class="row">
<div class="span6 offset6">
<h4>Search Results</h4>
<ul>
{{#each series}}
<li><a href="http://thetvdb.com/?tab=episode&seriesid{{tvdbseriesid}}&lid={{tvdblid}}">{{seriesname}}</li>
{{/each}}
</ul>
</div>
</div>
</template>
Then, within my client js code, I do this:
Template.search.events({
'click #search' : function(evt) {
evt.preventDefault();
var query = $('#query').val();
if (query) {
Meteor.call('queryshow', query, function(error, result) {
Template.results.series = result;
console.log(Template.results());
});
}
}
});
The "queryshow" server method is just a Collection query method that returns an object containing the data that my template needs.
The problem is this however: the change isn't reflected in the browser window. I can't seem to figure out why, because the console.log(Template.results()) call shown below returns the correct html that I am expecting.
How do I fix this? I tried looking around Meteor's docs and I can't seem to find a function that forcibly re-renders a Template. Any help will be much appreciated!
Template.results.series should be a function that returns the series, rather than set directly to the series you want to use. That way, when Meteor runs the function to get the series, it can register what the function depends on, and then re-run the function whenever any of the dependencies change.
The easiest way to register a dependency for your information is to use the Session object. So your code would look something like:
Template.results.series = function () { return Session.get('resultsSeries'); }
Meteor.call('queryshow', query, function (err, res) {
// handle the error if it's there
// ...
// Then set the session variable, which will re-run anything that depends on it.
Session.set('resultsSeries', res)
}

The context of "this" in Meteor template event handlers (using Handlebars for templating)

A quick question on the context of the event handlers for templates in Meteor (with Handlebars).
In the section of Documentation on template instances (http://docs.meteor.com/#template_inst) it is mentioned that "Template instance objects are found as the value of this in the created, rendered, and destroyed template callbacks and as an argument to event handlers"
In the Templates section (http://docs.meteor.com/#templates) it says "Finally, you can use an events declaration on a template function to set up a table of event handlers. The format is documented at Event Maps. The this argument to the event handler will be the data context of the element that triggered the event."
Well, this is only partially true. Let's use an example from the docs:
<template name="scores">
{{#each player}}
{{> playerScore}}
{{/each}}
</template>
<template name="playerScore">
<div>{{name}}: {{score}}
<span class="givePoints">Give points</span>
</div>
</template
Template.playerScore.events({
'click .givePoints': function () {
Users.update({_id: this._id}, {$inc: {score: 2}});
});
Here the "this" context of the 'click .givePoints' event handler is indeed the template instance of playerScore. Let's modify the html:
<template name="scores">
<span class="click-me">Y U NO click me?<span>
{{#each player}}
{{> playerScore}}
{{/each}}
</template>
<template name="playerScore">
<div>{{name}}: {{score}}
<span class="givePoints">Give points</span>
</div>
</template>
... and add an event handler for .click-me on the scores template:
Template.scores.events({
'click .click-me': function () {
console.log(this);
}
});
Now, if you click the span, what do you get logged? The Window object! What did I expect to get? The template object! Or maybe the data context, but it's neither. However, inside the callbacks (e.g. Template.scores.rendered = function(){ ... }) the context of "this" is always the template instance.
I guess my real question would be: is this something to do with
a bug in Handlebars, Meteor or somewhere in between?
slightly incomplete documentation on the templates?
me completely misinterpreting the docs or not understanding something fundamental
about Meteor or Handlebars?
Thanks!
This video explains the concepts:
http://www.eventedmind.com/posts/meteor-spark-data-annotation-and-data-contexts.
The direct answer to your question:
The thisArg inside an event handler should point to a data context. But sometimes the data context is undefined. When you use the Function.prototype.call(thisArg, ...) in JavaScript, if the thisArg is undefined (e.g. a dataContext is undefined) the browser will set this equal to window. So, the docs aren't wrong per se but the event handling code isn't guarding against the possibility of a data context being undefined. I'm guessing that will be fixed in short order.
So, what produces a data context for a template? Normally your root template won't even have a data context. In other words, the Template function is called without an object. But if you use the {{#with block helper or the {{#each iterator, a data context will be created for each item in the list, or in the case of the with helper, the object.
Example:
var context = {};
<template name="withHelper">
{{#with context}}
// data context is the context object
{{/with}}
</template>
var list = [ {name: "one"}, {name: "two"} ];
<template name="list">
{{#each list}}
{{ > listItem }} // data context set to the list item object
{{/each}}
</template>
The first parameter in the function is the event. So you could use the target of the event to grab your element.
Template.scores.events({
'click .click-me': function (event, template) {
console.log(event.target);
$(event.target).text("O but I did!");
}
});

Categories

Resources