Adding an array object to Users collection in Meteor.js - meteor

I'm very new to Meteor.js and I'm finding the documentation a bit hard to understand.
I'm starting with a very simple app where Users will simply be allowed to add existing Games to their profile by clicking a button. The Games are stored in another Meteor Collection.
In rails I would just create a has_and_belongs_to_many relationship but that isn't how Meteor works. I thought the best way would be to add an empty array when the user's account is created - then, when they click the "add game" button it would pass the game's title into the users array.
I have this in my /server/users.js file:
Accounts.onCreateUser(function(options, user){
user.games = [];
return user;
});
Meteor.methods({
addGame: function(title) {
Meteor.users.update(Meteor.userId(), { $addToSet: { games: title}});
}
});
And I'm making a call to the addGame method in my /client/views/games/games_list.js file as such:
Template.gamesList.events({
'click .add-to-chest-btn': function(e){
var title = $(e.target).attr('name');
e.preventDefault();
Meteor.call('addGame', title, function(title){ console.log(title)});
}
});
Am I on the right track or is there a better way to do this?

You're on the right track, but do declare an array instead of an object:
Accounts.onCreateUser(function(options, user){
user.games = [];
return user;
});
Push the value directly instead of an object, and use $addToSet to avoid duplicates in case you push the same gameId multiple times:
Meteor.methods({
addGame: function(gameId) {
Meteor.users.update(Meteor.userId(), { $addToSet: { games: gameId }});
}
});

Related

Why is this collection empty? (Collection.find() is successful om server)

Ok this is what I've got.
The collection called Posts has content and I want to publish this under the name Merchs, the find() in the publish-function finds data but that is not shared to the client where Merchs is always empty.
//shared
Merchs = new Meteor.Collection('merchs');
// Posts has data I want to publish as "Merchs"
this.Posts = new Meteor.Collection('posts');
//server
Merchs.allow({
insert: function(userId, doc) {
return true;
},
update: function(userId, doc, fields, modifier) {
return true;
},
remove: function(userId, doc) {
return true;
}
});
Meteor.publish('merchs', function(data) {
return Posts.find();
});
//client
Deps.autorun( function() {
Session.get('selectedCategories');
subs.subscribe('merchs');
});
When creating your collection, the name in parentheses should be the name of the Mongo collection.
Merchs = new Meteor.Collection('merchs');
Should be:
Merchs = new Mongo.Collection('Posts');
That is, unless you already have a Posts variable defined in code that you didn't show. If you've already defined Posts and you're just looking to make another subscription to the same collection then you don't need this line at all:
Merchs = new Meteor.Collection('merchs');
You also don't need your allow() method (you can just use the one defined for Posts). All you need is the publish() method that you defined.
On the client side you also need:
Meteor.subscribe('merchs');
Also note the use of Mongo.Collection instead of Meteor.Collection which was renamed in Meteor 0.9.1.
You might want to read this excellent answer regarding publish/subscribe: https://stackoverflow.com/a/21853298/4665459

Updating client view in MeteorJS application

