Meteor client side collection needs to have all data populated before anything else - meteor

I'm trying to use a client side collection as a site configuration system. I insert documents representing my different pages, and the iron-router and navigation tabs all use them to determine what pages they are and what templates are represented by them. Each page uses a {{> contentTemplate}} inclusion helper to load it's relevant template.
It all works great, when the data has all loaded. When I restart the app on certain pages, the data hasn't loaded yet, and I receive the Exception from Deps recompute function: Error: Expected null or template in return value from inclusion function, found: undefined error.
Here's my javascript:
StoriesArray = [
{ category: 'teaching', contentTemplate: 'teachingHome', title: 'Teaching Home'},
...
];
Stories = new Meteor.Collection(null);
StoriesArray.forEach(function (story, index) {
story._id = index + '';
Stories.insert(story);
});
// in main.js
Template.teachingPost.contentTemplate = function() {
console.log(this);
console.log(this.contentTemplate);
return Template[this.contentTemplate];
};
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() { return Stories.findOne({contentTemplate: 'teachingHome', category: 'teaching'}); }
});
The console logs in the contentTemplate helper above log twice, the first time as this:
Object {} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
undefined main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
and the second time as this:
Object {category: "teaching", contentTemplate: "teachingHome", title: "Teaching Home"} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
teachingHome main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
so the router is simply trying to load this data too early.
I've tried putting the StoriesArray loading process into different files all over my app, including lib, and even tried putting it into Meteor.startup, but it's always the same result.
The normal iron-router waitOn/subscription pattern doesn't really apply here, since this is a client side collection built with null, that has no server representation. I don't want this to have server representation, because this is static content that there's no need to go to my server for.
How do I ensure this information is done before continuing?

Untested, but per Iron Router's docs on waitOn:
Returning a subscription handle, or anything with a ready method from the waitOn function will add the handle to a wait list.
Also in general it's better to use find with data, rather than findOne, as find will return an empty cursor when the collection is empty as opposed to findOne returning undefined. So try this:
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() {
return Stories.find({contentTemplate: 'teachingHome', category: 'teaching'});
},
waitOn: function() {
var handle = {};
handle.ready = function() {
if (Stories.find().count() !== 0)
return true;
else
return false;
}
return handle;
}
});
And adjust your Template.teachingPost.contentTemplate function to work with a cursor rather than an object.

Related

How to use angular-meteor helpers and Meteor.methods together

