Meteor on finish renderlist - meteor

Is there a way to get a callback for when an entire list renders?
I've tried
Template.articles.rendered = function() {
var lastChapter = Chapters.findOne({}, {
sort: {
createdTime: -1
}
})
if (lastChapter._id != this.data._id)
return
doSomething()
};
But this is unreliable because chapters are added 1 by 1 instead of all at once, so this actually fires multiple times.
Thanks.

rendered is called when a part of the template is re-rendered, so you should check inside your rendered method whether you want to do anything now. When does "the entire list renders" happen? You know that in your code, for instance by checking if the list is of an expected length yet.

Related

Returned value from Meteor Helper not showing up in template

I have a Meteor Helper that does a GET request and am supposed to get response back and pass it back to the Template, but its now showing up the front end. When I log it to console, it shows the value corerctly, for the life of mine I can't get this to output to the actual template.
Here is my helper:
UI.registerHelper('getDistance', function(formatted_address) {
HTTP.call( 'GET', 'https://maps.googleapis.com/maps/api/distancematrix/json? units=imperial&origins=Washington,DC&destinations='+formatted_address+'&key=MYKEY', {}, function( error, response ) {
if ( error ) {
console.log( error );
} else {
var distanceMiles = response.data.rows[0].elements[0].distance.text;
console.log(response.data.rows[0].elements[0].distance.text);
return distanceMiles;
}
});
});
In my template I pass have the following:
{{getDistance formatted_address}}
Again, this works fine and shows exactly what I need in the console, but not in the template.
Any ideas what I'm doing wrong?
I posted an article on TMC recently that you may find useful for such a pattern. In that article the problem involves executing an expensive function for each item in a list. As others have pointed out, doing asynchronous calls in a helper is not good practice.
In your case, make a local collection called Distances. If you wish, you can use your document _id to align it with your collection.
const Distances = new Mongo.collection(); // only declare this on the client
Then setup a function that either lazily computes the distance or returns it immediately if it's already been computed:
function lazyDistance(formatted_address){
let doc = Distances.findOne({ formatted_address: formatted_address });
if ( doc ){
return doc.distanceMiles;
} else {
let url = 'https://maps.googleapis.com/maps/api/distancematrix/json';
url += '?units=imperial&origins=Washington,DC&key=MYKEY&destinations=';
url += formatted_address;
HTTP.call('GET',url,{},(error,response )=>{
if ( error ) {
console.log( error );
} else {
Distances.insert({
formatted_address: formatted_address,
distanceMiles: response.data.rows[0].elements[0].distance.text
});
}
});
}
});
Now you can have a helper that just returns a cached value from that local collection:
UI.registerHelper('getDistance',formatted_address=>{
return lazyDistance(formatted_address);
});
You could also do this based on an _id instead of an address string of course. There's a tacit assumption above that formatted_address is unique.
It's Meteor's reactivity that really makes this work. The first time the helper is called the distance will be null but as it gets computed asynchronously the helper will automagically update the value.
best practice is not to do an async call in a helper. think of the #each and the helper as a way for the view to simply show the results of a prior calculation, not to get started on doing the calculation. remember that a helper might be called multiple times for a single item.
instead, in the onCreated() of your template, start the work of getting the data you need and doing your calculations. store those results in a reactive var, or reactive array. then your helper should do nothing more than look up the previously calculated results. further, should that helper be called more times than you expect, you don't have to worry about all those additional async calls being made.
The result does not show up because HTTP.call is an async function.
Use a reactiveVar in your case.
Depending on how is the formated_address param updated you can trigger the getDistance with a tracker autorun.
Regs
Yann

Get reactive value from subscription in meteor