I'm new to MeteorJS and there is something that I'm not understanding with subscriptions and publications. I have a tablets collections. This is my setup with autopublish removed.
Server/publictions.js
Meteor.publish('tablets', function() {
return Tablets.find({}, {sort: {manufacturer: 1}});
});
In Client/view.js
Meteor.subscribe('tablets');
Template.tabletsList.helpers({
tablets: function() {
return Tablets.find();
}
});
Then in client/view.html
{{#each tablets}}
{{> tabletPreview}}
{{/each}}
This all works fine and I can see my tablets. But now I have a search box and when the search is submitted I want to update the tablets view to only show the search results.
I have a events handler but can't figure out how to update the tablets to only show the search results as the below code doesn't work. Should I use a session instead or have a totally new view.
client.view.js
Template.tabletsList.events({
"click .search": function (event, template) {
var query = $('input[name=search]').val();
Template.tabletsList.tablets = Tablets.find({manufacturer: query}, {sort: {manufacturer: 1}});
}
});
Here's the approach:
Add a reactive variable to your template on creation which holds the user's last search.
When the user updates the search, you should update the reactive variable.
Use the reactive variable in your helper to fetch only the matching documents.
Here is one possible implementation:
Template.tabletsList.created = function() {
// add a reactive variable to this template to track the search
this.search = new ReactiveVar('');
};
Template.tabletsList.events({
'click .search': function (event, template) {
var query = $('input[name=search]').val();
// update the search based on the user's input
template.search.set(query);
}
});
Template.tabletsList.helpers({
tablets: function() {
// read the user's last search (if any)
var search = Template.instance().search.get();
// sort options
var options = {sort: {manufacturer: 1}};
if (search.length) {
// find all tablets matching the search expression
var re = new RegExp(search);
return Tablets.find({manufacturer: re}, options);
}
else {
// if the user didn't input a search just find all tablets
return Tablets.find({}, options);
}
}
});
Notes:
See this post for more details on scoped reactivity.
Sorting in a publish doesn't really do anything for you in most cases. See my article on common mistakes.

Meteor: Subscription doesn't work

I'm trying to get a document from the server and display it on the client but the subscription always return a collection with no document.
// server/publications.js
Meteor.publish('myPages', function() {
return Pages.findOne({userId: this.userId});
});
// collection/pages.js
MyPages = new Meteor.Collection('myPages');
// client/main.js
Meteor.subscribe('myPages');
// client/view.js
Template.myView.helpers({
myPages: function(e, t) {
console.debug(MyPages.find({}));
return MyPages.find({});
}
});
You cannot move a document between collections via a subscription. If you subscribe to get a document that's in Pages collection, defined as new Meteor.Collection("pages"), then no matter how your pubsub channels look like, on the client the document will be found in the collection defined as new Meteor.Collection("pages"). So remove all traces of MyPages and use Pages on the client as well. You'll find the document there.
I don't think you can use findOne to publish collections: it doesn't return a cursor but an actual object.
Does this not work?
Meteor.publish('myPages', function() {
return Pages.find({userId: this.userId});
});
or, if necessary:
Meteor.publish('myPages', function() {
return Pages.find({userId: this.userId}, {limit: 1});
});

Server side reactive Meteor publication

Assume the following current case:
I do have a collection "Tables"
A table is an object with properties like {private:0, private1,private2,…} (seats 0,1,2…)
I publish the collection with 2 arguments, one the tableId the second one the seat.
Given the seat, the publication will filter (hide) properties the client must not be able to see.
For now the tableID and seat were taken from the client's session so everything was reactive.
I have a "takeSeat(seatNb)" method. If a client invoke this method and is allowed to seat at the table, the seat number is sent back to client which then put it into it's session under the seat key. This will therefore update the subscription and filter the table's seats content correctly.
I'm not satisfied by this design because I realised that the client might be cheating by subscribing to a seat by itself. Also (and more important) I'm using another DDP client in c++ and would like to keep this logic part in the server side. i.e. not to have to subscribe with another seat once I get one, if I do take a seat at a table I would like the server to show the right fields on the table by itself.
After several searches I decided to add a collection aside for "Players" so that I might easily get notified within my "tables" collection a "player" is added or removed to a table. But this is only half of the problem. I do have to actually change the handler of the publication itself so that the filter will become reactive. This is where I'm stuck, here is some simplified code to understand the case:
Meteor.publish("current-table", function(table_id)
{
var self = this;
var handle = Players.find({"tableID": table_id}).observeChanges(
{
added: function(id)
{
console.log("A player joined the table added");
self.changed("tables", table_id);
},
removed: function(id) {
console.log("A player left the table");
self.changed("tables", table_id);
}
});
self.onStop(function() {
handle.stop();
});
// PUBLISH THE TABLE BUT HIDE SOME FIELDS BEFORE
var player = Players.findOne({"userID": this.userId, "tableID": table_id}) || {"seat": -1};
var seat = player.seat;
var privateFilter = {"private0": false, "private1": false, "private2": false, "private3": false};
delete privateFilter["private" + seat];
return Tables.find(table_id, {fields: privateFilter});
});
How to proceed ? Is there a more elegant way to achieve this ?
You could store the seat in the user's profile. Then your publication would watch for changes to the user's profile and adjust as appropriate.
For example:
Meteor.publish("current-table", function() {
var self = this;
var getFilteredTableForSeat = function(seat_id) {
// Get the table for the given seat_id, filtering the fields as appropriate
};
var handle = Meteor.users.find({_id: this.userId}).observeChanges({
changed: function (id, fields) {
if(fields.profile)
self.changed("tables", 'current-table', getFilteredTableForSeat(fields.profile.seat_id));
}
});
self.added("tables", 'current-table', getFilteredTableForSeat(Meteor.users.findOne(this.userId).profile.seat_id));
self.ready();
self.onStop(function() {
handle.stop();
});
});
If the user's seat changes then the current-table document of the Tables collection will update.
This example makes some assumptions, and will require adjustment if these aren't true for you:
You can find a table given a seat_id (if you can't, you may need to store the table id in the user's profile as well)
A seat_id always belongs to the same table (if it doesn't, you'll need to add a changed handler to wherever that information is stored)
The table information returned by the publication doesn't change (if it does, you'll need to add a changed handle to the Table collection, similar to the user handle)
Thanks to jrullmann's answer I decided to make a custom filtered publication using the reactivity of 2 collections. Here is my final code:
Meteor.publish("current-table", function(table_id)
{
var self = this;
function getFilteredTable()
{
var player = Players.findOne({"userID": self.userId, "tableID": table_id}) || {"seat": -1};
var seat = player.seat;
var privateFilter = {"prv": false, "prv0": false, "prv1": false, "prv2": false, "prv3": false};
delete privateFilter["prv" + seat];
return Tables.findOne(table_id, {fields: privateFilter});
}
var tableHandler = Tables.find(table_id).observeChanges(
{
added: function()
{
self.added('tables', table_id, getFilteredTable());
},
removed: function()
{
self.removed('tables', table_id);
},
changed: function()
{
self.changed('tables', table_id, getFilteredTable());
}
});
self.ready();
self.onStop(function() {
tableHandler.stop();
});
var handle = Players.find({"tableID": table_id}).observeChanges(
{
added: function(collection, id, fields)
{
self.changed('tables', table_id, getFilteredTable());
console.log("added");
},
removed: function(collection, id, fields)
{
// Little trick to avoid meteor use the former deleted (hidden) properties
self.removed('tables', table_id);
self.added('tables', table_id, getFilteredTable());
console.log("removed");
}
});
self.onStop(function() {
handle.stop();
});
});
I had a similar problem and wrote these two atmosphere packages which solve the problem:
https://atmosphere.meteor.com/package/server-deps
https://atmosphere.meteor.com/package/reactive-publish
Install the second package with meteorite, use "Meteor.reactivePublish" instead of "Meteor.publish" and it will automatically update when the results of any queries with the option {"reactive": true} change.
This example from the readme will publish precisely those items which the user's team can see, and will update when either the user changes team or the team's visible items change.
Meteor.reactivePublish(null, function() {
if (this.userId) {
var user = Meteor.users.findOne({_id: this.userId}, {reactive: true});
if (user.team) {
var team = Collections.teams.findOne({_id: user.team}, {reactive: true});
var visibleItems = _.compact(team.visibleItems);
return Collections.items.find({_id: {$in: visibleItems}});
}
}
});

Adding new users to a collection and having them render on a list automatically?

So im doing the leaderboard example on the meteor site but instead of the predefined data I start off with, I want to create a new name and score that automatically appears on the screen when someone creates an account, so at this point I get the name and the score on the screen only after I create an account and hit the refresh button on the browser, what do I want to do so that I don't have to hit the refresh button and the user login name and score automatically appears on the screen?
do I want to use deps.flush() or meteor.render somehow?
server.js
// newUser Method
Meteor.methods({
newUser: function() {
var user = Meteor.user();
userVar = {
name: user.username,
score: 0
};
Players.insert(userVar);
}
});
client.js
Deps.autorun(function() {
Meteor.call('newUser');
});
Template.leaderboard.players = function () {
return Players.find({}, {sort: {score: -1, name: 1}});
};
Template.leaderboard.selected_name = function () {
var player = Players.findOne(Session.get("selected_player"));
return player && player.name;
};
Template.player.selected = function () {
return Session.equals("selected_player", this._id) ? "selected" : '';
};
Template.leaderboard.events({
'click input.inc': function () {
Players.update(Session.get("selected_player"), {$inc: {score: 5}});
}
});
Template.player.events({
'click': function () {
Session.set("selected_player", this._id);
}
});
If your starting point is a working version of the example then you should be seeing reactive changes to the web page each time the Players collection changes. Deps.flush or Meteor.render are unnecessary.
The Deps.autorun() function you have is only called once when the client starts. At that point you may not have a user and your method will fail when you try to get a username from the null variable, 'user'.
To trigger the autorun each login and when you have a user you need it to refer to a reactive data source. If you rewrite it like this you should see a new player showing up each time a user logs in:
//on client
Deps.autorun( function(){
if ( Meteor.userId() ){
Meteor.call('newUser');
}
});
I also wonder if your method on the server will have a problem as this.userId is how I usually get the user information inside a method. Here is an alternative to avoid the method and just insert the player on the client:
//on client
Deps.autorun( function(){
var user = Meteor.user();
if ( user ) { //insert will run on login or any change in the user
var userVar = {
name: user.username,
score: 0
};
Players.insert(userVar);
}
});
So I assume then that "player" records belong to a user in some way? So when you create a new user, you create their new default player record?
Maybe you just need your helpers to check that a player record for the user exists, and if not, create it.
Template.leaderboard.players = function () {
var players = Players.find({/* Get players for this user */ }, {sort: {score: -1, name: 1}});
if(!players) {
players = /* Insert default player record */
}
return players;
};

Resources