I have the following route defined in my iron-router:
this.route("/example/:id", {
name: "example",
template: "example",
action: function () {
this.wait(Meteor.subscribe('sub1', this.params.id));
this.wait(Meteor.subscribe('sub2', <<data of sub1 needed here>>));
if (this.ready()) {
this.render();
} else {
this.render('Loading');
}
}
});
I want to wait for sub1 and sub2 before rendering my actual template. The problem is that I need a piece of data which is part of the result of sub1 for the sub2 subscription.
How can I wait sequential for subscriptions? So that I can split the wait in two steps and wait for my first subscription to be finished. Then start the second subscription and then set this.ready() to render the template?
A workaround that I thought of was to use Reactive-Vars for the subscriptions and dont use .wait and .ready which is provided by iron-router. But I would like to use a more convenient solution provided by iron-router or Meteor itself. Do you know a better solution for this?
Thanks for your answers!
Publish Composite Package:
If the second subscription is reactively dependent on certain fields from the first dataset -- and if there will be a many-to-many "join" association, it might be worth looking into reywood:publish-composite package:
It provides a clean and easy way to manage associated subscriptions for collections with hierarchical relations.
Publication:
Meteor.publishComposite('compositeSub', function(id) {
return {
find: function() {
// return all documents from FirstCollection filtered by 'this.params.id' passed to subscription
return FirstCollection.find({ _id: id });
},
children: [
find: function(item) {
// return data from second collection filtered by using reference of each item's _id from results of first subscription
// you can also use any other field from 'item' as reference here, as per your database relations
return SecondCollection.find({ itemId: item._id });
}
]
}
});
Subscription:
Then you can just subscribe in the router using:
Meteor.subscribe('compositeSub', this.params.id);
Router hooks:
As a suggestion, hooks in iron-router are really useful, as they take care of a lot of things for you. So why not use the waitOn hook that manages this.wait and loading states neatly?
this.route('/example/:id', {
name: "example",
template: "example",
// 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('compositeSub', this.params.id);
// FYI, this can also return an array of subscriptions
},
action: function () {
this.render();
}
});
You can use the configure option to add a template for loading event:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading'
});
Note regarding the comment in question:
If both subscriptions only depend on the same id parameter passed to it, you can use the following, as mentioned by #abj27 in the comment above -- however, this does not seem to be the case, going by your example:
Publication:
Meteor.publish("subOneAndTwo", function (exampleId) {
check(exampleId, String);
return [
FirstCollection.find({ _id: exampleId });
SecondCollection.find({ firstId: exampleId })
];
});
Subscription:
Meteor.subscribe('subOneAndTwo', this.params.id);
So just check what you need and use a solution accordingly.
https://github.com/kadirahq/subs-manager
With this package, you can assign a subscription to a variable. Then, you can check that variable's ready state. I just got this working... after years of trying to understand.
Here is my code snippet that works, but oddly I had to wrap it in a 1ms timeout to work...
```
Router.route('/activity/:activityId', function (params) {
var params = this.params;
setTimeout(function(){
var thePage = window.location.href.split("/");;
window.thePage = thePage[4];
dbSubscriptionActivites.clear();
window.thisDbSubscriptionActivites = "";
window.thisDbSubscriptionActivites = dbSubscriptionActivites.subscribe("activityByPage", window.thePage);
Tracker.autorun(function() {
if(window.thisDbSubscriptionActivites.ready()) {
dbSubscriptionComments.clear();
window.thisDbSubscriptionComments = "";
window.thisDbSubscriptionComments = dbSubscriptionComments.subscribe('fetchComments', "activity", Activities.findOne({})._id);
BlazeLayout.render("activity");
$('body').removeClass("shards-app-promo-page--1");
}
});
},1); // Must wait for DOM?
});
```
Examine: window.thisDbSubscriptionActivites = dbSubscriptionActivites.subscribe("activityByPage", window.thePage);
I'm setting as a window variable, but you could do a const mySub = ...
Then, you check that in the autorun function later.
You can see there is where I am doing subscriptions.
I suppose I really should move the BlazeLayout render in to another .ready() check for the comments.
Related
Is it possible to alter (add more fields for each record) a returned collection in onBeforeAction or similar hook?
I have InvoiceHistory collection which I am paginating through. I also want to display as part of each invoice the company name, business address, email address and VAT registration number - and these four fields are stored in another collection. So I would like to add these four fields to each record returned from InvoiceHistory. If there is another way to do it I am open to suggestions. I am using Alethes meteor pagination which loses the helper fields returned in its itemTemplate when you navigate/browse/page to the second page. Alethes pagination also relies on iron-router, is it maybe possible to achieve what I want with the help of iron-router
========================================================================
#Sean. Thanks. Below is the code that uses meteor publish composite:
if (Meteor.isServer) {
import { publishComposite } from 'meteor/reywood:publish-composite';
publishComposite('invoicesWithCompanyDetails', function(userId, startingDate,endingDate) {
return {
find() {
// Find purchase history for userId and the two dates that were entered. Note arguments for callback function
// being used in query.
return PurchaseHistory.find({Id:userId,transactionDate:{$gte:new Date(decodeURIComponent(startingDate)),
$lt:new Date(decodeURIComponent(endingDate))}});
},
children: [
{
find() {
return CompanySharedNumbers.find(
{ _id:"firstOccurrence" },
{ fields: { companyName: 1, companyAddress: 1 } }
);
}
}
]
}
});
}
Router.route('/store/invoices/:_username/:startingDate/:endingDate', { //:_startingDate/:_endingDate
name: 'invoices',
template: 'invoices',
onBeforeAction: function()
{
...
},
waitOn: function() {
var startingDate = this.params.startingDate;
var endingDate = this.params.endingDate;
return [Meteor.subscribe('systemInfo'),Meteor.subscribe('testingOn'),Meteor.subscribe('invoicesWithCompanyDetails',startingDate,endingDate)];
}
});
Pages = new Meteor.Pagination(PurchaseHistory, {
itemTemplate: "invoice",
availableSettings: {filters: true},
filters: {},
route: "/store/invoices/:_username/:startingDate/:endingDate/",
router: "iron-router",
routerTemplate: "invoices",
routerLayout: "main",
sort: {
transactionDate: 1
},
perPage: 1,
templateName: "invoices",
homeRoute:"home"
});
Instead of adding new fields to all your document. What you could do is to add a helper to load a company document :
Template.OneInvoice.helpers({
loadCompany(companyId){
return Company.findOne({_id: companyId});
}
});
And use it in your template code:
<template name="OneInvoice">
<p>Invoice id: {{invoice._id}}</p>
<p>Invoice total: {{invoice.total}}</p>
{{#let company=(loadCompany invoice.companyId)}}
<p>Company name: {{company.name}}</p>
<p>Company business address: {{company.businessAddress}}</p>
{{/let}}
</template>
This way, the code is very simple and easily maintainable.
If I'm understanding you correctly, this sounds like a job for a composite publication.
Using a composite publication you can return related data from multiple collections at the same time.
So, you can find all your invoices, then using the company ID stored in the invoice objects you can return the company name and other info from the Companies collection at the same time.
I've used composite publications before for complex data structures and they worked perfectly.
Hope that helps
I have a template that subscribes to a document. Everything works fine in the DOM and Blaze updates as soon as an attribute used in the template helpers is changed.
I also have some custom logic that doesn't appears in the DOM and depends on the document attributes. How can I call a function to change that logic when an attribute is updated?
I'm looking for something like this.data.attr.onChanged where this would refer to the template and this.data is the data send to the template, as usual; or a Meteor function that is rerun on change where I could put my callback in.
I hoped that template.onRendered would be recalled, but that's not the case.
I've read a lot about reactive variables, but could not find how they could be useful here.
[edit] the change is coming from the server that is communicating with another service
I've tried Tracker.autorun like this:
Template.editItem.onRendered(function() {
var self = this;
Tracker.autorun(function () {
console.log("tracker", self.data.item.socketId);
});
});
And the corresponding route is:
Router.route('editItem', {
path: '/edit/:_id',
waitOn: function () {
var sub = Meteor.subscribe('item', this.params._id);
return [sub];
},
data: function () {
return {item: Items.findOne(this.params._id)};
},
action: function () {
if (this.ready())
this.render();
}
});
At some point, the property socketId gets removed from the corresponding document by the server and I'm sure of that since I've checked in the shell, but the tracker doesn't rerun.
Use Template.currentData().item.socketId instead of self.data.item.socketId, this will give you reactivity.
And in templates generally, use self.autorun instead of Tracker.autorun (unlike Tracker.autorun, this will ensure that the autorun is stopped when the template is destroyed). Likewise, if you want to subscribe in a template, use self.subscribe instead of Meteor.subscribe.
Code to see if Template.currentData() works for you:
Template.editItem.onRendered(function() {
var self = this;
self.autorun(function () {
console.log("tracker", Template.currentData().item.socketId);
});
});
I'm not sure if I got you right, you just want to observe your html inputs and apply the new value to your helper method(s) on change?!
If so, you could use session variables to store your temporary UI state:
// observe your input
Template.yourTemplate.events({
"change #inputA": function (event) {
if(event.target.value != "") {
Session.set("valueA", event.target.value);
}
}
}
// apply the changed value on your helper function
Template.yourTemplate.helpers({
getSomeData: function() {
var a = Session.get("valueA");
// do something with a ..
}
}
In meteor's official todo app tutorial this concept is also used.
If you need to re-run something which is not part of DOM/helper, you can use Tracker.autorun. According to meteor docs, Run a function now and rerun it later whenever its dependencies change.
here's the docs link
Try moving the subscription into Tracker.autorun
Template.editItem.onRendered(function() {
var self = this;
Tracker.autorun(function () {
Meteor.subscribe('item', this.params._id);
console.log("tracker", self.data.item.socketId);
});
});
Of course you can't use this.params there so you can store this as a Session variable
I'm not sure why this code works once in a while and fails other times:
var u = Meteor.users.findOne(username:'john');
console.log(u);
When I go to my page for the first time, sometimes the console.log(u) shows some results. But if I press refresh, console.log(u) shows undefined. I can't consistently reproduce one issue or the other. It seems pretty random when i get undefined or a collection. What's wrong with my code? How do I consistently get a collection for the variable u?
Like Christian Fritz said in comment on your question, it's probably a matter of collection not being fully loaded when your code is executed. If you use iron:router, you can use subscribe or waitOn as described there: http://iron-meteor.github.io/iron-router/#the-waiton-option so the page is loaded only when the collections are ready (meaning they are fully loaded).
You can also put it in a helper or use a Tracker Autorun to detect when your entry is available and then do whatever you want to do with it.
Edit: A sample for iron:router below
// myproject.jsx
var Cars = new Mongo.Collection('cars');
if(Meteor.isServer)
{
Meteor.publish("myCollections", function () {
return Meteor.users.find();
});
Meteor.publish("anotherCollection", function(){
return Cars.find();
});
}
//lib/router.js
Router.route('/my-page', {
name: 'myPage',
layoutTemplate: 'myPage',
waitOn: function() {
'use strict';
return [Meteor.subscribe('myCollection'),Meteor.subscribe('anotherCollection')];
},
data: function() {
'use strict';
return Collection.findOne();
}
});
My Meteor app runs slowly in the beginning for about ten seconds, and then becomes fast again. I am trying to improve the performance but having troubles to find the real cause.
I thought the problem was that I am publishing all the course information like following:
if (Meteor.isServer) {
Meteor.publish("courses", function() {
return Courses.find();
});
}
I tried using Kadira to monitor exactly what's happening. However, looking at the result, I am starting to think maybe it's not the real problem.
If it only takes 292ms for pubsub response time, it shouldn't feel that laggy but I cannot think of any other reason why the app would be so slow in the beginning and become fast again. Can an expert point me to the redirection?
UPDATE:
I could improve the duration of lagginess in the beginning by making the following changes:
in /server/publications.js
if (Meteor.isServer) {
Meteor.publish("courses", function() {
// since we only need these two fields for the search bar's autocomplete feature
return Courses.find({}, {fields: {'catalog':1, 'titleLong':1}});
});
Meteor.publish("courseCatalog", function(catalog) {
// publish specific information only when needed
return Courses.find({"catalog": catalog});
});
}
and in router.js I made changes accordingly so I subscribe based on specific pages. But there's still some lag in the beginning and I wonder if I can make more optimizations, and what is the real cause of the slowness in the beginning.
UPDATE2:
I followed the suggestion and made changes like below:
Session.set('coursesReady', false); on startup.
and in router:
Router.route('/', function () {
Meteor.subscribe("courses", function(err) {
if (!err) {
console.log("course data is ready")
Session.set('coursesReady', true);
}
});
....
and in /lib/helpers.js which returns data for typeahead library
if (Meteor.isClient) {
Template.registerHelper("course_data", function() {
console.log("course_data helper is called");
if (Session.get('coursesReady')) {
var courses = Courses.find().fetch();
return [
{
name: 'course-info1',
valueKey: 'titleLong',
local: function() {
return Courses.find().fetch();
},
template: 'Course'
},
But now the problem is that when the helper function is called, the data is never ready. The console print:
Q: How do I ensure that the helper function is called only after the data is ready, OR called again when the data is ready? Since Session is reactive, shouldn't it be called again automatically?
I can't check this right now, but I believe your issue might be that the course_data helper is being run multiple times before all 1000+ documents in the subscription are ready, causing the typeahead package to re-run some expensive calculations. Try something like this:
/client/views/global/helpers.js
Template.registerHelper("course_data", function() {
if (!Session.get('coursesReady')) return [];
return [ //...
/client/subscriptions.js
Meteor.subscribe("courses", function(error) {
if (!error) Session.set('coursesReady', true);
});
Update:
Really, Meteor's new features this.subscribe() and Template.instance().subscriptionsReady() are ideal for this. Session isn't really the right choice, but it should still be reactively updating (not sure why it isn't for you). Try instead making the following changes to /client/views/navwithsearch.js (and main, though ideally both templates should share a single search template):
Template.NavWithSearch.onCreated(function() {
this.subscribe('courses');
});
Template.NavWithSearch.onRendered(function() {
this.autorun(function() {
if (Template.instance().subscriptionsReady()) {
Meteor.typeahead.inject();
}
});
});
The idea is to tie the lifecycle of the subscription to the view that will actually be using that subscription. This should delay the typeahead injection until the subscription is completely ready.
Current common Iron Router pattern is to display a loading template while waiting for data to be available. But I would prefer simply to wait on the previous rendered template/data context until data is available and then trigger rerendering. Data is quickly available, so that short flicker of loading template is worse than a short delay user will experience after a link click.
Does this pattern work for you?
Router.route('/', {
name: 'nameOfTemplate',
data: function() { return CollectionName.find({title: 'nameOfMongoDBQuery'}); },
waitOn: function() { return Meteor.subscribe('nameOfSubscription'); } // waits until resources arrive before rendering page
});
You'll want to specify an explicit action function like so:
action: function() {
if (this.ready()) {
this.render();
}
}
This will just not render anything until the data is around.