I use a bunch of helper methods in my project. Some of them require to load the whole collection into the client due the restriction of the api on client side (the distinct function!). I googled the problem and found Meteor.methods as solution.
Can I use helpers (like this.helpers) into Meteor methods? Or how should I dynamically update my data in the frontend?
Can someone give me an example?
Additional information:
class View2 {
constructor($interval, $scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.helpers({
getOrderNumber(){
this.tempVar = Kafkadata.find().fetch();
this.tempVar2 = _.pluck(this.tempVar, 'orderNumber');
this.tempVar3 = _.uniq(tempVar2, false);
return this.tempVar3;
},
});
}
This is an example for a helpers query. Currently, this code runs client-side. I get ALL orders(tempvar) and then remove ALL data except the ordernumbers(tempvar2). At the end I remove all multiple ordernumbers. ordernumber is not an unique value. Here is an example from one of the collections:
{"orderNumber":"f2a3ed95-fcc3-4da0-9b3f-32cf5ed087f8","value":12480,"booleanValue":false,"intValue":12480,"doubleValue":0,"status":"GOOD","itemName":"MILLING_SPEED","timestamp":1479145734448,"_id":{"_str":"5824f4bc7ff3f0199861f11d"}}
I want to use functions like db.collection.distinct(). But they only work server-side. I think I must use Meteor.methods()to make this thing server-side. But what about this helpers function? How do they work on Meteor.methods()?
EDIT2:
my test:
client-side:
folder:myProject/imports/ui/view1
class View1 {
constructor($interval, $scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.helpers({
// some code
getTestData(){
Meteor.call('allTestData',function(error, result){
if(error){
console.log("error");
}else{
return result;
}
});
}
}); //end of contructor
// this is my testfunction, which is bound to a button!
testFunction(){
Meteor.call('allTestData',function(error, result){
if(error){
alert('Error');
}else{
console.log(result);
}
});
}
on the server-side:
folder:myProject/server/main.js
Meteor.methods({
allTestData:()=>{
var results=Kafkadata.find().count();
console.log(results);
return results;
},
});
and this is my view1.html:
//some code
<md-button ng-click="view1.testFunction()"> It works!</md-button>
<h1>{{view1.getTestData}}</h1>
Why does the button work, but not the helper?
Even though .distinct is supported by Mongo, Meteor does not expose it, even on the server. You just have to use _.uniq as your example shows, but for performance reasons it's better if it runs on the server.
Below is an example of a helper that I use:
aweek: () => {
if (debug)
console.log("Querying weekly appointments for "+this.selectedWeek.format("Do MMMM"));
var weekApts = Appointments.find(
{start: {$gte: new Date(this.getReactively('this.selectedWeek').clone().day(1)),
$lt: new Date(this.getReactively('this.selectedWeek').clone().endOf('week'))},
elderid: Meteor.userId()
}).fetch();
return utils.services.getAWeek(weekApts,utils.data.elderTimeFormat);
},
Note the use of this.getReactively('this.selectedWeek') in the code... basically this tells Meteor to run this helper reactively, so if the value of this.selectedWeek changes, the helper will get re-run. So when I click on a week in the calendar and update the variable, it runs my helper again to get the data.
The utils.services.getAWeek() function does some calculation and formatting on the array of data that makes is easier to display.
If you create a Meteor Method to do processing, I would make it update a collection with its results, and then your helper on the client will update automatically. Best make the technology do the work for you :)

Is there a way to update the URL in Flow Router without a refresh/redirect?

Is there a way to update a part of the URL reactively without using FlowRouter.go() while using React and react-layout?
I want to change the value in the document that is used to get the document from the DB. For example, if I have a route like ~/users/:username and update the username field in the document, I then have to user FlowRouter.go('profile', {data}) to direct the user to that new URL. The "old" route is gone.
Below is the working version I have, but there are two issues:
I have to use FlowRouter.go(), which is actually a full page refresh (and going back would be a 404).
I still get errors in the console because for a brief moment the reactive data for the component is actually wrong.
Relevant parts of the component are like this:
...
mixins: [ReactMeteorData],
getMeteorData() {
let data = {};
let users = Meteor.subscribe('user', {this.props.username});
if (user.ready())
data.user = user;
return data;
}
...
updateName(username) {
Users.update({_id:this.data.user._id}, {$set:{username}}, null, (e,r) => {
if (!e)
FlowRouter.go('profile', {username});
});
},
...
The route is like this:
FlowRouter.route('/users/:username', {
name: 'profile',
action(params) {
ReactLayout.render(Main, {content: <UserProfile {...params} />});
}
});
The errors I get in the console are:
Exception from Tracker recompute function:
and
TypeError: Cannot read property '_id' of undefined

Where do I set Session defaults so that they are available in my subscriptions?

I have a helper function that depends on a collection document lookup, the result of which it passes to a subscription via a Session. It then needs to query the documents from that subscription.
The code explains it better than I could.
Helper:
var selection = Selections.findOne()
var itemIds = function() {
return selection && selection.itemIds
}
var itemIdsArray = itemIds()
Session.set('itemIdsArray', itemIdsArray)
console.log(Session.get('itemIdsArray'))
_.each(Items.find({_id: {$in: itemIdsArray}}).fetch(), function(element, index, list) {
//doing stuff
})
Subscription:
Meteor.subscribe('itemsById', Session.get('itemIdsArray'))
Publication:
Meteor.publish('itemsById', function(itemIdsArray) {
return Items.find({_id: {$in: itemIdsArray}})
})
My console.log returns an undefined value before it returns the array of IDs. So undefined gets passed all the way to the publication, which complains of a null value (which is weird in itself) after $in and breaks.
My solution was to set the Session to default to [],
Session.setDefault(`itemIdsArray`, [])
which I honestly had high hopes that it'd work, but alas, it did not.
I've tried putting it inside IronRouter's onBeforeAction, I've tried putting it at the top of the helper, I've tried putting it pretty much anywhere but it still logs and returns undefined once before it gets the correct value.
I've also tried to move around my subscription, from waitOn to subscriptions to onAfterAction to onRendered, but those attempts have been utterly fruitless.
What should I do?
That's fairly typical behavior in Meteor. Session variables are not always ready at the time. The usual way of dealing with this is to introduce a guard in the helper that checks the variable is defined before doing anything else with it.
In your case something like this would work: itemsIdArray = itemIds() || [];
To answer the actual question you are asking, where do you set Session defaults that they are available in your subscriptions: it's not important where you set them, but when you access them. You can wait for the subscription to be ready using iron router's waitOn() function, or you can check the subscription handle's ready() function (see https://github.com/oortcloud/unofficial-meteor-faq#user-content-how-do-i-know-when-my-subscription-is-ready-and-not-still-loading)
If you return a subscription in your waitOn option of Iron Router you should have the data in your template then:
Router.route('/yourRoutePath/:_id', {
// this template will be rendered until the subscriptions are ready
loadingTemplate: 'loading',
waitOn: function () {
// return one handle, a function, or an array
return Meteor.subscribe('itemsById', this.params._id);
},
action: function () {
this.render('myTemplate');
}
});
Your template helper:
Template.myTemplate.helpers({
items: function() {
return Items.find();
}
});
I noticed that you publish Items collection and you want to use Selections collection in your helper. If you need more than one subscription, you can return an array of subscriptions in waitOn:
waitOn: function () {
// return one handle, a function, or an array
return [
Meteor.subscribe('itemsById', this.params._id),
Meteor.subscribe('selections')
];
}
WaitOn ensures that your template will be rendered when all subscriptions are ready.

Cannot acces properties of resulting array of cursor.fetch();

I have a helper where I want to acces the properties of a different collection.
Template.notification.helpers({
username: function () {
game = Games.findOne({_id: this.gameId}, {fields: {players:1}});
console.log(game) // output is correct
}
})
If I log this, it wil produce the result I expected:
Object {players: Array[2], _id: "qF3skjX2755BYcr8p"}
However, if I in my helper function I try to use/reach this properties I get an undefined error.
Template.notification.helpers({
username: function () {
game = Games.findOne({_id: this.gameId}, {fields: {players:1}});
console.log(game._id) // error;
console.log(game.players) // error
}
})
Output:
Exception from Deps recompute function: TypeError: Cannot read property 'players' of undefined
Why is this happening?
This happens because when Meteor initiall loads on your web browser, all the html and js is ready, but the data is not yet ready.
If you tried to check console.log(game) it may be null. It does this only when the page has loaded. If you load the template after all the data has downloaded you wouldn't see this issue.
When the data arrives the username helper would re-run with the new data.
In the meanwhile you just need to take care of this exception:
var game = Games.findOne({_id: this.gameId}, {fields: {players:1}});
if(!game) return null;

Why is Meteor removing an object when observing a collection?

Background
I have "Lists" and "Products" collections, Products belong to a List
A List has a description, from which products are generated
On startup, a new List is created that's unique for that visitor
The List id is stored in the Session
What I Want
I want Products to be generated when the description of a List changes.
The first step is that when the list for the current visitor is changed, I want a new product to be inserted.
I get the feeling I'm going about this totally wrong...
The Problem
The product is inserted, appears in the browser for a split second, then vanishes. It's been removed by Meteor.
Code
Products = new Meteor.Collection("products");
Lists = new Meteor.Collection("lists");
if (Meteor.isClient) {
Meteor.startup(function () {
var my_list_id = Lists.insert({description: "Default list"});
Session.set("my_list", my_list_id);
var observed = Lists.find({_id: my_list_id}).observe({
changed: function (newDocument, oldDocument) {
Products.insert({list: newDocument._id, name: newDocument.description});
}
});
});
toggleElement = function (elementName) {
if(editedElementIs(elementName)) {
var newListDescription = $('textarea').val();
Lists.update(Session.get("my_list"), {description: newListDescription});
setEditedElement("");
} else {
setEditedElement(elementName);
}
};
// Including the rest in case I've misunderstood something.
// I don't see how any of this could cause the issue.
setEditedElement = function (elementName) {
return Session.set("edited_element", elementName);
};
editedElementIs = function (elementName) {
return Session.get("edited_element") == elementName;
};
Handlebars.registerHelper('editedElementIs', editedElementIs);
Handlebars.registerHelper('products', function() {
return Products.find({list: Session.get("my_list")});
});
Template.list_form.listDescription = function () {
return Lists.findOne({_id: Session.get("my_list")}).description;
};
Template.adminbar.events({
'click a#editlist' : function () {
toggleElement("list");
},
'click a#editsidebar' : function () {
toggleElement("sidebar");
}
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
});
}
What I've Tried
Obviously, I can just do this:
if(editedElementIs(elementName)) {
var newListDescription = $('textarea').val();
Products.insert({list: Session.get("my_list"), name: newListDescription});
Lists.update(Session.get("my_list"), {description: newListDescription});
...
But that's writing clumsy update code that I'd like to house in an observer.
It looked like the product was being removed. So I've observed when a product is removed thus:
Products.find({list:my_list_id}).observe({
removed: function (oldDocument) {
throw error("wow");
console.log("Removed Product" + oldDocument);
}
})
and this observer is called immediately after the Product is inserted.
I get the stack trace:
at Object.Products.find.observe.removed (http://localhost:3000/ListyMeteor.js?2d867b7481df6389658be864b54d864151e87da5:22:15)
at Object.cursor.observeChanges.removed (http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:909:52)
at http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:275:15
at _.extend.runTask (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:30:11)
at _.extend.flush (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:58:10)
at _.extend.drain (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:66:12)
at LocalCollection.remove (http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:500:22)
at Object.self._connection.registerStore.update (http://localhost:3000/packages/mongo-livedata/collection.js?682caa185350aa26968d4ffc274579a33922f0e6:109:32)
at Object.store.(anonymous function) [as update] (http://localhost:3000/packages/livedata/livedata_connection.js?5d09753571656c685bb10c7970eebfbf23d35ef8:404:48)
at http://localhost:3000/packages/livedata/livedata_connection.js?5d09753571656c685bb10c7970eebfbf23d35ef8:984:19
It looks like Meteor is flushing the Products collection on the client side.
I'm clearly misunderstanding how Meteor works.
Any ideas on why this is happening?
Update 1
It looks like this is happening because insert is being called within an observer:
Why does meteor undo changes to collections nested in an observer method?
I'll post back here once I confirm.
Is autosubscribe turned on or off?
If you turn autosubscribe off, it could happen that your client updates the server copy and then on a subsequent update from the server - does not get all the items because its not subscribed to that collection.
Easiest way to check is to query the mongo db -
meteor mongo
Query the mongo db if your product has been added to the document.
If it has, then it is an autosubscribe issue -
You will have to create publish (on server) and subscribe (on client) methods as given here http://docs.meteor.com/#meteor_publish

Resources