Updating client view in MeteorJS application - meteor

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.

Related

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 an array object to Users collection in Meteor.js

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 }});
}
});

Detecting which reactive query was triggered

I have an probably not so unique issue of having a complicated meteor app.
I have several actions which are causing parts of the page to refresh which really aren't needed. But I'm having trouble locating which find() (or multiple find()'s ) is the one being triggered. I know the Collection in question, just not which find().
I could use observeChanges on every find I use, but that would be a lot of extra code.
Is there an easy way to see what is being triggered and by what?
Thanks!
Here is a render logging function you might find useful. It logs the number of times each template is rendered to the console. You know if a template is re-rendered after initial page load, it's because a reactive data source that it relies on has changed. Either this reactive data source could have been accessed in a helper method, or the template is a list item (i.e. inside an {{#each ...}} block helper) and a list item was added/moved/removed/changed. Also keep in mind that child templates call their parent's rendered callback when the child is rendered or re-rendered. So, this might confuse you into thinking the parent has actually been taken off the DOM and put back, but that's not true.
So, you can call this function at the end of your client code to see the render counts:
function logRenders () {
_.each(Template, function (template, name) {
var oldRender = template.rendered;
var counter = 0;
template.rendered = function () {
console.log(name, "render count: ", ++counter);
oldRender && oldRender.apply(this, arguments);
};
});
}
EDIT: Here is a way to wrap the find cursor to log all changes to a cursor to the console. I just wrote a similar function to this for a new package I'm working on called reactive-vision. Hopefully released soon.
var wrappedFind = Meteor.Collection.prototype.find;
Meteor.Collection.prototype.find = function () {
var cursor = wrappedFind.apply(this, arguments);
var collectionName = this._name;
cursor.observeChanges({
added: function (id, fields) {
console.log(collectionName, 'added', id, fields);
},
changed: function (id, fields) {
console.log(collectionName, 'changed', id, fields);
},
movedBefore: function (id, before) {
console.log(collectionName, 'movedBefore', id, before);
},
removed: function (id) {
console.log(collectionName, 'removed', id);
}
});
return cursor;
};
Thank you #cmather for the idea.
Her is Meteor 1.3 adapted and more advanced version of logRenders
// Log all rendered templates
// If filter is set, only templates in filter will be logged
// #params filter - name or names of template to filter
logRenders = function logRenders (filter) {
for (name in Object(Template)){
if (filter && !Array.isArray(filter)) filter = [filter];
var template = Template[name];
if (!template) continue;
if (filter && filter.indexOf(name) == -1){
// Clear previous logRenders
if ('oldRender' in template) template.rendered = template.oldRender;
delete template.oldRender;
continue;
}
var t = function(name, template){
if (!('oldRender' in template)) template.oldRender = template.rendered;
var counter = 0;
template.rendered = function () {
console.log(name, ++counter, this);
this.oldRender && this.oldRender.apply(this, arguments);
};
}(name, template);
};
};

Cannot access other users' email addresses in Meteor app

Before I begin, I have followed the steps here: Meteor Querying other users by email
And I have read the Meteor documentation about publishing users and how to add more fields than the id, username, and profile. My situation exists in spite of all of these things.
I'm trying to access other user's email addresses, beyond just the currently logged in user. I have 2 templates that need this access. The first template works and is able to access it. The second template is unable to.
Here is the setup code I have for publishing the emails field, and subscribing (I've also tried not specifying 'address' [e.g. fields: {emails: 1}] but that has the same result)
if (Meteor.isServer) {
Meteor.publish("allUsers", function () {
return Meteor.users.find({});
});
Meteor.publish("allUserData", function () {
return Meteor.users.find({}, {fields: {"emails.address": 1}});
});
};
if (Meteor.isClient) {
Meteor.subscribe("allUsers");
Meteor.subscribe("allUserData");
};
Here is the code from the template that works:
Template.createPartner.events({
'click .setup-partner' : function(event, template) {
var partner = Meteor.users.findOne({"emails.address": 'example#mail.com' }); <-- works
}
});
Here is the code from the template that doesn't work:
Template.infoSelect.partnerEmail = function() {
var partnerId = Meteor.user().profile.partnerId; <-- works
var partner = Meteor.users.findOne({_id: partnerId}); <-- works but only _id and profile are returned
return partner.emails[0].address; <-- throws exception because the 'emails' field doesn't exist
};
I've also tried this, but no difference:
var partner = Meteor.users.find({_id: partnerId}, {fields: {"emails.address": 1}});
Why can I not see the user's email address in the second template, but I can in the first?
I think its because you're subscribing to two sets of the same collection. Meteor uses the first subscription and ignores the second. I'm not sure why it works on one occasion though.
If you remove the first subscription and go with the second It should work, basically remove the line:
Meteor.subscribe("allUsers");
One more tip. You could alter your email function to:
Template.infoSelect.partner = function() {
var partnerId = Meteor.user().profile.partnerId; <-- works
var partner = Meteor.users.findOne({_id: partnerId}); <-- works but only _id and profile are returned
return partner;
};
And your handlebar would be : (it just opens up more options for your partner variable so you could reference him/her by name too)
<template name="infoSelect">
{{partner.email.0.address}}
{{partner.profile.name}} <!--If you have configured profiles -->
</template>

Resources