Comentarios

10

Porcentaje completado

En este capítulo:

  • Mostraremos los comentarios existentes.
  • Añadiremos un formulario para enviar comentarios.
  • Aprenderemos cómo cargar solo los comentarios del post actual.
  • Añadiremos una propiedad contador de comentarios a los posts.
  • El objetivo de un sitio de noticias es crear una comunidad de usuarios, y será difícil hacerlo sin que puedan a hablar unos con otros. En este capítulo, vamos a agregar los comentarios.

    Empezaremos creando una nueva colección para almacenar los comentarios.

    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

    No olvidemos que debemos publicar y suscribir la nueva colección:

    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

    Commit 10-1

    Añadidos comentarios a la colección, pub/sub y datos de p…

    Ten en cuenta que para que se carguen los nuevos datos de prueba, es necesario ejecutar meteor reset. Después de la restauración, ¡no olvides crear una nueva cuenta y volver a entrar!

    En primer lugar, hemos creado un par de usuarios (inventados), insertándolos en la base de datos y usando los ids para seleccionarlos después en la base de datos. Luego añadimos un comentario de cada usuario al primer post, enlazando el comentario al post (con postId), y el usuario (con userId). Además, añadimos la fecha y el cuerpo de cada comentario, junto un campo denormalizado denominado author.

    Además, hemos extendido nuestro router para que espere a un array que contiene las dos colecciones, comentarios y posts.

    Mostrando comentarios

    Está bien tener comentarios en la base de datos, pero habrá que mostrarlos en la página de discusión. Este proceso ya nos debe ser familiar:

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

    Ponemos el bloque {{#each comments}} dentro de la plantilla del post, por lo que this es un post para el ayudante comments. Para encontrar los comentarios adecuados, buscamos los que están vinculados a ese post a través de postId.

    Con todo lo que hemos aprendido acerca de ayudantes y plantillas, sabemos que mostrar un comentario es bastante sencillo. Vamos a crear un nuevo directorio comments dentro de templates para almacenar toda la información acerca de los comentarios, y una nueva plantilla commentItem dentro:

    <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

    Vamos a crear rápidamente un ayudante de plantilla para dar a nuestra fecha de envío submitted un formato más amigable:

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

    A continuación, vamos a mostrar el número de comentarios de cada 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>
    
    client/templates/posts/post_item.html

    Y añadimos el ayudante commentsCount a 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

    Commit 10-2

    Mostrar los comentarios en `postPage`.

    Ahora deberíamos ser capaces de mostrar nuestros comentarios de prueba y ver algo como esto:

    Displaying comments
    Displaying comments

    Enviando comentarios

    Vamos a añadir una forma de que los usuarios puedan hacer nuevos comentarios. El proceso será bastante similar a como ya hemos hecho para permitir a los usuarios crear nuevos posts.

    Empezaremos añadiendo un área de envío en la parte inferior de cada post:

    <template name="postPage">
      <div class="post-page page">
        {{> postItem}}
        <ul class="comments">
          {{#each comments}}
            {{> commentItem}}
          {{/each}}
        </ul>
        {{#if currentUser}}
          {{> commentSubmit}}
        {{else}}
          <p>Please log in to leave a comment.</p>
        {{/if}}
      </div>
    </template>
    
    client/templates/posts/post_page.html

    Y a continuación, crear la plantilla del formulario para los comentarios:

    <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

    Para enviar comentarios, llamaremos a un método comment en el fichero comment_submit.js que funciona de forma similar a lo que hicimos para al enviar posts:

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

    Al igual que anteriormente establecimos un método post en el servidor, vamos a hacer lo mismo para crear comentarios, comprobar que todo está bien, y finalmente insertar el nuevo comentario dentro de su colección.

    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

    Commit 10-3

    Creado el formulario de envío de comentarios.

    Comprobamos que el usuario está conectado, que el comentario tiene cuerpo, y que está vinculado a un post.

    El formulario de envío de comentarios
    El formulario de envío de comentarios

    Controlando la suscripción a los comentarios

    Tal como están las cosas, publicamos todos los comentarios de todos los posts a todos los clientes conectados. ¿No estaremos derrochando recursos? Después de todo, en un momento dado solo usamos un pequeño subconjunto de estos datos. Así que vamos a mejorar nuestras publicaciones y suscripciones para controlar exactamente qué comentarios se publican.

    Si lo pensamos bien, el único momento en el que necesitamos suscribirnos a la publicación comments es cuando un usuario accede a la página de un post individual, y solo hay que cargar el subconjunto de comentarios relacionados con ese post en particular.

    El primer paso va a ser cambiar la forma de suscribirse a los comentarios. Hasta ahora, nos hemos estado suscribiendo a nivel router, lo que significa que cargamos todos nuestros datos una vez cuando este se inicializa.

    Pero ahora queremos que nuestra suscripción dependa de un parámetro de ruta, y, obviamente, este parámetro puede cambiar en cualquier momento. Así que tendremos que cambiar nuestro código de suscripción desde el nivel de router al nivel de ruta.

    Esto tiene otra consecuencia: en vez de cargar nuestros datos cuando se inicializa la aplicación, ahora los cargaremos cada vez que llegamos a una ruta concreta. Esto significa que ahora tendremos tiempos de carga mientras navegamos por la aplicación. Esto es un inconveniente inevitable a no ser que queramos cargar siempre todos los datos.

    Primero, dejaremos de pre-cargar todos los comentarios en el bloque configure, eliminando la línea Meteor.subscribe('comments') (dicho de otro manera, volvemos a lo que teníamos anteriormente):

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

    Y añadiremos una nueva función waitOn a nivel de ruta en la ruta 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

    Estamos pasando this.params._id como argumento a la suscripción. Así que, utilicemos esa nueva información para asegurarnos que limitamos el conjunto de datos a los comentarios que pertenecen al post actual:

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

    Commit 10-4

    Creado un mecanismo simple de publicación/suscripción par…

    Solo hay un problema: cuando volvemos a la página principal, todos nuestros mensajes tienen 0 comentarios:

    ¡Los comentarios han desaparecido!
    ¡Los comentarios han desaparecido!

    Contando comentarios

    La razón de que esto ocurra está bien clara: solo cargaremos comentarios en la ruta postPage, así que cuando llamamos a Comments.find({postId: this._id}) en nuestro ayudante commentsCount del gestor client/views/posts/post_item.js, Meteor no encuentra los datos necesarios en el lado del cliente para devolver un resultado.

    La mejor manera de resolver esto es denormalizar el número de comentarios dentro del post (si no sabes lo que significa denormalizar, no te preocupes, lo veremos en el próximo capítulo). Aunque, como veremos, hay que añadir un poco de complejidad a nuestro código, a cambio, mejoramos la velocidad al no tener que publicar todos los comentarios de la base de datos solo para contarlos.

    Lo conseguiremos añadiendo una propiedad commentsCount a la estructura de datos del post (y restableceremos Meteor con meteor reset - no olvides volver a crear una cuenta de usuario):

    // 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

    Como de costumbre cuando actualizamos el fichero de fixtures, deberás ejecutar meteor reset para inicializar la base de datos y asegurarnos que se ejecutan de nuevo los fixtures.

    Luego, nos aseguramos de que todos los nuevos posts empiezan con 0 comentarios:

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

    Y entonces actualizamos commentsCount cuando hacemos un nuevo comentario usando el operador $inc de Mongo (que incrementa campos numéricos):

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

    Finalmente, tenemos que eliminar el ayudante commentsCount de client/templates/posts/post_item.js, ya que el campo está disponible directamente en el post.

    Commit 10-5

    Denormalizando el número de comentarios.

    Ahora que los usuarios pueden hablar entre sí, sería una lástima que se perdieran los nuevos comentarios de otros usuarios. En el siguiente capítulo veremos cómo implementar ¡notificaciones!