Is it possible to do something like "filtered subscription" in Meteor: for example if you have a filter on month june and switching to july fetches the new data and subscribes to it?
i tried something like:
Meteor.publish("report", function (query, opt) {
return Report.find({ 'timestamp' : { $gte : query.from, $lt: query.to }}, options);
}
on client with iron router:
HomeController=RouteController.extend({
template:"home",
waitOn:function(){
var dates = getDates();
return Meteor.subscribe("report", dates);
},
fastRender: true
});
but it does not work.
Is there a better method to dynamically subscribe? Or does it just help to navigate with url pattern?
thanks
Is there a better method to dynamically subscribe?
There is an alternative method using template subscriptions, example below. I don't think it's better, just different.
Or does it just help to navigate with url pattern?
If you want to handle the subscriptions in the Router, then storing the subscription query params in the URL does help and has some added benefits in my opinion. But it depends on your desired app behavior.
Using Template Subscriptions approach :
This Meteor Pad example will subscribe to a range of data based on a select :
http://meteorpad.com/pad/26dd8YQevBbA5uNGA/Dynamic%20Subscription
Using Iron Router approach :
This route example will subscribe based on the URL . "items/0/10" will subscribe to the itemData with a range of zero to 10.
Router.route('Items', {
name:'Items',
path:'items/:low/:high',
subscriptions : function(){
var low = parseInt(this.params.low);
var high = parseInt(this.params.high);
return [
Meteor.subscribe("itemData",low,high),
];
},
action: function () {
if (this.ready()) {
this.render();
} else {
this.render('Loading');
}
}
});
I think either approach is fine and depends on your interface. Using the URL is nice because you can provide links directly to the range of data, use forward and back buttons in browser, good for paging lists of data.
The template subscriptions approach might be appropriate to change the data on a graph.
The specific issue you are having might be due to the fact that your getDates() is not reactive, so the subscription is only run once when the route waitOn is first run.
Related
So I have an Angular controller with a meteor helper method, as below.
function localeCtrl($scope, $reactive, $stateParams{
$reactive(this).attach($scope);
var self = this;
self.helpers({
locale: function(){ return Locales.findOne($stateParams.id)},
staff: function(){
// Load data from second collection based on current Locale.
// But how?
},
address: function(){
// Take self.location.address and massage it to provide
// google maps link. How?
}
tags: function(){
// Collect all unique instances of a given tag by
// iterating over the available locales.
// E. G. If 10 locales have the 'restaurant' tag, and 5
// more have the 'library' tag, I want an array of
// ['restaurant', 'library'] -- easy enough to do
// by iterating over the locales, but how do I do that
// reactively?
}
});
}
Unfortunately, I need to set additional properties based on the data fetched by locale(). I can't set them up when I initialize the controller because the value in locale() changes as data is fetched from the server. But I need access to the data in locale to, for example, create the google maps address, or fetch associated records. (They aren't imbedded in the locale document for reasons that I'm sure made sense at the time).
Edit:
Additionally, I'm using ground DB to store a local copy of the data for offline access, which makes life even more complicated.
Probably you best bet is to publish your collection using publishComposite which is implemented using the reywood:publish-composite package.
Add the package:
meteor add reywood:publish-composite
Now where you publish the Locales collection you would do something like this:
Meteor.publishComposite('locales', function() {
return {
find() {
//Put whatever you need in the query for locales
const query = {
_userId: this.userId
};
return Locales.find(query);
},
children: [{
find(locale) {
return Staff.find({ localeId: locale._id });
}
}]
};
});
Then in your controller before the helper you add this:
this.subscribe('locales');
Now you should be able to simply call the code like this:
this.helpers({
locale(){
return Locales.findOne(this.$stateParams.id);
}
});
And access it in the template like this:
locale.staff
Give that a try and let me know!
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!
Here is the problem :
I am currently programming a chatapp based on what i found on github (https://github.com/sasikanth513/chatDemo)
I am refactoring it with iron-router.
When I go to the page (clicking on the link) I get an existing chatroom (that's what I want)
When I refresh the page (F5) I get a new created chatroom ! (what i want is getting the existing chatroom ...)
Here is the code in ironrouter :
Router.route('/chatroom', {
name: 'chatroom',
data: function() {
var currentId = Session.get('currentId'); //id of the other person
var res=ChatRooms.findOne({chatIds:{$all:[currentId,Meteor.userId()]}});
console.log(res);
if(res){
Session.set("roomid",res._id);
}
else{
var newRoom= ChatRooms.insert({chatIds:[currentId, Meteor.userId()],messages:[]});
Session.set('roomid',newRoom);
}
}
});
You can find my github repo with the whole project : https://github.com/balibou/textr
Thanx a lot !
Your route data depends on Session variables which will be erased after a refresh. You have a few options but the easiest would be to put the room id directly into the route: '/chatroom/:_id'. Then you can use this.params._id to fetch the appropriate ChatRooms document. Note that you could still keep '/chatroom' for cases where the room doesn't exist, however you'd need to redirect to '/chatroom/:_id' after the insert.
In meteor, the Session object is empty when the client starts, and loading/refreshing the page via HTTP "restarts" the client. To deal with this issue, you could persist the user's correspondent id in a Meteor.user attribute, so that you could easily do:
Router.route('/chatroom', {
name: 'chatroom',
data: function() {
var currentId = Meteor.user().profile.correspondentId;
var res=ChatRooms.findOne({chatIds:{$all:[currentId,Meteor.userId()]}});
console.log(res);
if(res){
Session.set("roomid",res._id);
}
else{
var newRoom= ChatRooms.insert({chatIds:[currentId, Meteor.userId()],messages:[]});
Session.set('roomid',newRoom);
}
}
});
This would work, with the proper permissions, but I would recommend not allowing the direct update of that value on the client (I don't know if you want users to be able to override their correspondentId). So if you want to secure this process, replace all that code with a server method call, where your updates are safer.
Another (and more common case) solution was given by David Weldon, if you don't mind having ids in your URL (and therefore not a single url)
I have a template that displays documents from three different collections Cars, CarPaints, and CarPaintTypes. I know I need all these upfront at the Router level. The template will show a Car document, all the CarPaints that reference that Car, and all the CarPaintTypes that reference the returned CarPaints respectively (think nested list). The route to the template takes an id from the URL that represents Car._id.
Both the Cars collection and CarPaints collection make use of the Car._id as a field (it's the native _id of the Cars collection and a field in the CarPaints collection) so that's easy. However, the CarPaintTypes uses the CarPaint._id as a reference to what CarPaint it belongs to.
So I have three publications:
Meteor.publish('car', function(carId) {
return Cars.find({_id: carId});
});
Meteor.publish('carPaints', function(carId) {
return CarPaints.find({carId: carId});
});
Meteor.publish('carPaintTypes', function(carPaintId) {
return CarPaintTypes.find({carPaintId: carPaintId});
});
My route looks like:
this.route('car', {
path: '/car/:_id',
waitOn: function() {
return [Meteor.subscribe('car', this.params._id),
Meteor.subscribe('carPaints', this.params._id)];
// Can't figure out how to subscribe to or publish
// the carPaintTypes using all the results of what gets
// returned by 'carPaints'
}
});
My question is CarPaintTypes doesn't have the Car._id as a field, just the CarPaint._id to reference to a CarPaint document. Where and how I do take the results of the subscription to carPaints and pass each carPaint document that's returned to a subscription to carPaintTypes? Or is there a way to combine them all in the publication? Is it better to do it later on in my helpers? I figure since I know what I need at the route level, all the subscription calls should be in the route code.
You can grab all 3 cursors inside Meteor.publish method and simply return them:
Meteor.publish('carThings', function(carId){
var carPaint = CarPaints.findOne({carId:carId});
return [
Cars.find({_id: carId}),
CarPaints.find({carId: carId}),
CarPaintTypes.find({carPaintId: carPaint._id});
]
})
On client:
this.route('car', {
path: '/car/:_id',
waitOn: function() {
return [Meteor.subscribe('carThings', this.params._id)]
}
}
With Kuba Wyrobek's help, I figured it out. For what I was trying to achieve, the publish looks like this:
Meteor.publish('carThings', function(carId){
var carPaints = CarPaints.find({carId: carId}).fetch();
return [
Cars.find({_id: carId}),
CarPaints.find({carId: carId}),
CarPaintTypes.find({carPaintId: {$in: _.pluck(carPaints, "_id")}})
];
});
I didn't get that you could do manipulations inside your publication blocks. This is super cool and flexible. Thanks for your help.
I'm using the meteor-paginated-subscription package in my app. On the server, my publication looks like this:
Meteor.publish("posts", function(limit) {
return Posts.find({}, {
limit: limit
});
});
And on the client:
this.subscriptionHandle = Meteor.subscribeWithPagination("posts", 10);
Template.post_list.events = {
'click #load_more': function(event, template) {
template.subscriptionHandle.loadNextPage();
}
};
This works well, but I'd like to hide the #load_more button if all the data is loaded on the client, using a helper like this:
Template.post_list.allPostsLoaded = function () {
allPostsLoaded = Posts.find().count() <= this.subscriptionHandle.loaded();
Session.set('allPostsLoaded', allPostsLoaded);
return allPostsLoaded;
};
The problem is that Posts.find().count() is returning the number of documents loaded on the client, not the number available on the server.
I've looked through the Telescope project, which also uses the meteor-paginated-subscription package, and I see code that does what I want to do:
allPostsLoaded: function(){
allPostsLoaded = this.fetch().length < this.loaded();
Session.set('allPostsLoaded', allPostsLoaded);
return allPostsLoaded;
}
But I'm not sure if it's actually working. Porting their code into mine does not work.
Finally, it does look like Mongo supports what I want to do. The docs say that, by default, cursor.count() ignores the effects of limit.
Seems like all the pieces are there, but I'm having trouble putting them together.
None of the answers do what you really want becase none provide solution that is reactive.
This package does exactly what you want and also reactive.
publish-counts
I think you can see the demo: counts-by-room in meteor doc
It can help you publish the counts of your posts at server and get it at client
You can simply write this:
// server: publish the current size of your post collection
Meteor.publish("counts-by-room", function () {
var self = this;
var count = 0;
var initializing = true;
var handle = Posts.find().observeChanges({
added: function (id) {
count++;
if (!initializing)
self.changed("counts", 'postCounts', {count: count});
},
removed: function (id) {
count--;
self.changed("counts", postCounts, {count: count});
}
});
initializing = false;
self.added("counts", 'postCounts', {count: count});
self.ready();
self.onStop(function () {
handle.stop();
});
});
// client: declare collection to hold count object
Counts = new Mongo.Collection("counts");
// client: subscribe to the count for posts
Tracker.autorun(function () {
Meteor.subscribe("postCounts");
});
// client: simply use findOne, you can get the count object
Counts.findOne()
The idea of sub.loaded() is to help you with exactly this problem.
Posts.count() isn't going to return the right thing because, as you've guessed, on the client, Meteor has no way of knowing the real number of posts that live on the server. But what the client knows is how many posts it's tried to load. That's what that .loaded() tells you, and is why the line this.fetch().length < this.loaded() will tell you if there are more posts on the server or not.
What I would do is write a Meteor server side method that retrieves the count like so:
Meteor.methods({
getPostsCount: function () {
return Posts.find().count();
}
});
Then call it on the client, in observe to make it reactive:
function updatePostCount() {
Meteor.call('getPostsCount', function (err, count) {
Session.set('postCount', count);
});
}
Posts.find().observe({
added: updatePostCount,
removed: updatePostCount
});
Although this question is old, I thought I would provide an answer that ended up working for me. I did not create the solution, I found the basis for it here (so credit where credit is due): Discover Meteor
Anyway, in my case I was trying to get "size" of the database from client side, so I can determine when to hide the "load more" -button. I was using template level subscriptions. Oh and for this solution to work, you need to add reactive-var -package. Here is my (in short):
/*on the server we define the method which returns
the number of posts in total in the database*/
if(Meteor.isServer){
Meteor.methods({
postsTotal: function() {
return PostsCollection.find().count();
}
});
}
/*In the client side we first create the reactive variable*/
if(Meteor.isClient){
Template.Posts.onCreated(function() {
var self = this;
self.totalPosts = new ReactiveVar();
});
/*then in my case, when the user clicks the load more -button,
we call the postsTotal-method and set the returned value as
the value of the totalPosts-reactive variable*/
Template.Posts.events({
'click .load-more': function (event, instance){
Meteor.call('postsTotal', function(error, result){
instance.totalPosts.set(result);
});
}
});
}
Hope this helps someone (I recommend checking the link first). For template level subscriptions, I used this as my guide Discover Meteor - template level subscriptions. This was my first stacked-post and I am just learning Meteor, so please have mercy...:D
Ouch this post is old, anyway maybe it will help someone.
I had exactly the same issue. I managed to solve it with 2 simple lines...
Remember the :
handle = Meteor.subscribeWithPagination('posts', 10);
Well I used in client handle.loaded() and Posts.find().count(). Because when they are different it means that all the posts are loaded. So here is my code :
"click #nextPosts":function(event){
event.preventDefault();
handle.loadNextPage();
if(handle.loaded()!=Posts.find().count()){
$("#nextPosts").fadeOut();
}
}
I had the same problem, and using the publish-counts package didn't work with the subs-manager package. I created a package that can set a reactive server-to-client session, and keep the document count in this session. You can find an example here:
https://github.com/auweb/server-session/#getting-document-count-on-the-client-before-limit-is-applied
I'm doing something like this:
On cliente
Template.postCount.posts = function() {
return Posts.find();
};
Then you create a template:
<template name="postCount">
{{posts.count}}
</template>
Then, whatever you want to show the counter: {{> postCount}}
Much easier than any solution i have seen.