Imagine you have multiple collections you want displayed on a single social news feed, for example Posts and Users (new signups). How would you reactively display both in a list sorted by creation date?
Idea 1 - merging in client
You publish the entities separately, and in the client, sort each of them by date, and merge them into a single sorted array, which you then display in an {{#each}} iterator. Problem: AFAIK this requires flattening the reactive cursors into static arrays, so now the page won't update. (Perhaps there's a way of making the page recalculate this array when any collection changes, salving this approach?)
Idea 2 - creating a new collection
You create a new collection, say FeedItems. When a new Post or User is created, you also create a new FeedItem and copy the relevant information into it. Displaying the items in the client is now very straightforward. Problem: Now there is no reactivity between the canonical objects and the FeedItem versions of them, so if someone changes their name, deletes a post, etc., this won't be reflected in the feed. (Perhaps there's a way of creating reactivity between collections to salvage this approach?)
Idea 3 - merging in the publication
Perhaps there's some way of sticking with the existing collections, but creating an additional 'newsFeed' publication which would somehow merge them. I haven't seen any way of doing this, however. I see in the docs that you can publish an array of collections, but AFAIK this is equivalent to publishing the same collections one at a time.
Is one of these approaches on the right track? Or is there another I haven't thought of?
By far the easiest solution is to merge them on the client in a template helper. For example:
Template.dashboard.helpers({
peopleAndPosts: function() {
var people = People.find().fetch();
var posts = Posts.find().fetch();
var docs = people.concat(posts);
return _.sortBy(docs, function(doc) {return doc.createdAt;});
}
});
and the template (assuming people and posts have a name):
<template name="dashboard">
<ul>
{{#each peopleAndPosts}}
<li>{{name}}</li>
{{/each}}
</ul>
</template>
This will be reactive because the helper is a reactive context so any changes to People or Posts will cause the returned array to be recomputed. Be aware that because you are not returning a cursor, any changes to either collection will cause the entire set to render again. This won't be a big deal if the length of the returned array is relatively short.
Idea 1 seems to work out of the box. I created a new meteor project and change it as follows:
test.js:
Users = new Meteor.Collection("users");
Posts = new Meteor.Collection("posts");
if (Meteor.isClient) {
Template.hello.array = function () {
var a = Users.find().fetch()
.concat(Posts.find().fetch());
return _.sortBy(a, function(entry) { return entry.votes; });
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
Users.insert({name: "user1", votes: 1});
Users.insert({name: "user2", votes: 4});
Users.insert({name: "user3", votes: 8});
Users.insert({name: "user4", votes: 16});
Posts.insert({name: "post1", votes: 2});
Posts.insert({name: "post2", votes: 4});
Posts.insert({name: "post3", votes: 6});
Posts.insert({name: "post4", votes: 8});
});
}
test.html:
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
{{#each array}}
<div>{{votes}} {{name}}</div>
{{/each}}
</template>
This gives the expected list:
1 user1
2 post1
4 user2
4 post2
6 post3
8 user3
8 post4
16 user4
Then I did Users.insert({name: "new", votes: 5} in the console and got (reactively):
1 user1
2 post1
4 user2
4 post2
5 new
6 post3
8 user3
8 post4
16 user4
I came across this problem and I was wondering if a mixed solution would be better (or at least more efficient):
The solution proposed by David works great but, as he says, problems could arise when dealing with big collections.
What about keeping a local collection (just in the client), merge both collections into that local one when template is created and then register an observer (or maybe an observer for each remote collection) to keep the local collection up to date?
Related
I'm fairly confused as to which is the best way of doing this, since I didn't manage to find a package that solves this easily and other answers of similar problems don't address this properly.
I need to have a template that lists days and display all the documents created on each single day, for example:
10/27/2015
- Document A
- Document B
10/26/2015
- Document C
- Document D
- Document E
Or, less vaguely:
10/23/2014
- John Smith received 10 points on Basquet
- Paul Atreides received 20 points on Future Telling
10/21/2014
- Heisenberg received 25 points on National Trade
- etc.
Being the displayed document something like a 'Reports' collection that joins 'Players' with 'Activities', for example.
What is the proper way of achieving this functionality?
I guess creating a Days Collection is not the best option.
Thanks in advance
You can just create an array of days in your template helper, iterate over that with {{#each}} then have another helper that returns a cursor of documents for each day.
html:
<template name="docsByDay">
{{#each days}}
Date: {{this}} <!-- 'this' will be an individual date ->
{{#each documents this}}
{{content}}
{{/each}}
{{/each}}
</template>
Replace {{content}} with whatever field(s) you want to display from your collection.
js:
Template.docsByDay.helpers({
days: function(){
var arrayOfDates =[];
// create your array based on the date range and interval you want
return arrayOfDates;
},
documents: function(d){
var start = Date(getFullYear(d),getMonth(d),getDate(d));
var end = Date(getFullYear(d),getMonth(d),getDate(d)+1);
return Documents.find({ date: { $gte: start, $lt: end }});
}
});
See javascript - get array of dates between 2 dates
I have built a small meteor app based on code generated by the excellent Meteor Kitchen project. This code works and renders the collection to the page, but there is one thing I am confused about.
A subset of the code is here:
router.js
this.route("articles", {path: "/articles", controller: "ArticlesController"});
ArticlesController
this.ArticlesController = RouteController.extend({
template: "Articles",
onBeforeAction: function() {
this.next();
},
action: function(){
if (this.isReady()) {
this.render();
} else {
this.render("loading");
}
},
isReady: function() {
var ready = true;
var subs = [ Meteor.subscribe('allArticles') ];
_.each(subs, function(sub) {
if(!sub.ready())
ready = false;
});
return ready;
},
data: function() {
return {
articles: Articles.find({})
};
}
});
server/pubs/articles.js
Meteor.publish('allArticles', function() {
return Articles.find({});
});
Meteor.publish('singleArticle', function(articleId) {
check(articleId, String);
return Articles.find({_id: articleId});
});
As I understand how this code is working, the following takes place:
(1) Collection is published via allArticles and singleArticle subscriptions
(2) ArticlesController subscribes to allArticles
(3) data function in the ArticlesController extracts the data (from the subscription?) to the articles array which is then exposed to the Blaze template.
Where I am confused:
Why do we need to do a Articles.find({}) in the data function? Where do we access the allArticles data ... it seems we are going back to the Articles collection directly and how is that possible if we have subscribed only to allArticles ?
While you don't show it, I'm assuming you have the following line in your code, defined somewhere that will execute it on both the server, and the client:
Articles = new Mongo.Collection('articles');
/*CollectionName = new Mongo.Collection('DBCollectionName');*/
Docs. When this is executed on the server a Collection is created, and assigned to the variable name Articles. 'articles' is the name used to store this collection in MongoDB.
When this is executed on the client, a mini mongo Collection is created. It initially will have no documents in it.
Next you have server only code:
Meteor.publish('allArticles', function() {
return Articles.find({});
});
Meteor.publish('singleArticle', function(articleId) {
check(articleId, String);
return Articles.find({_id: articleId});
});
Docs. This defines two publications, 'allArticles' and 'singleArticle'. These are not Collections themselves, but are rules that specify a set of data that the server will publish, and a client may subscribe to. While these two publications return data from the Server's Articles collection, publications can return data from one or more collections, or by directly using the underlying ddp protocol you can publish data that comes from another data source (not mongodb).
Next on the client you subscribe to a collection:
Meteor.subscribe('allArticles')
Docs. This call takes the name of a publication defined on the server ('allArticles'), and subscribes to it. The server then executes the publish function, and sends over ddp the set of data returned. This data is stored in the Client-side Mini Mongo Collection created above, and named Articles.
Also the server will monitor the Articles collection for changes, and if the resultset of the 'allArticles' publication changes, will send these changes as updates to the client.
So next you have the data function in your Controller (Client side).
data: function() {
return {
articles: Articles.find({})
};
}
Docs. This sets the data context for the render function.
The reason this calls Articles.find rather than allArticles.find is because allArticles is not a collection, but was instead the name of the publication the client used to request the server send data, that was stored in the clients mini mongo collection named Articles.
Where do we access the allArticles data ... it seems we are going back
to the Articles collection directly and how is that possible if we
have subscribed only to allArticles ?
You return this as part of your data object so that you can access it in your template. In your Articles template you can now use {{#each articles}} directly without a helper function because articles is part of your data context. You can also access the articles returned from your controllers data portion inside of your Articles template helpers by using this.articles.
Why do we need to do a Articles.find({}) in the data function?
These queries being performed in your controllers data function act on the clients minimongo Articles collection as opposed to the servers. Once the information is published from the server, and the client has subscribed to it, the client has this information available in their minimongo instance, but still needs to access it somehow. Basically, the publication makes the information available, but the Articles.find({}) accesses it for the client.
Accessing this information inside of the data function of your controller is simply to avoid doing it inside of your template.
I think that your misunderstanding comes from the third step that you have described:
data function in the ArticlesController extracts the data (from the
subscription?) to the articles array which is then exposed to the
Blaze template.
The data function extracts the data from minimongo on the client which contains the information from the subscription. Minimongo lies between the subscription and the data function.
I would need to know more about your app to answer the question. Are you viewing just a single article, or is there a page that lists them all?
If you are viewing a single article you would need to subscribe to the singleArticle publication.
If you were showing a list of articles, you would need to subscribe to allArticles. If there are a lot of articles, you could improve the speed of your app by limiting the number of fields with a query projection.
I'm working on a simple app where a User can follow other users. Users can star posts. And a user's feed is composed of posts that have been starred by users they follow. Pretty simple actually. However, this all gets complicated in Mongo and Meteor...
There are basically two way of modeling this that I can think of:
A user has a property, following, which is an array of userIds that the user follows. Also, a post has a property, starrers, which is an array of userIds that have starred this post. The good thing about this method is that publications are relatively simple:
Meteor.publish 'feed', (limit) ->
Posts.find({starrers: {$in: Meteor.users.findOne(#userId).following}}, {sort: {date: -1}, limit:limit})
We aren't reactively listening to who the user is following, but thats not too bad for now. The main problem with this approach is that (1) the individual documents will become large and inefficient if 1000000 people star a post. Another problem is that (2) it would be pain to keep track of information like when a user started following another user or when a user starred a post.
The other way of doing this is having two more collections, Stars and Follows. If a user stars a post, then we create a document with properties userId and postId. If a user follows another user, then we create a document with properties userId and followId. This gives us the advantage of smaller document sizes for Users and Posts, but complicated things when it comes to querying, especially because Mongo doesn't handle joins!
Now, I did some research and people seem to agree that the second choice is the right way to go. Now the problem I'm having is efficiently querying and publishing. Based on the Discover Meteor chapter about Advanced Publications, I created a publication that publishes the posts that are starred by user's followers -- sorted, and limited.
# a helper to handle stopping observeChanges
observer = (sub, func) ->
handle = null
sub.onStop ->
handle?.stop?()
() ->
handle?.stop?()
handle = func()
Meteor.publish 'feed', (limit) ->
sub = this
userId = #userId
followIds = null
eventIds = null
publishFollows = observer sub, () ->
followIds = {}
Follows.find({userId:userId}).observeChanges
added: (id, doc) ->
followIds[id] = doc.followId
sub.added('follows', id, doc)
publishStars()
removed: (id) ->
delete followIds[id]
sub.removed('follows', id)
publishStars()
publishStars = observer sub, () ->
eventIds = {}
Stars.find({userId: {$in: _.keys(followIds)}).observeChanges
added: (id, doc) ->
eventIds[id] = null
sub.added('stars', id, doc)
publishEvents()
removed: (id) ->
delete eventIds[id]
sub.removed('stars', id)
publishEvents()
publishEvents = observer sub, () ->
Events.find({_id: {$in: _.keys(eventIds)}}, {sort: {name:1, date:-1}, limit:limit}).observeChanges
added: (id, doc) ->
sub.added('events', id, doc)
changed: (id, fields) ->
sub.changed('events', id, fields)
removed: (id) ->
sub.removed('events', id)
While this works, it seems very limited at scale. Particularly, we have to compile a list of every starred post by every follower. The size of this list will grow very quickly. Then we do a huge $in query against all posts.
Another annoyance is querying for the feed on the client after we subscribe:
Meteor.subscribe("feed", 20)
posts = null
Tracker.autorun ->
followers = _.pluck(Follows.find({userId: Meteor.userId()}).fetch(), "followId")
starredPostIds = _.pluck(Stars.find({userId: {$in: followers}}).fetch(), "postId")
posts = Posts.find({_id: {$in: starredPostIds}}, {sort: {date: -1}, limit: 20}).fetch()
Its like we're doing all this work twice. First we do all the work on the server to publish the feed. Then we need to go through the exact same logic again on the client to get those posts...
My question here is a matter of design over everything. How can I efficiently design this feed based on followers staring posts? What collection / collection schemas should I use? How should I create the appropriate publication? How can I query for the feed on the client?
So it turns out that Mongo and "non-relational" databases simply aren't designed for relational data. Thus, there is no solution here with Mongo. I've ended up using Neo4j, but SQL would work fine as well.
meteor add reywood:publish-composite
Meteor.publishComposite('tweets', function(username) {
return {
find: function() {
// Find the current user's following users
return Relationships.find({ follower: username });
},
children: [{
find: function(relationship) {
// Find tweets from followed users
return Tweets.find({user: relationship.following});
}
}]
}
});
Meteor.publish('ownTweets', function(username) {
return Tweets.find({user: username});
});
I have an app where you can choose (or add if they don't exist!) a superhero/villain character from a certain universe on the first page; then outfit him with weapons, clothes, and gadgets on the second page (build).
I have this route defined:
Router.route('/build/:character', {
name: 'build'
waitOn: Meteor.subscribe('characters', {name: this.params.character})
//and a few other subscriptions and sessions as well for the items
//and stuff, but those don't matter here.
}
The link from the specific character, though, passes along a query as well:
<a href="{{pathFor 'build' query=this.universe}}">
So the final link could look something like this:
/build/Aquaman?DCComics
Now the page you are on will display a list of weapons and gadgets where you could also add other stuff if you so wish. Then you are supposed to drag the items you want to include onto your version of this hero.
Problem is, at this point the app doesn't know you even want to create your own hero. Maybe the user is just looking through them for fun. There's a button that the user has to click first to initialize the creating process, and that's when the actual _id is created, something like this:
Meteor.methods({
buildHero: function(heroCharacterName, heroUniverse) {
var heroToAdd = {}
heroToAdd['characterName'] = heroCharacterName
heroToAdd['universe'] = heroUniverse
heroToAdd['_createdAt'] = new Date()
CreatedHeroes.insert(heroToAdd, function() {
if (! error)
//Update the subscription somehow...
})
}
})
So, the _id that is created here in the new Collection must be passed along to a subscription somehow, because I don't want the user to see other personal heroes that have been created, only his own newly created one.
The solution I have in mind is adding the _id onto the URL in form of a hastag, and use this.params.hash in the subscription like so:
Router.route('/build/:character', {
name: 'build'
waitOn: [Meteor.subscribe('characters', {name: this.params.character}),
Meteor.subscribe('createdheroes', this.params.hash)]
}
First of all, is this a valid approach? If so, how do I accomplish it; how do I actually update the URL to include this hash?
If not, what would be a better approach?
I think you have to handle this logic in the data context or in a template helper and not in the way of subscribing/publishing.
If I was you I would besure that the newly created item is being published and subscribed by the client and modify your search query just that it only adds the newly created item.
I am not sure if I understand your question well but what I got, you will know the last _id which was used on your insert.
Instead of letting done this automatically by meteor, just use the meteor method to create / get that _id value >> see Meteor Documentation
var new_id = new Mongo.ObjectID()
col1.insert({ _id: new_id, ... });
col2.insert({ ..., ref_col1_id: new_id, ... });
I am currently working on a chat app in meteor. When first entering the room you get 25 messages initially. Now as new messages come into the page, that value should go up accordingly.
Now I so far I have a tried a couple different things, all not giving the desired result.
I have tried setting up a session variable on the client side that reactively re-subscribes to a given publish as the message count goes up. The problem with this route is this gives an adverse effect as new messages come in where all of the messages on the page need to reload because of the subscribe.
I have recently tried using the reactive-publish package with little luck in that the package has some various adverse effects of it's own.
What might be the best way to tackle this kind of problem? I am hoping there is a solution where I can set up some kind of publish that just streams in messages based on a numerical value that I in the database for each user.
EDIT: To add context
I am thinking something along the lines of
Meteor.publish 'messages', (roomId) ->
dl = // Some value that I pull from the database, which gets updated as new messages come into a room
Messages.find({room: roomId, type: "user_message"}, {sort: {time: -1}, limit: dl, fields: {_id: 1, name: 1, message: 1, room: 1, time: 1, type: 1}})
A huge amount of flexibility in Pub/Sub flexibility is achievable by using the low-level publications API - so much so that I've just written a blog post about it. It should make it pretty clear how you update variables when new documents appear in a query set.
It seems like you want each user to have a unique subscription based on the time that they enter the chat room, i.e. Meteor.subscribe("messages", roomId, new Date), that includes up to 25 messages from before they entered the room. Here's one option:
Meteor.publish("messages", function (roomId, time) {
var lastRecent = _.last(Messages.find({
room: roomId, type: "user_message"
}, {sort: {time: -1}, limit: 25}).fetch());
var cutoffTime = lastRecent && lastRecent.time || time;
return Messages.find({
room: roomId, type: "user_message", time: {$gt: cutoffTime}
});
});
If you want to successively add e.g. 25 old messages at a time whenever a user scrolls to the top of the chat window, consider that you may not actually need to "subscribe" to those older messages. You may be able to set up a method call like Meteor.call("getNOlderMessages", {roomId: roomId, N: 25, priorTo: oldestShownTime}) to fetch them, insert them into a local collection on the client (e.g. OlderMessages = new Meteor.Collection(null);), and then do something like:
<template="messages">
{{#each olderMessages}} {{> message}} {{/each}}
{{#each messages}} {{> message}} {{/each}}
</template>