I have an issue with a template rendering before my subscription is received from server + computation function calculates the necessary data to actually render it. Sometimes, it works fine and the process first subscribes to the MongoDB, renders my computeValues() function and then loads the template. When I try the same thing again, the template renders before the actual data is calculated and results in the below error:
Exception in template helper: TypeError: Cannot read property 'length'
of undefined
That obviously tells me that the computeValues() function ran without getting data from my subscription, where it went through an empty DB and couldn't do the calculation properly.
Here is the Iron:Router code:
Router.route("/management/bankroll", {
subscriptions: function(){
return Meteor.subscribe("bank");
},
action: function(){
this.wait(computeValues());
if(this.ready()){
this.layout("bankroll");
} else{
this.layout("loading");
}
},
name: "bankroll"
});
I tried all kinds of hooks like onBeforeAction, I used .wait()...pretty much anything and the behavior is still the same. One instance, the upload works and the template rendering waits until data is pulled like it's supposed to, and if I try it again, the compute function runs before subscription is received.
I have also used waitOn for the subscription and that doesn't work either.
It's basically the template renders before the computeValues function does the calculation due to not being able to retrieve the subscription in time. This happens more often in larger files where retrieving the subscription takes longer.
Anyone know what the issue is?
Related
I'm a beginner in Ionic and Firebase. To learn using ionic+firebase, I'm writing a RandomQuote app to fetch a random entry from Firebase. A reload() method is called when I click a reload button, and the random quote is displayed as expected.
However, I also want the quote to display when the app is loaded, i.e., before I click the reload button. I call the reload() method in the constructor but it doesn't work. I have tried to search for answers on the web but cannot find anything that I could understand. Not sure if I'm searching the wrong keywords or in the wrong domains.
The following is the reload() method that I put in my FirebaseProvider class and called from my home.ts:
reload(){
this.afd.list('/quoteList/').valueChanges().subscribe(
data => {
this.oneQuote = data[Math.floor(Math.random() * data.length)];
}
)
return this.oneQuote;
}
Can anyone give me some hints? Or any pointer to useful books / materials for beginners will also be highly appreciated. Thank you very much.
Data is loaded from Firebase asynchronously. This means that by the time your return statement runs this.oneQuote doesn't have a value yet.
This is easiest to say by placing a few log statements around your code:
console.log("Before subscribing");
this.afd.list('/quoteList/').valueChanges().subscribe(
data => {
console.log("Got data");
}
)
console.log("After subscribing");
When you run this code, the output is:
Before subscribing
After subscribing
Got data
This is probably not what you expected. But it completely explains why your return statement doesn't return the data: that data hasn't been loaded yet.
So you need to make sure your code that needs the data runs after the data has been loaded. There are two common ways to do this:
By moving the code into the callback
By returning a promise/subscription/observable
Moving the code into the callback is easiest: when the console.log("Got data") statement runs in the code above, the data is guaranteed to be available. So if you move the code that requires the data into that place, it can use the data without problems.
Returning a promise/subscription/observable is a slightly trickier to understand, but nicer way to doing the same. Now instead of moving the code-that-needs-data into the callback, you'll return "something" out of the callback that exposes the data when it is available. In the case of AngularFire the easiest way to do that is to return the actual observable itself:
return this.afd.list('/quoteList/').valueChanges();
Now the code that needs the quotes can just subscribe to the return value and update the UI:
reload().subscribe(data => {
this.oneQuote = data[Math.floor(Math.random() * data.length)];
}
A final note: having a reload() method sounds like an antipattern. The subscription will already be called whenever the data in the quoteList changes. There is no need to call reload() for that.
I believe the issue is with the routing, but I may be wrong here.
I have an issue where a function that I call via Iron Router sometimes doesn't fully finish making necessary computations before the template renders and the page is filled with n/a's and NaNs (because of the calculations not finishing before the template loads or renders). So what happens is after the user logs in, he/she uploads data via external file. This data is then stystematically added to an existing MongoDB collection then calculations are being made by function computeValues().
My problem, as I've stated above, is that there are times where the computeValues() function doesn't completely finish making necessary calculations, but the template renders anyway. When I refresh this page, the process works as it's supposed to. It's only when I'm routed from one page to this particular template/page (called mainPage) when this issue exists. Then, there are other instances where it works like it's supposed to and the template doesn't render before the function executes and computers the data completely.
Here is the particular code for the Iron Router:
Router.route("/analyze/main", {name: "main",
subscriptions: function(){
return Meteor.subscribe("data");
},
onBeforeAction: function(){
if(! Meteor.user()){
this.layout("accessDenied");
} else{
this.next();
}},
action: function(){
this.wait(computeValues());
if(this.ready()){
this.layout("mainPage");
} else{
this.layout("loading");
}
}});
Here is what I added to the template's onRendered() function:
Template.mainPage.onRendered(function(){
computeValues();
});
Adding or removing the onRendered part for the template makes no reason. The problems still persist from time to time. I just can't figure out what's causing this or how to fix it.
The particular function that does all the calculations, computeValues(), is located in the lib folder due to the need to expedite the loading process and have access to global variables immediately. I can't post the code because it's over 3000 lines long. But the extensiveness of this function does not explain why it works 20% of the time when routed, and not working 80% of the time, all while working 100% of the time being on the mainPage template and refreshing for reload.
You could use IR to waitOn one or more subscriptions but it's not quite as well suited to waiting on set of computations.
Firstly I would move computeValues() to Template.mainPage.onCreated rather than onRendered
Secondly I would set a session variable to false when starting the computation and to true when finished and show a spinner in the template until it becomes true.
I'm running the latest Meteor (v1.1.0.3) on OS X 10.6.8 in Firefox 39.0.
I'm using accounts-ui and accounts-google for login management. I have a hand-rolled profile form with (among other things) a 'name' field. The initial value of this field should be either the name that is already set in their profile or the one that Google supplies.
I've defined the following template helper:
Template.profile_edit.helpers({
my_name: (function () {var u=Meteor.user(); return u.profile.name || u.services.google.name;}())
});
I use the value in my template as {{ my_name }}. When I start meteor everything compiles just fine, but when I load the profile page I get the following Javascript error:
TypeError: u is undefined
...me: (function () {var u=Meteor.user(); return u.profile.name || u.services.googl...
Not immediately relevant, but just for completeness:
After the page loads, the 'name' field in the form is blank.
I am logged in.
When I pull up Meteor's mongo instance in my terminal I can see my user record; the name in the profile is NOT set, the name in the services.google.name IS set.
Why is this error happening and how can I solve it?
The Problem
This is a common issue that people have when first starting out with Meteor. The problem is that when this helper is executed during page load, depending on the response time for the publication from the server, the data for the currently logged in user may not be available yet. This makes it seem intermittent because at times the data is published in time, and others it's not.
Possible Solutions
One possible solution is to install meteorhacks:fast-render. This will publish the logged in user (due to the null publication) with the initial html for the page and guarantee that the user is available when this helper is run. Data other than the currently logged in user will need to be properly set up as subscriptions on the router for fast render to take effect.
The other solution, and one that will work without installation of a new package is to guard against undefined. This will effectively let the helper return undefined when there is no data, but once the data is available the helper will reactively rerun and the proper data will be returned.
Template.profile_edit.helpers({
my_name: function () {
var u=Meteor.user();
if(u){
return u.profile.name || u.services.google.name;
}
}
});
Aside
In your helper I notice that you are using syntax like my_name:(function(){}()). While this will give you what seems like a desired outcome, the problem is that you are immediately invoking this function and assigning its value to the my_name helper instead of assigning it a function that can be called multiple times when the value changes. This breaks reactivity and so the second solution would not work due to it's reliance on it.
Template.profile_edit.helpers({
my_name: (function () {
/*
This is immediately invoked and basically
like using my_name: "bob"
*/
}())
});
Is it a good practice to make DOM changes from Meteor helpers? I'm currently relying on a javascript function to run inside the Meteor helper which makes the function run every time a collection data change occurs.
I know there is Tracker.autorun() but as far as I know, Tracker.autorun() only works for Session variables and does not work for collection data changes.
My current ways so far has not failed me or caused any problems but I'm not 100% sure if this was how Meteor was meant to be used.
Code Example
Template.page_body.helpers({
orange: function() {
do_some_rand_function()
return this.name
}
})
This code will make sure that do_some_rand_function() is ran every time this.name changes (this.name is a variable gotten from a Mongo Collection, therefore it is reactive).
No. Helpers should not have side effects (such as manually updating your DOM, modifying the database, making an HTTP request, etc.).
Your description sounds like a good use case for adding a template autorun in the rendered callback. All autoruns are reactive computations so they will rerun if any reactive variable used within them changes (Session, Meteor.user, Collections, etc.).
Give something like this a try:
Template.myTemplate.onRendered(function() {
this.autorun(function() {
if (MyCollection.findOne()) {
do_some_rand_function();
}
});
});
Also note that template autoruns are automatically stopped when the template is destroyed.
I have a template that contains a chart, rendered using MorrisJS. The chart should update when the currentData session variable is changed, so I have made it a reactive data source with:
Template.chart.rendered = function() {
var template = this;
Deps.autorun(function(c) {
// Stop if the template is removed from the dom
// Q: Is this really right?
if(template.__component__.dom.parentNode() === null) {
c.stop();
return;
}
var results = Session.get('currentData');
// ... render a chart with `results` as the data
Morris.Bar({element: template.$(".chart-container"), data: results, ...});
});
};
Notice how I do a fairly horrid check for when to stop the autorun above. This was necessary because without this, when I navigate away from the page using the template (I'm using iron-router) to another page and back, I get warnings in the log like "Can't select in removed DomRange". I'm pretty sure this is happening because the template instance is removed, but the autorun is still running.
I feel like I'm doing something wrong here, though. Is there (a) a better place to put the autorun so that it doesn't have this problem or (b) a better way to stop the computation when the template instance is removed from the DOM?
I tried to find a way to do it with created and destroyed handlers, but I couldn't figure out how.
Tracker.autorun returns a handle that you can store as a template instance property, then call its stop method in the onDestroyed lifecycle event.
Template.chart.onRendered(function(){
this.computation = Tracker.autorun(function(){...});
});
Template.chart.onDestroyed(function(){
this.computation.stop();
});
EDIT 29-09-2014
In newer versions of Meteor (0.9 onward), there is a new autorun function available on template instances which provide simpler code to achieve the same result : no need to store and stop the computation manually, this is taken care of by the framework.
Template.chart.onRendered(function(){
this.autorun(function(){...});
});
With the new autorun
Template.chart.onRendered(function(){
this.autorun(function(computation){
...
computation.stop();
});
});
but with this autorun, when chart template is removed from the DOM it is removed automatically.
This is in Meteor documentation here:
The Computation is automatically stopped when the template is destroyed.