I'm trying to publish a user profile. I have the following publish function in publish.js:
Meteor.publish("singleProfile", function ( profileId ) {
check(profileId, String);
return Meteor.users.find(profileId, { fields: { _id: 1, services: 1, profile: 1 }});
});
This is my route in router.js:
Router.route('/profile/:_id', {
name: 'profilePage',
template: 'appProfile',
onBeforeAction: function() {
var currentUser = Meteor.userId();
if(currentUser) {
this.next();
} else {
this.render("signin");
}
},
waitOn: function() {
this.response = Meteor.subscribe('singleProfile', this.params._id);
return this.response;
},
action: function() {
this.render('appProfile');
}
});
Question is, how do I access the profile details in the appProfile template? Do I need a template helper defined? Or do I need to modify this code?
You can use a template helper for this:
Template.appProfile.helpers({
users() {
return Meteor.users.find();
}
});
Then in your template:
...
{{#each users}}
{{profile.myProperty}} <!-- Renders the myProperty field of the profile. -->
{{/each}}
Related
I'm publishing users and a second collection called joboffers. The joboffers collection stores the userIds for employer and candidate. I would like to get the candidate name to display next to the joboffer, for an admin page, however nothing I do seems to work. All of the joboffers display but not the candidate name. In my console I just get object .
Path: publish.js
// publish all jobs for admin to view
Meteor.publish('allJobs', function () {
return JobOffers.find({});
});
Meteor.publish('allUsersWithJobs', function () {
var offers = JobOffers.find({}, {fields: {candidateUserId: 1}}).fetch();
var ids = _.pluck(offers, 'candidateUserId');
options = { fields: {username: 1, emails: 1, "profile.firstName": 1, "profile.familyName": 1 } };
return Meteor.users.find({ _id: { $in: ids } }, options);
});
Path: alljoboffers.js
Template.alljoboffers.onCreated(function() {
Meteor.subscribe("allJobs");
Meteor.subscribe("allUsersWithJobs");
});
Template.alljoboffers.helpers({
alljoboffers: function() {
return JobOffers.find({});
},
candidateName: function() {
console.log(this);
var user = this.userId;
var candidate = (user && user.profile && user.profile.firstName);
return candidate;
},
});
Path: alljoboffers.html
{{#each alljoboffers}}
{{positionTitle}}
{{#with candidateName}}
{{profile.firstName}}
{{/with}}
{{/each}}
You're mixing user id and user object. It's a good idea to create a global helper to help keep a convention on how you manage them. This will help reduce the confusion. For example:
Template.registerHelper('usernameById', function(userId) {
var user = Meteor.users.findOne({_id: userId});
return user && user.profile.firstName;
});
Then in your template:
{{#each alljoboffers}}
{{positionTitle}}
{{usernameById candidateUserId}}
{{/each}}
I have a meteor project where all my users have their own profile page setup in this way using routes:
Routes code:
Router.route('/#:username', {
name: 'profile',
controller: 'ProfileController'
});
ProfileController = RouteController.extend({
template: 'profile',
waitOn: function() {
return Meteor.subscribe('userProfile', this.params.username);
},
data: function() {
var username = Router.current().params.username;
return Meteor.users.findOne({
username: username
});
}
});
Server code:
Meteor.publish('users', function() {
return Meteor.users.find({}, {fields: {username: 1, emails: 1, profile: 1, roles: 1}});
});
Meteor.publish('userProfile', function(username) {
// Try to find the user by username
var user = Meteor.users.findOne({
username: username
});
// If we can't find it, mark the subscription as ready and quit
if (!user) {
this.ready();
return;
}
// If the user we want to display the profile is the currently logged in user
if(this.userId === user._id) {
// Then we return the curresonding full document via a cursor
return Meteor.users.find(this.userId);
} else {
return Meteor.users.find(user._id, {
fields: {
profile: 0
}
});
}
});
I want to do something similar with a pages collection that I've set up. Creating the collection works and the collection page has an _id field that is made upon creation.
Right now the program works nicely for users where mysite.com/# works. Now I want the same thing to work for mysite.com/&
I've basically attempted to do the exact same thing as I did in the above code with the user name but it wasn't working. I've checked to make sure my creation of the collection items are working and they are. But somehow I can't figure out how to do this same thing with collections since I'm relatively new to using routes.
This is what I've attempted:
Here's my routes:
var pageRoute = '/&:_id';
Router.route(pageRoute, {
name: 'page',
controller: 'PageController'
});
PageController = RouteController.extend({
template: 'page',
waitOn: function() {
return Meteor.subscribe('Page', this.params._id);
},
data: function() {
var _id = Router.current().params._id;
return Meteor.pages.findOne({
_id: _id
});
}
});
Server code:
Meteor.publish('pages', function() {
return Pages.find({});
});
Meteor.publish('Page', function(_id) {
// Try find the page by _id
var page = Meteor.pages.findOne({
_id: _id
});
// If we can't find it, mark the subscription as ready and quit
if (!page) {
this.ready();
return;
}
// If the page we want to display is not claimed, display it
if(true) {
return Meteor.pages.find(this._id);
} else {
// Redirect to the page
}
});
The Schema of the Page Collection:
_id: ,
createdAt: ,
CreatedBy: ,
claimedAt: ,
claimedBy: ,
Update:
I've scoped it down to this problem, I get the following error in the console server-side:
I20160202-11:16:24.644(2)? Exception from sub qrPage id 2kY6RKCTuCpBDbuzm TypeError: Cannot call method 'findOne' of undefined
I20160202-11:16:24.645(2)? at [object Object].process.env.MAIL_URL [as _handler] (server/ecclesia.life_server.js:40:33)
I20160202-11:16:24.645(2)? at maybeAuditArgumentChecks (livedata_server.js:1698:12)
I20160202-11:16:24.645(2)? at [object Object]._.extend._runHandler (livedata_server.js:1023:17)
I20160202-11:16:24.645(2)? at [object Object]._.extend._startSubscription (livedata_server.js:842:9)
I20160202-11:16:24.646(2)? at [object Object]._.extend.protocol_handlers.sub (livedata_server.js:614:12)
I20160202-11:16:24.646(2)? at livedata_server.js:548:43
This error occurs whenever I try to direct to mysite.com/&<_id>
Based on this website: https://perishablepress.com/stop-using-unsafe-characters-in-urls/
It looks like # is considered an unsafe character to use in a URL string. On the web page above, it looks like there are several symbols you could use instead as safe characters.
I just tried this on my own machine, and I don't think Meteor plays nicely when the # is introduced in the URL.
This got it working...
Publications:
Meteor.publish('qrpages', function() {
return QRPages.find({});
});
Meteor.publish('qrPage', function(id) {
// Try find the qrpage by _id
var qrpage = QRPages.find({_id: id});
// If we can't find it, mark the subscription as ready and quit
if (!qrpage) {
this.ready();
return;
}
return qrpage;
});
Routes:
var qrpageRoute = '/$:_id';
Router.route(qrpageRoute, {
name: 'qrpage',
controller: 'QRController'
});
QRController = RouteController.extend({
template: 'qrpage',
waitOn: function() {
var id = this.params._id;
return Meteor.subscribe('qrPage', id);
},
data: function() {
var id = this.params._id;
return QRPages.findOne({
_id: id
});
}
});
I am building a forum with Meteor and would like to display comments to answers. And the answers are on the same page as an individual question. I am able to save the comments once submitted (I can check the mongo database and I see that I am successfully submitting comments), but I'm not able to display them using templates. I would think the problem has something to do with publications and subscriptions, but I can't locate the error. Below are the relevant snippets of code.
answer_item.js
Template.answerItem.helpers({
submittedText: function() {
return this.submitted.toString();
},
comments: function() {
return Comments.find({answerId: this._id});
}
});
answer_item.html
<template name="answerItem">
...
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
...
</template>
comment_item.html
<template name="commentItem">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submitted}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>
comment_item.js
Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});
lib/collections/comment.js
Comments = new Mongo.Collection('comments');
Meteor.methods({
commentInsert: function(commentAttributes) {
check(this.userId, String);
check(commentAttributes, {
answerId: String,
body: String
});
var user = Meteor.user();
var answer = Answers.findOne(commentAttributes.answerId);
if (!answer)
throw new Meteor.Error('invalid-comment', 'You must comment on an answer');
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
Answers.update(comment.answerId, {$inc: {commentsCount: 1}});
comment._id = Comments.insert(comment);
return comment._id
}
});
router.js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
QuestionsListController = RouteController.extend({
template: 'questionsList',
increment: 5,
questionsLimit: function() {
return parseInt(this.params.questionsLimit) || this.increment;
},
findOptions: function() {
return {sort: this.sort, limit: this.questionsLimit()};
},
subscriptions: function() {
this.questionsSub = Meteor.subscribe('questions', this.findOptions());
},
questions: function() {
return Questions.find({}, this.findOptions());
},
data: function() {
var self = this;
return {
questions: self.questions(),
ready: self.questionsSub.ready,
nextPath: function() {
if (self.questions().count() === self.questionsLimit())
return self.nextPath();
}
};
}
});
NewQuestionsController = QuestionsListController.extend({
sort: {submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.newQuestions.path({questionsLimit: this.questionsLimit() + this.increment})
}
});
FollowedQuestionsController = QuestionsListController.extend({
sort: {follows: -1, submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.followedQuestions.path({questionsLimit: this.questionsLimit() + this.increment})
}
});
Router.route('/', {
name: 'home',
controller: NewQuestionsController
});
Router.route('/new/:questionsLimit?', {name: 'newQuestions'});
Router.route('/followed/:questionsLimit?', {name: 'followedQuestions'});
Router.route('/questions/:_id', {
name: 'questionPage',
waitOn: function() {
return [
Meteor.subscribe('singleQuestion', this.params._id),
Meteor.subscribe('answers', this.params._id),
Meteor.subscribe('comments', this.params._id)
];
},
data: function() { return Questions.findOne(this.params._id); }
});
Router.route('/questions/:_id/edit', {
name: 'questionEdit',
waitOn: function() {
return Meteor.subscribe('singleQuestion', this.params._id);
},
data: function() { return Questions.findOne(this.params._id); }
});
Router.route('/submit', {name: 'questionSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'questionPage'});
Router.onBeforeAction(requireLogin, {only: 'questionSubmit'});
server/publications.js
Meteor.publish('comments', function(answerId) {
check(answerId, String);
return Comments.find({answerId: answerId});
});
It looks like you need to run a separate query in your comment publication, to get a list of all of the answers for the given question, then use the results of that query to get a list of all the comments for all of the answers.
Meteor.publish('comments', function(questionId) {
check(questionId, String);
var answerIds = _.pluck(Answers.find({'questionId': questionId}, {fields: {_id: 1}}).fetch(), '_id');
return Comments.find({answerId: {$in: answerIds});
});
EDIT
I have a similar feature within an app that I'm working on now, with the same issue you were running into. I spent a few hours on it yesterday and came to the conclusion that the issue has to do with the fact that the _.pluck statement converts the results from the Answers cursor to an array, which prevents the publish function from being reactive.
After looking into several solutions the best one I found was the publish composite package. The syntax is a little verbose, but it gets the job done. To make it all work properly you need to merge all of the publish functions for the question, answers, and comments all into one publish function. Under the covers it creates observeChanges watchers on each of the answers under the question, so it can be reactive.
Meteor.publishComposite('question', function(questionId) {
return {
find: function() {
return Questions.find({_id: questionId});
},
children: [
{
find: function(question) {
return Answers.find({questionId: question._id});
},
children: [
{
find: function(answer, question) {
return Comments.find({answerId: answer._id});
}
}
]
}
]
}
});
I am fairly new to Meteor.
I have a template helper function which requires to work with data I am publishing from the server side. When the page loads, the value for profile below is undefined. But after that when I execute the same code snippet from browser's console, it works just fine. From what I understand, the template helper is being executed before the data is published. How can I wait until the data is published to run the UI helper?
Here is relevant code.
Helper Function
Template.header.helpers({
me: function() {
var user = Meteor.users.findOne(Meteor.userId());
if (user) {
return Profiles.findOne({
spid: user.profile.spid
});
}
return null;
}
});
HTML Template
<template name="header">
<header class="ui fixed inverted menu">
{{> thumb user=me}}
</header>
</template>
Thumbnail Template
<template name="thumb">
{{#with user}}
<div class="thumb">
<div class="text" style="background-color:{{color}}">
{{initials name}}
</div>
<div class="pic" style="background-image:url('{{pic}}')"></div>
</div>
{{/with}}
</template>
Publications
Meteor.publish('profiles', function() {
return Profiles.all();
});
Meteor.publish('departments', function() {
return Departments.all();
});
Meteor.publish('requestServiceIds', function() {
return Requests.getServiceIds();
});
Meteor.publish('relevantServices', function() {
return Services.getRelevant(this.userId, 5);
});
Router
Router.configure({
layoutTemplate: 'main',
waitOn: function() {
Deps.autorun(function() {
Meteor.subscribe('profiles', Partitioner.group());
Meteor.subscribe('departments', Partitioner.group());
});
}
});
Router.onBeforeAction(function() {
if (this.route.getName() === 'not-authorized') return this.next();
if (!Meteor.userId() || !Cookie.get('TKN')) {
this.render('login');
} else {
this.next();
}
});
Router.route('/', {
name: 'home',
waitOn: function() {
Deps.autorun(function() {
Meteor.subscribe('requestServiceIds', Partitioner.group());
Meteor.subscribe('relevantServices', Partitioner.group());
});
}
});
---
UPDATE 1
I updated the the router a bit. But it did not seem to have had made any difference.
Router.configure({
layoutTemplate: 'main',
waitOn: function() {
// Deps.autorun(function() {
// Meteor.subscribe('profiles', Partitioner.group());
// Meteor.subscribe('departments', Partitioner.group());
// });
return [
Meteor.subscribe('profiles', Partitioner.group()),
Meteor.subscribe('departments', Partitioner.group())
];
}
});
Router.route('/', {
name: 'home',
waitOn: function() {
// Deps.autorun(function() {
// Meteor.subscribe('requestServiceIds', Partitioner.group());
// Meteor.subscribe('relevantServices', Partitioner.group());
// });
return [
Meteor.subscribe('requestServiceIds', Partitioner.group()),
Meteor.subscribe('relevantServices', Partitioner.group())
];
}
});
Create a dumb loading template such as this one (using font awesome):
<template name="loading">
<div class="loading">
<i class="fa fa-circle-o-notch fa-4x fa-spin"></i>
</div>
</template>
And try replacing your Router.configure() part with something like this:
Router.configure({
layoutTemplate: 'main',
action: function() {
if(this.isReady()) { this.render(); } else {this.render("loading");}
},
isReady: function() {
var subs = [
Meteor.subscribe('profiles', Partitioner.group());
Meteor.subscribe('departments', Partitioner.group());
];
var ready = true;
_.each(subs, function(sub) {
if(!sub.ready())
ready = false;
});
return ready;
},
data: function() {
return {
params: this.params || {},
profiles: Profiles.find(),
departments: Departments.find()
};
}
}
});
I guess that could be achieved using waitOn, but since your way does not work, I give you a working recipe I use everyday. Code inspired from meteor Kitchen generated code.
To answer your comments:
Regarding the Action not being triggered, it might be because we tried to put in inside the Router.configure. I don't do it that way, here are some details on how I implement it.
First, I set a controller for each route inside a router.js file (where I have my Router.configure() function too. It looks like that:
Router.map(function () {
this.route("login", {path: "/login", controller: "LoginController"});
// other routes
}
Next, I create a controller that I store in the same folder than my client view template. It looks like that:
this.LoginController = RouteController.extend({
template: "Login",
yieldTemplates: {
/*YIELD_TEMPLATES*/
},
onBeforeAction: function() {
/*BEFORE_FUNCTION*/
this.next();
},
action: function() {
if(this.isReady()) { this.render(); } else { this.render("loading"); }
/*ACTION_FUNCTION*/
},
isReady: function() {
var subs = [
];
var ready = true;
_.each(subs, function(sub) {
if(!sub.ready())
ready = false;
});
return ready;
},
data: function() {
return {
params: this.params || {}
};
/*DATA_FUNCTION*/
},
onAfterAction: function() {
}
});
So this action function is working when I extend a route using a controller. Maybe it will solve your issue.
For completeness sake, here is how my Router.configure() looks like:
Router.configure({
templateNameConverter: "upperCamelCase",
routeControllerNameConverter: "upperCamelCase",
layoutTemplate: "layout",
notFoundTemplate: "notFound",
loadingTemplate: "loading",
//I removed the rest because it is useless.
});
I have db with posts and all of them have bollean flagged
I have one template and navigation like (Read, Dont Read).
Problem is that I see all posts (when I must see posts with flagged false or flagged true), and I dont understand why, I think problem in publish/subscribe
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
});
AllPostsController = RouteController.extend({
template: 'timeTable',
waitOn: function() {
return Meteor.subscribe('allPosts');
}
});
readPostController = AllPostsController.extend({
waitOn: function() {
return Meteor.subscribe('readPosts');
}
});
dontreaderPostController = AllPostsController.extend({
waitOn: function() {
return Meteor.subscribe('dontreadPosts');
}
});
Router.map(function() {
this.route('timeTable', {path: '/',
controller: AllPostsController
});
this.route('readPosts', {path: '/read',
controller: readPostsController
});
this.route('dontreaderPosts', {
path: '/dontreader',
controller: dontreaderPostController
});
});
Meteor.publish('allPosts', function(){
return Posts.find({},{ sort: { createdAt: -1 }});
});
Meteor.publish('readPosts', function(){
return Posts.find({read:true},{ sort: { createdAt: -1 }});
});
Meteor.publish('dontreadPosts', function(){
return Posts.find({read:false},{ sort: { createdAt: -1 }});
});
If someone need more code, Just ask me
Anybody help
EDIT : David solved problem for regular tasks. Main problem that I have specific return Posts.find(...) in my tamplate helper.
<template name="timeTable">
{{#if posts_exist_week}}
{{> table posts=week}}
{{/if}}
{{#if posts_exist_month}}
{{> table posts=month}}
{{/if}}
</template>
<template name="table">
<table class="main-table table">
{{#each posts}}
{{> post}}
{{/each}}
</table>
</template>
You solved my problem if I did not have template timeTable (that show posts for last week and month) Because here it Template helper
Template.timeTable.helpers({
week: function() {
//...
return Posts.find({createdAt: {$gte: weekstart, $lt: yesterday}},{ sort: { createdAt: -1 }}); //return posts that was created in this week
},
month: function() {
//...
return Posts.find({createdAt: {$gte: monthstart, $lte: weekstart}},{ sort: { createdAt: -1 }});
}
});
And now you see that if I choose your decision (David) I will have 2 !!
return
first - in router
second - in template helper
I recreated this locally and found that extend causes the parent controller's waitOn to run. So whenever you go to the /read route it will actually activate both subscriptions and you'll end up with all of the documents on your client. A simple fix is to refactor your controllers like so:
PostController = RouteController.extend({
template: 'timeTable'
});
AllPostsController = PostController.extend({
waitOn: function() {
return Meteor.subscribe('allPosts');
}
});
readPostController = PostController.extend({
waitOn: function() {
return Meteor.subscribe('readPosts');
}
});
dontreaderPostController = PostController.extend({
waitOn: function() {
return Meteor.subscribe('dontreadPosts');
}
});
That being said, you don't want to build your app in a way that it breaks when extra subscriptions happen to be running. I would rewrite the controllers to select only the documents that pertain to them. For example:
dontreaderPostController = PostController.extend({
waitOn: function() {
return Meteor.subscribe('dontreadPosts');
},
data: {selector: {read: false}}
});
And now your helpers can use the selector like this:
Template.timeTable.helpers({
week: function() {
var selector = _.clone(this.selector || {});
selector.createdAt = {$gte: weekstart, $lt: yesterday};
return Posts.find(selector, {sort: {createdAt: -1}});
}
});
Also note that sorting in the publish functions may not be useful - see common mistakes.