Commenti

10

Percentuale Tradotta

In questo capitolo imparerai:

  • Visualizzare commenti esistenti.
  • Aggiungere un modulo di invio dei commenti.
  • Imparare come caricare i commenti del post corrente.
  • Aggiungere il totale dei commenti a un post.
  • Lo scopo di un sito di social news è creare una comunità di utenti, e sarebbe davvero impossibile farlo se questi utenti non avessero la possibilità di parlare tra di loro. Dunque, in questo capitolo, aggiungiamo i commenti!

    Iniziamo col creare una nuova collezione dove salvare i commenti, e aggiungendo un pò di dati di esempio a questa collezione.

    Comments = new Meteor.Collection('comments');
    
    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: now - 7 * 3600 * 1000
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: tom._id,
        author: tom.profile.name,
        submitted: now - 5 * 3600 * 1000,
        body: 'Interesting project Sacha, can I get involved?'
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: sacha._id,
        author: sacha.profile.name,
        submitted: 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: now - 10 * 3600 * 1000
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: now - 12 * 3600 * 1000
      });
    }
    
    server/fixtures.js

    Non dimentichiamoci di pubblicare e sottoscrivere la collezione che abbiamo appena creato:

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

    Commit 10-1

    Added comments collection, pub/sub and fixtures.

    È bene osservare che, perché il codice sopra sia eseguito, devi prima lanciare meteor reset per svuotare il tuo database. Dopo aver eseguito il reset, non dimenticare di creare un nuovo utente ed effettuare di nuovo il login!

    Prima di tutto, abbiamo creato un paio di (finti) utenti, li abbiamo inseriti nel database e abbiamo usato i loro ids per selezionarli dal database stesso subito dopo. Dopodichè, abbiamo aggiunto dei commenti al primo post, uno per ciascuno degli utenti, collegando il commento al post (con postId) e all'utente (con userId). Abbiamo anche aggiunto una data di inserimento e un testo per ciascun commento, insieme a un valore per author, un campo non normalizzato.

    Inoltre, abbiamo modificato in nostro router per attendere sulle collezioni comments e posts.

    Visualizzare i commenti

    È già un risultato l'aver inserito i commenti nel database, ma dobbiamo anche mostrarli nella pagina di discussione. La procedura dovrebbe ormai esserti familiare, e ti sei già fatto un'idea di quali sono i passi necessari:

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

    Inseriamo il blocco {{#each comments}} dentro al template post, così this si riferisce a un post dentro all'helper comments. Per selezionare quali commenti mostrare, filtriamo la collezione selezionando quelli collegati allo specifico post tramite l'attributo postId.

    Considerato quanto abbiamo già imparato riguardo agli helpers e ad Spacebars, visualizzare un commento è abbastanza semplice. Creiamo una nuova cartella comments dentro a views per salvare il template del dettaglio commenti:

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

    Creiamo un template helper per formattare la data (campo submitted) in modo che sia leggibile (a meno che tu non sia una di quelle persone che riesce a leggere con naturalezza timestamp UNIX e codici colore esadecimali?)

    Template.comment.helpers({
      submittedText: function() {
        return new Date(this.submitted).toString();
      }
    });
    
    client/views/comments/comment.js

    Mostriamo anche il numero totale di commenti per ciascun 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">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html

    E, per fare questo, aggiungiamo un helper chiamato commentsCount al template manager postItem:

    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/views/posts/post_item.js

    Commit 10-2

    Display comments on `postPage`.

    Dovresti essere ora in grado di visualizzare i commenti di esempio, qualcosa del genere:

    Visualizzazione dei commenti
    Visualizzazione dei commenti

    Inserire commenti

    Ora inseriamo la possibilità per i nostri utenti di creare nuovi commenti. La procedura che seguiremo sarà abbastanza simile a quella che abbiamo usato per la funzionalità di inserimento di nuovi post.

    Incominciamo con il creare il modulo di inserimento alla fine di ciascun post:

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

    E poi create il template del modulo di inserimento:

    <template name="commentSubmit">
      <form name="comment" class="comment-form">
        <div class="control-group">
            <div class="controls">
                <label for="body">Comment on this post</label>
                <textarea name="body"></textarea>
            </div>
        </div>
        <div class="control-group">
            <div class="controls">
                <button type="submit" class="btn">Add Comment</button>
            </div>
        </div>
      </form>
    </template>
    
    client/views/comments/comment_submit.html
    Il modulo di inserimento di commenti
    Il modulo di inserimento di commenti

    Per inserire il commento, chiameremo un metodo comment nel manager commentSubmit che funziona in maniera simile a quello nel manager postSubmit:

    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
        };
    
        Meteor.call('comment', comment, function(error, commentId) {
          if (error){
            throwError(error.reason);
          } else {
            $body.val('');
          }
        });
      }
    });
    
    client/views/comments/comment_submit.js

    Nello stesso modo in cui abbiamo precedentemente creato un Method meteor lato server chiamato post, ora ne creiamo uno chiamato comment per creare i commenti; controlliamo che tutto sia in regola, e inseriamo il nuovo commento nella collezione comments.

    Comments = new Meteor.Collection('comments');
    
    Meteor.methods({
      comment: function(commentAttributes) {
        var user = Meteor.user();
        var post = Posts.findOne(commentAttributes.postId);
        // ensure the user is logged in
        if (!user)
          throw new Meteor.Error(401, "You need to login to make comments");
    
        if (!commentAttributes.body)
          throw new Meteor.Error(422, 'Please write some content');
    
        if (!post)
          throw new Meteor.Error(422, 'You must comment on a post');
    
        comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
          userId: user._id,
          author: user.username,
          submitted: new Date().getTime()
        });
    
        return Comments.insert(comment);
      }
    });
    
    collections/comments.js

    Commit 10-3

    Created a form to submit comments.

    Non stiamo facendo nessun controllo particolare, solo verificando che l'utente sia autorizzato, che il commento non sia vuoto, e che sia collegato a un post.

    Ottimizzare la sottoscrizione Comments

    Allo stato attuale, Meteor manda tutti i commenti di tutti i post a tutti gli utenti collegati; ci sembra uno spreco, perché alla fin fine, ciascun utente usa solo una piccola parte di tutti questi dati (i commenti del post che sta visualizzando). Ottimizziamo la pubblicazione e la sottoscrizione così da controllare meglio quali commenti sono inviati.

    A pensarci bene, l'applicazione ha bisogno di sottoscrivere la pubblicazione comments solo quando un utente accede alla pagina che visualizza un post, e ha bisogno di caricare solo i commenti relativi a quel particolare post.

    Il primo passo sarà cambiare il modo in cui effettuiamo la sottoscrizione alla collezione comments. Finora, l'abbiamo effettuata a livello di router, il che significa che carichiamo tutti i dati quando il router viene inizializzato.

    Ma ora vogliamo che la nostra sottoscrizione dipenda dal particolare indirizzo della pagina che stiamo visualizzando, e tale indirizzo può ovviamente cambiare. Dunque dobbiamo spostare il codice che effettua la sottoscrizione dal livello di router a quello di route.

    Questo ha un'altra conseguenza: invece di caricare tutti i dati quando inizializziamo l'applicazione per la prima volta, questi verranno caricati quando la particolare route viene chiamata; questo significa che ora l'applicazione sarà più lenta in fase di navigazione (perché i dati vengono caricati man mano che servono), ma è inevitabile, a meno che non si vogliano precaricare tutti i dati all'avvio.

    Prima di tutto, eviteremo di precaricare tutti i commenti all'interno del blocco configure rimuovendo Meteor.subscribe('comments'):

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

    E aggiungeremo una nuova funzione waitOn a livello di route:

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

    Noterete che stiamo passando this.params._id come parametro della pubblicazione. Usiamo questo dato per far sì che i dati scaricati siano solo quelli riferiti al post corrente:

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

    Commit 10-4

    Made a simple publication/subscription for comments.

    Osserviamo ora un ultimo problema: quando torniamo alla pagina principale, l'applicazione dice che il nostro post ha 0 commenti:

    Tutti i commenti sono spariti!
    Tutti i commenti sono spariti!

    Contare i commenti

    La ragione di questo comportamento è semplice: noi carichiamo i commenti solo quando entriamo nella pagina di visualizzazione del post, dunque, quando viene eseguita l'istruzione Comments.find({postId: this._id}) nell'helper commentsCount del manager post_item, Meteor non riesce a trovare il dato da mostrare.

    La maniera migliore per risolvere il problema è denormalizzare, salvando il numero dei commenti dentro al post stesso (se non sei del tutto sicuro cosa significhi, non preoccuparti: questo argomento viene spiegato nel prossimo approfondimento). Anche se, come vedremo, il nostro codice diventerà un po’ più complesso, questo aumento di complessità sarà giustificato dall'aumento delle prestazioni dell'applicazione, per non dover pubblicare tutti i commenti già nella pagina che mostra i post.

    Dobbiamo dunque aggiungere una proprietà commentsCount alla struttura dati post. Per cominciare, aggiorniamo i nostri dati di esempio (e ricordiamoci di lanciare meteor reset - non dimenticare anche di ricreare il tuo utente, dopo!):

    var telescopeId = Posts.insert({
      title: 'Introducing Telescope',
      ..
      commentsCount: 2
    });
    
    Posts.insert({
      title: 'Meteor',
      ...
      commentsCount: 0
    });
    
    Posts.insert({
      title: 'The Meteor Book',
      ...
      commentsCount: 0
    });
    
    server/fixtures.js

    Dopodichè, ogni post deve partire da 0 commenti:

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime(),
      commentsCount: 0
    });
    
    var postId = Posts.insert(post);
    
    collections/posts.js

    Ed incrementiamo il valore di commentsCount quando un utente aggiunge un nuovo commento utilizzando l'operatore $inc di Mongo (che serve a incrementare il valore di un campo numerico):

    // update the post with the number of comments
    Posts.update(comment.postId, {$inc: {commentsCount: 1}});
    
    return Comments.insert(comment);
    
    collections/comments.js

    Possiamo dunque rimuovere l'helper commentsCount dal file client/views/posts/post_item.js, dal momento che il campo è disponibile nel post stesso.

    Commit 10-5

    Denormalized the number of comments into the post.

    Ora che gli utenti possono parlare l'un l'altro, sarebbe un peccato se la pagina non fosse aggiornata con i nuovi commenti via via che questi vengono inseriti; il prossimo capitolo ci spiega come utilizzare le notifiche per questo scopo.