anon
anon

Reputation: 2343

Unable to display data from database

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

Upvotes: 0

Views: 62

Answers (1)

Brian Shamblen
Brian Shamblen

Reputation: 4703

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

Upvotes: 1

Related Questions