May 10, 2021 Meteor
The goal of social news sites is to create a community of users, which would be difficult without providing a way for people to communicate with each other. So in this chapter, let's add comments!
Let's start by creating a new set to store comments and adding some initial data to that set.
Comments = new Mongo.Collection('comments');
// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000)
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000)
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000)
});
}
Don't forget to publish and subscribe to our new collection:
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function() {
return Comments.find();
});
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
}
});
Note that in order to use the new data, you need to command
Meteor reset
clear the database.
Don't forget to create a new user and sign in again!
First, we created several (fake) users in the database and selected them
id
T
hen add a comment to the first post, linking to
postId
and the
userId
We've also added a submission date, comments, and a denormalized
author
item.
In addition, we add an array in the router that waits for a subscription with comments and posts.
Save comments to the database and display them on the comments page.
<template name="postPage">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
</template>
Template.postPage.helpers({
comments: function() {
return Comments.find({postId: this._id});
}
});
We put
{{#each comments}}
post template, so
comments
helper,
this
to the current post.
To find relevant comments, we can find
postId
to the post via the postId property.
After we've learned about helper and Spacebars, it's fairly simple to show a comment.
Under
templates
create a
comments
and
commentItem
template to store all comment-related information:
<template name="commentItem">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submittedText}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>
Let's write a template helper to help us
submitted
submission date format with the date format:
Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});
We'll then show the number of comments in each post:
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
Add
commentsCount
helper
post_item.js
Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
},
commentsCount: function() {
return Comments.find({postId: this._id}).count();
}
});
You should now be able to display the initial comments and see the following:
Let users create new comments, a process that will be very similar to in the past when we allowed users to create new posts.
First we add a submission box at the bottom of each post:
<template name="postPage">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
{{#if currentUser}}
{{> commentSubmit}}
{{else}}
<p>Please log in to leave a comment.</p>
{{/if}}
</template>
Then create a comment form template:
<template name="commentSubmit">
<form name="comment" class="comment-form form">
<div class="form-group {{errorClass 'body'}}">
<div class="controls">
<label for="body">Comment on this post</label>
<textarea name="body" id="body" class="form-control" rows="3"></textarea>
<span class="help-block">{{errorMessage 'body'}}</span>
</div>
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
</form>
</template>
Call
comment_submit.js
the message to
comment
a new comment, which is similar to the way you submitted a post in the past:
Template.commentSubmit.onCreated(function() {
Session.set('commentSubmitErrors', {});
});
Template.commentSubmit.helpers({
errorMessage: function(field) {
return Session.get('commentSubmitErrors')[field];
},
errorClass: function (field) {
return !!Session.get('commentSubmitErrors')[field] ? 'has-error' : '';
}
});
Template.commentSubmit.events({
'submit form': function(e, template) {
e.preventDefault();
var $body = $(e.target).find('[name=body]');
var comment = {
body: $body.val(),
postId: template.data._id
};
var errors = {};
if (! comment.body) {
errors.body = "Please write some content";
return Session.set('commentSubmitErrors', errors);
}
Meteor.call('commentInsert', comment, function(error, commentId) {
if (error){
throwError(error.reason);
} else {
$body.val('');
}
});
}
});
Similar to
post
server-side Meteor method, we'll establish a
comment
Meteor method to create a comment, check for correctness, and insert it into the comment collection.
Comments = new Mongo.Collection('comments');
Meteor.methods({
commentInsert: function(commentAttributes) {
check(this.userId, String);
check(commentAttributes, {
postId: String,
body: String
});
var user = Meteor.user();
var post = Posts.findOne(commentAttributes.postId);
if (!post)
throw new Meteor.Error('invalid-comment', 'You must comment on a post');
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
return Comments.insert(comment);
}
});
What's not too fancy here is just check to see if the user is signed in, the comment has a content, and links to a post.
As always, we'll post all comments to each connected client that belongs to all posts. I t seems a bit wasteful. A fter all, in any given period of time, only a small portion of that data is actually used. So let's improve the accuracy of publishing and subscribing to reviews.
If you think about it, we need
comments
time, when the user visits the page of a post, and at this point only needs to load a subset of the comments for that post.
The first step will change the way we subscribe to reviews. Currently, it is a router-level subscription, which means that when the router is initialized, all data is loaded.
But now hopefully our subscription depends on the path parameters, and the parameters can change at any time. Therefore, we need to change our subscription code from router level to path level.
Another consequence of this is that the data is loaded whenever the path is opened, rather than when the app is initialized. T his means that you wait for the load time while you browse within the program. Unless you intend to load all the contents to the client, this is an inevitable disadvantage.
First, stop
configure
all comments (in other words, go back to the previous method) by removing
Meteor.subscribe('comments')
in the configuration block:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return Meteor.subscribe('posts');
}
});
We'll add a new path-level waitOn
function
for
postPage
waitOn
//...
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return Meteor.subscribe('comments', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
//...
We
this.params._id
an argument to the subscription.
So let's use this new information to make sure that restricting comments belongs to the current post:
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
Here's a question: When we go back to the page, all posts are shown with 0 comments:
The reason for this problem is that we only load comments on
postPage
path, so when we call
Comments.find({postId: this._id})
Meteor
commentsCount
client data to provide us with a count value.
The best way to deal with this problem is to add a denormalized comment count to the post (if you don't understand what that means, don't worry, the next appendix will explain!). ) 。 As we'll see, the complexity in the code increases slightly, but in the All comments that never post the post list, the resulting performance improvement is worth it.
We'll
post
by adding a
commentsCount
to the post data structure.
First, update the post's test data
meteor reset
-- don't forget to re-create your account later):
// 测试数据
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000),
commentsCount: 2
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000),
commentsCount: 0
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000),
commentsCount: 0
});
}
As always, when you update the test data file, you must reset the database
meteor reset
to make sure it runs again.
Then we want to make sure that the comment count for all new posts starts at 0:
//...
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0
});
var postId = Posts.insert(post);
//...
When we create a new comment, update
$inc
operation (add one
commentsCount
//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
// 更新帖子的评论数
Posts.update(comment.postId, {$inc: {commentsCount: 1}});
return Comments.insert(comment);
//...
Finally, simply
client/templates/posts/post_item.js
because the value is available from the post.
commentsCount
Users can now talk to each other, and if they miss out on new comments, it will be unforgivable. The next sections will show you how to implement notifications to prevent this from happening!