In Meteor, how do I properly return a value from a property (in this case response) from a collection so that I can send it to a data propert? I have tried the following function:
Responses: function(answer) {
return Responses.findOne({answerId: answer.hash.answer});
}
Which I call in the spacebars template as:
data-selected="{{Responses answer=_id}}
When I look at the HTML it says that the data-selected="[object object]" which is not super surprising but I can't figure out how to return just the response value from the match. I can add .response onto the end and it works but gives an expected 'undefined' error I know that it is not reactive. I have seen people use wrapasync but that was for methods, not subscriptions. I should note that there are several responses, so the code would have to be suitable inside a #for loop of answers which the responses variable is keeping track of the input from each user separately.
Thanks!
I'm not sure about data-selected="{{Responses answer=_id}}". Since you're already inside a for loop your html code can simply look like this:
data-selected="{{Responses}}"
Inside your helper function you can then say:
var myResponse = Responses.findOne({answerId: this._id}).response;
if (myResponse != null){
return myResponse;
else {
return "";
}
When the page is still loading and the data is not yet available, myResponse is still null, so the helper returns the empty string "". Shortly thereafter the data becomes available and your data gets returned, without any error in your console.

Facebook like load new posts in meteor

I'm in the process of learning meteor. I followed the tutorial to create microscope. If some one submits a post meteor will re render the template for all users. This could be very annoying if there are hundreds of posts then the user will come back to the top of the page and loose track of where he was. I want to implement something similar to what facebook has. When a new post is submitted template isn't rendered rather, a button or link will appear. Clicking it will cause the template to re-render and show the new posts.
I was thinking of using observeChanges on the collection to detect any changes and it does stop the page from showing new posts but only way to show them is to reload the page.
Meteor.publish('posts', function(options) {
var self = this, postHandle = null;
var initializing = true;
postHandle = Posts.find({}, options).observeChanges({
added: function(id, post) {
if (initializing){
self.added('posts', id, post);
}
},
changed: function(id, fields) {
self.changed('posts', id, fields);
}
});
self.ready();
initializing = false;
self.onStop(function() { postHandle.stop(); });
});
Is this the right path to take? If yes, how do I alert the user of new posts? Else, what would be a better way to implement this?
Thank you
This is a tricky question but also valuable as it pertains to a design pattern that is applicable in many instances. One of the key aspects is wanting to know that there is new data but not wanting to show it (yet) to the user. We can also assume that when the user does want to see the data, they probably don't want to wait for it to be loaded into the client (just like Facebook). This means that the client still needs to cache the data as it arrives, just not display it immediately.
Therefore, you probably don't want to restrict the data displayed in the publication - because this won't send the data to the client. Rather, you want to send all the (relevant) data to the client and cache it there until it is ready.
The easiest way involves having a timestamp in your data to work from. You can then couple this with a Reactive Variable to only add new documents to your displayed set when that Reactive Variable changes. Something like this (code will probably be in different files):
// Within the template where you want to show your data
Template.myTemplate.onCreated(function() {
var self = this;
var options = null; // Define non-time options
// Subscribe to the data so everything is loaded into the client
// Include relevant options to limit data but exclude timestamps
self.subscribe("posts", options);
// Create and initialise a reactive variable with the current date
self.loadedTime = new ReactiveVar(new Date());
// Create a reactive variable to see when new data is available
// Create an autorun for whenever the subscription changes ready() state
// Ignore the first run as ready() should be false
// Subsequent false values indicate new data is arriving
self.newData = new ReactiveVar(false);
self.autorun(function(computation) {
if(!computation.firstRun) {
if(!self.subscriptionsReady()) {
self.newData.set(true);
}
}
});
});
// Fetch the relevant data from that subscribed (cached) within the client
// Assume this will be within the template helper
// Use the value (get()) of the Reactive Variable
Template.myTemplate.helpers({
displayedPosts = function() {
return Posts.find({timestamp: {$lt: Template.instance().loadedTime.get()}});
},
// Second helper to determine whether or not new data is available
// Can be used in the template to notify the user
newData = function() {
return Template.instance().newData.get();
});
// Update the Reactive Variable to the current time
// Assume this takes place within the template helper
// Assume you have button (or similar) with a "reload" class
Template.myTemplate.events({
'click .reLoad' = function(event, template) {
template.loadedTime.set(new Date());
}
});
I think this is the simplest pattern to cover all of the points you raise. It gets more complicated if you don't have a timestamp, you have multiple subscriptions (then need to use the subscription handles) etc. Hope this helps!
As Duncan said in his answer, ReactiveVar is the way to go. I've actually implemented a simple facebook feed page with meteor where I display the public posts from a certain page. I use infinite scroll to keep adding posts to the bottom of the page and store them in a ReactiveVar. Check the sources on github here and the live demo here. Hope it helps!

What does fetch() in meteor exactly does?

Experimenting with meteor I came across a behavior I did not expect of fetch. Let's say I have a function :
findStuff = function(){
var cursor = Stuff.find({});
console.log(stuff.fetch()); // just to check
return cursor;
}
and I call it from a template
Template.stuff.helpers({
stuff : function(){
var stuff = findStuff();
console.log(stuff.fetch()); // just to check
return stuff;
}
});
The first log will correctly display the array but the second will display an empty array. I am quite confused about why it is so. My solution is to avoid calling fetch if I don't need it explicitly but I like to use it as a debugging tool.
You should read about cursor.rewind().
The forEach, map, or fetch methods can only be called once on a
cursor. To access the data in a cursor more than once, use rewind to
reset the cursor.

Adding a model to Collection in Backbone [duplicate]

I'm running into an odd issue with a Backbone.js Model where an array member is being shown as blank. It looks something like this:
var Session = Backbone.Model.extend({
defaults: {
// ...
widgets: []
},
addWidget: function (widget) {
var widgets = this.get("widgets");
widgets.push(widget);
this.trigger("change:widgets", this, widgets);
},
// ...
// I have a method on the model to grabbing a member of the array
getWidget: function (id) {
console.log(this.attributes);
console.log(this.attributes.widgets);
// ...
}
});
I then add a widget via addWidget. When trying getWidget the result I get (in Chrome) is this:
Object
widgets: Array[1]
0: child
length: 1
__proto__: Array[0]
__proto__: Object
[]
It's showing that widgets is not empty when logging this.attributes but it's shown as empty when logging this.attributes.widgets. Does anyone know what would cause this?
EDIT
I've changed the model to instantiate the widgets array in the initialization method to avoid references across multiple instances, and I started using backbone-nested with no luck.
Be careful about trusting the console, there is often asynchronous behavior that can trip you up.
You're expecting console.log(x) to behave like this:
You call console.log(x).
x is dumped to the console.
Execution continues on with the statement immediately following your console.log(x) call.
But that's not what happens, the reality is more like this:
You call console.log(x).
The browser grabs a reference to x, and queues up the "real" console.log call for later.
Various other bits of JavaScript run (or not).
Later, the console.log call from (2) gets around to dumping the current state of x into the console but this x won't necessarily match the x as it was in (2).
In your case, you're doing this:
console.log(this.attributes);
console.log(this.attributes.widgets);
So you have something like this at (2):
attributes.widgets
^ ^
| |
console.log -+ |
console.log -----------+
and then something is happening in (3) which effectively does this.attributes.widgets = [...] (i.e. changes the attributes.widget reference) and so, when (4) comes around, you have this:
attributes.widgets // the new one from (3)
^
|
console.log -+
console.log -----------> widgets // the original from (1)
This leaves you seeing two different versions of widgets: the new one which received something in (3) and the original which is empty.
When you do this:
console.log(_(this.attributes).clone());
console.log(_(this.attributes.widgets).clone());
you're grabbing copies of this.attributes and this.attributes.widgets that are attached to the console.log calls so (3) won't interfere with your references and you see sensible results in the console.
That's the answer to this:
It's showing that widgets is not empty when logging this.attributes but it's shown as empty when logging this.attributes.widgets. Does anyone know what would cause this?
As far as the underlying problem goes, you probably have a fetch call somewhere and you're not taking its asynchronous behavior into account. The solution is probably to bind to an "add" or "reset" event.
Remember that [] in JS is just an alias to new Array(), and since objects are passed by reference, every instance of your Session model will share the same array object. This leads to all kinds of problems, including arrays appearing to be empty.
To make this work the way you want, you need to initialize your widgets array in the constructor. This will create a unique widget array for each Session object, and should alleviate your problem:
var Session = Backbone.Model.extend({
defaults: {
// ...
widgets: false
},
initialize: function(){
this.set('widgets',[]);
},
addWidget: function (widget) {
var widgets = this.get("widgets");
widgets.push(widget);
this.trigger("change:widgets", this, widgets);
},
// ...
// I have a method on the model to grabbing a member of the array
getWidget: function (id) {
console.log(this.attributes);
console.log(this.attributes.widgets);
// ...
}
});
Tested in a fiddle with Chrome and Firefox: http://jsfiddle.net/imsky/XBKYZ/
var s = new Session;
s.addWidget({"name":"test"});
s.getWidget()
Console output:
Object
widgets: Array[1]
__proto__: Object
[
Object
name: "test"
__proto__: Object
]

Resources