コメント機能

10

翻訳の進捗

本章で学ぶこと:

  • コメントの表示
  • コメント投稿フォームの追加
  • 今ある投稿のコメントだけを読み込む方法
  • postsへのコメント数をカウントするプロパティの追加
  • ソーシャルニュースサイトの目的はユーザーのコミュニティを作ることであり、 ユーザー同士が話し合える方法を提供しなければ、ユーザーコミュニティを作ることは難しいでしょう。 この章では、コメントを追加します!

    では、コメントを保存するための新しいコレクションを作って、 基本的なテストデータをコレクションに入れることから始めていきましょう。

    Comments = new Mongo.Collection('comments');
    
    lib/collections/comments.js
    // 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)
      });
    }
    
    server/fixtures.js

    新しく作ったコレクションにパブリッシュとサブスクリプションをすることを忘れずに。

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function() {
      return Comments.find();
    });
    
    server/publications.js
    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
      }
    });
    
    lib/router.js

    コミット 10-1

    Added comments collection, pub/sub and fixtures.

    この固定データ読み込みを実行するために、データベースをクリアするmeteor resetを使う必要があります。 リセット後に、新しいアカウントを作ってログインし直すことを忘れずに!

    最初に、私たちは(完全に偽物の)二人のユーザーを作りました。そして、 データベースに二人のデータを挿入して、その後でデータベースからデータを選択するためにidを使います。 それから私たちは最初の投稿にユーザーがお互いにコメントを追加しました。投稿にコメントをリンクして(postIdを使って) この時は、postIdを使って投稿とuserIdを使ってユーザーへのコメントにリンクしています。 私たちは非正規化したフィールドであるauthorと一緒に投稿日時と本文を各コメントに追加しました。 

    また、私たちはルーターを増やしてコメントと投稿を待つようにしました。

    コメントを表示する

    データベースにコメントを入れるのは良いのですが、私たちはディスカッションページにコメントを表示する必要があります。 そろそろこのプロセスに慣れてきているころでしょうか。 というのも、あなたはこのステップに関連したアイデアをすでに持っているからです。

    <template name="postPage">
      {{> postItem}}
    
      <ul class="comments">
        {{#each comments}}
          {{> commentItem}}
        {{/each}}
      </ul>
    </template>
    
    client/templates/posts/post_page.html
    Template.postPage.helpers({
      comments: function() {
        return Comments.find({postId: this._id});
      }
    });
    
    client/templates/posts/post_page.js

    私たちは投稿のテンプレート内に{{#each comments}}ブロックを置いたので、thiscommentsヘルパー内の投稿です。 関連するコメントを見つけるために、私たちはpostId属性を使って投稿にリンクされたコメントをチェックします。   ヘルパーとSpacebarsを学習したことを考えると、コメントをレンダリングすることはかなり簡単です。 すべてのコメントの情報を格納するために、私たちはtemplates内に新しくcommentsディレクトリを作りました。

    <template name="commentItem">
      <li>
        <h4>
          <span class="author">{{author}}</span>
          <span class="date">on {{submittedText}}</span>
        </h4>
        <p>{{body}}</p>
      </li>
    </template>
    
    client/templates/comments/comment_item.html

    それでは、よりユーザーフレンドリーなフォーマットにsubmitted日付をフォーマットするため、 手早くテンプレートヘルパーを設定しましょう:

    Template.commentItem.helpers({
      submittedText: function() {
        return this.submitted.toString();
      }
    });
    
    client/templates/comments/comment_item.js

    それから、それぞれの投稿にコメントの数を表示します。:

    <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>
    
    client/templates/posts/post_item.html

    そして、commentsCountヘルパーを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();
      }
    });
    
    client/templates/posts/post_item.js

    コミット 10-2

    Display comments on `postPage`.

    これで、固定データのコメントを表示して見ることができるはずです:

    Displaying comments
    Displaying comments

    コメントをサブミットする

    ユーザーが新しいコメント作るための方法を追加します。 私たちがたどるプロセスはユーザーが新しい投稿を作成する許可する方法にとても似ています。 

    私たちは各投稿の下にコメント投稿ボックスを作ることから始めていきましょう。 

    <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>
    
    client/templates/posts/post_page.html

    それからコメントフォームテンプレートを作ります。:

    <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>
    
    client/templates/comments/comment_submit.html
    The comment submit form
    The comment submit form

    コメントをサブミットするために、投稿をサブミットするためにしたものと同様の方法で、 comment_submit.jscommentメソッドを呼び出します。:

    Template.commentSubmit.created = 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('');
          }
        });
      }
    });
    
    client/templates/comments/comment_submit.js

    以前、postをサーバーサイドのMeteorメソッドをセットアップしたように、 コメントを作るためにcommentMeteorメソッドをセットアップして、 すべてがちゃんとしているかチェックして、最後に新しいコメントをcommentsコレクションに挿入します。

    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);
      }
    });
    
    lib/collections/comments.js

    コミット 10-3

    Created a form to submit comments.

    ここでは手が込んだことは何もしていません。 ただユーザーがログインしているか、コメントに本文があるか、コメントが投稿にリンクされているかチェックしています。

    The comment submit form
    The comment submit form

    コメントのサブスクリプションの操作

    今のところ、私たちはすべての投稿にすべてのコメントをパブリッシュしています。 これはちょっと無駄が多いように見えます。 結局、私たちはどんなときでもコメントデータの小さな一部分を使うだけなので、 どのコメントをパブリッシュさせるかコントロールするために パブリケーションとサブスクリプションを改善していきましょう。  

    この点を考えると、commentsのパブリケーションにサブスクライブする必要のある時というのは、 ユーザーが投稿の個別ページにアクセスする時だけなので、 私たちは特定の投稿に関係するコメント部分だけを読み込む必要があります。

    最初のステップとして、私たちはコメントにサブスクライブする方法を変えていきます。 今まで私たちはルーターレベルでサブスクライブしていました。 つまり、私たちはルーターが初期化するときにすべてのデータを一度に読み込んでいます。

    しかし、私たちはサブスクリプションがパスのパラメータで決めるようにしたいのです。 パラメータはどの時点でも明確に変えることができます。 そのため、私たちはサブスクリプションコードをルーターレベルからルートレベルに 移行する必要があります。

    これは別の結果です:アプリを初期化する時にデータをロードするかわりに、 今の私たちはルートにヒットした時はいつもロードしています。 これはアプリ内を見ている間にロード時間またせてしまいます、 しかし、これはあなたが永遠にすべてのデータセットをフロント側にもたせるつもりでない限り、 避けられないマイナス面です。

    最初に、私たちはconfigureブロックでMeteor.subscribe('comments')を削除して コメントをあらかじめ読み込むことをストップします(前の状態に戻すと言い換えてもいいでしょう) :

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return Meteor.subscribe('posts');
      }
    });
    
    lib/router.js

    それから、新たにルートレベルのwaitOn関数をpostPageルートへ追加します。

    //...
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      waitOn: function() {
        return Meteor.subscribe('comments', this.params._id);
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    //...
    
    lib/router.js

    私たちはthis.params._idをサブスクリプションへの引数として渡していることにお気づきかと思います。 そのため、現在の投稿に帰属しているコメントへのデータセットを制限するために新しい情報を使いましょう。:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function(postId) {
      check(postId, String);
      return Comments.find({postId: postId});
    });
    
    server/publications.js

    コミット 10-4

    Made a simple publication/subscription for comments.

    1つだけ問題があります:ホームページに戻るときに、すべての投稿でコメントは0だと言っています。

    Our comments are gone!
    Our comments are gone!

    コメントのカウント

     この理由はすぐに明らかになります: 唯一postPageルート上でコメントを読み込むので、 commentsCountヘルパー内でComments.find({postId:this._id})を呼び出すときに Meteorは私たちを提供するために必要なクライアント側のデータを見つけることができません。

    この問題に対処する最も良い方法は、投稿にコメントの数を非正規化することです。 (これが何を意味しているのか自信がなくでも、問題ありません。次の補足事項でカバーします!) これまで見てきたように、コードにちょっとだけ複雑なものを追加していますが、 投稿リストを表示する上で、すべてのコメントにパブリッシュする必要がなくなったことで、 私たちが得るパフォーマンスベネフィットには、それだけの価値があります。

    postデータ構造にcommentsCountを追加することで これを実現します。 はじめに、私たちは投稿のテストデータをアップデートします。 (さらに、これをリロードするために meteor reset をします。後でユーザーアカウントを作り直すことを忘れずに。):

    // 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),
        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
      });
    }
    
    server/fixtures.js

    固定ファイル(fixtures.js)を更新するときにいつものように、 データベースをmeteor resetして再実行される事を確認する必要があります。

    次に、すべての新しい投稿が0コメントからスタートするようにします。: 

    //...
    
    var post = _.extend(postAttributes, {
      userId: user._id,
      author: user.username,
      submitted: new Date(),
      commentsCount: 0
    });
    
    var postId = Posts.insert(post);
    
    //...
    
    collections/posts.js

    それから、MongoDBの$inc演算子を使うことで新しいコメントを作る時に、 関係するcommentsCountを更新します。(これで数値に関するフィールドを1つずつ増加させます。): 

    //...
    
    comment = _.extend(commentAttributes, {
      userId: user._id,
      author: user.username,
      submitted: new Date()
    });
    
    // update the post with the number of comments
    Posts.update(comment.postId, {$inc: {commentsCount: 1}});
    
    return Comments.insert(comment);
    
    //...
    
    collections/comments.js

    最後に、postのフィールドに直接アクセスするため、単純にclient/templates/posts/post_item.jsからcommentsCount helperを削除します。

    コミット 10-5

    Denormalized the number of comments into the post.

    これでユーザー同士で話し合えるようになったので、ユーザーが新しいコメントに気づかないとしたら、 残念なことです。なんと驚くことに、次の章はこれを防ぐために通知を実装する方法を説明します!。