Vue.js comment module

In this tutorial, I’ll show how to build a basic blog commenting system using vue.js.

Project files:

  • container.vue
  • container.js
  • comment.vue
  • comment.js
  • comment.css

The module expects the comment data to be provided with the structure demonstrated below. Please note that you can add other useful properties in the json like upvote, downvote, profile pic...

1. Set json data

container.js
       
const commentsJSON={comments_list:[{
    id:1,
    author_id:1,
    author:'laura',
    date:'2020-09-17 11:00:00',
    text:'this is a great blog. lots of precious informations!',
    count:2,//count replies
    reply:[{  id:2,
        author_id:2,
        author:'LBU',
        date:'2020-09-17 11:30:00',
        text:'so helpful article!'
        },{  id:4,
        author_id:1,
        author:'karim',
        date:'2020-09-17 11:30:00',
        text:'great one.'
    }]
    },
    {
    id:5,
    author_id:1,
    author:'gigi',
    date:'2020-09-18 11:30:00',
    text:'But I wish to manually mount the .vue file depending on a condition. The docs state',
    count:0,
    reply:[]

    }],count:2};//count comments
    

2. Add the comment box and list of comments.

Now that we’ve got the comments data for our blog post this template will display the comments and provide a way for our readers to submit comments, commentComponent element is a link to an external template that represent a single comment. Looping commentComponent will serialize all comments in commentsJSON.

container.vue
       
     <!-- Comment Form -->
    <div class="row" style="padding:10px">

      <div class="col">
        <textarea v-model.trim="commentInput" id="commentText" maxlength="500" class="commentBox" placeholder="write a comment..."></textarea>
        <div class="invalid-feed">commentValidity</div>
      </div>
    </div>
    <div>
      <button class="btn btn-default read-more" style="float:right;margin-right:10px" v-on:click="AddComment(post.id)">Submit</button>
    </div>
  

    <!-- Posted Comments -->
<div id="comment-container">
    <div id="comment-div">
    <commentComponent  v-for="comment in commentsJSON.comments_list" 
                    v-bind:key="comment.id" v-bind:comment="comment">
    </commentComponent>
    <div v-if="moreComments" id="more-comments">
    <a class="small-links" v-on:click="getNextComments(factor++)">
                    View more comments</a></div>
    <div v-else>No more comments</div>
</div>


3. Get Comments

container.js
 getNextComments:function(){
     //factor is the number of sequence of following comments
     //post is the related post
      var args = {"factor": this.factor,
          "post": this.post.id};
          //get next comments from database as commentsJSON
            if (commentsJSON.length==0){
              this.moreComments=false;//no more comments
            }
            else{
                 for (var i=0;i<commentsJSON.length;i++){
                     //add comments to the local json
                        this.commentsJSON.comments_list.push(comments[i]);
                }
            
          }
          
 }


getNextComments fetch the comments array from the server. The success callback returns the commentsJSON array as a result. this function is called on "created" and when view more comments is clicked.

4. New comment script function

container.js
     addComment:function(postId){
        if(this.commentInput == ''){ this.commentValidity="please fill the comment";}
        else{
        var args = {
            "post": Number(postId),
            "parent": null,
            "body": this.commentInput};
       //send args to server and return comment
          if(comment){
            this.commentsJSON.comments_list.unshift(comment);//add new comment on top
            this.commentValidity="";
            this.commentInput="";
          }
        });
      
    
    },


addComment function is used to create a new comment to the server. The parameters are post id , parent id of comment if the comment is a reply and comment text. The success callback returns the created comment as array of data. the new array will be added on top of commentsJSON and will automatically appear in template.

5. Single comment template with replies

comment.vue
    <template>

    <div class="media cmntBox" >

        <span class="voteBox" style="font-size:18px;" >
    <a  class="upVote glyphicon glyphicon-chevron-up"
      data-toggle="tooltip" data-placement="right" href="#"
    >
    </a>
    <h6>
      <span > 3</span>
    </h6>
    <a   class=" downVote glyphicon glyphicon-chevron-down" data-toggle="tooltip" data-placement="right" 
       href="#"  >
    </a>
  </span>
        <div class="media-body">
            <a class="cmntPPic" v-bind:href="'#/profile-view/'+comment.nickname">
                <img class="media-object cmntPPic" src="" alt="">
                </a>
          
            <div class="cmntContent" :id="'cmntContent'+comment.id">
                <h5 class="media-heading author">
                     <a v-bind:href="'#/profile-view/'+comment.nickname">comment.nickname</a>
                   <small><span class="far fa-clock" :title="comment.created_on">comment.created_on
                  </span></small></h5>
                <div class="toolbox">
                    <a v-if="comment.parent==null" v-on:click="addReplyBox(comment.id)">reply</a>
                    
                    <a  href="#" >report</a>
    
                </div>
                <div class="cmntText">comment.body</div>
            </div>

            <div v-if="comment.reply !== []" :id="'reply-box'+comment.id">
                <Comment v-for="reply in comment.reply" v-bind:key="reply.id" v-bind:comment="reply"></Comment>
            </div>

            <div v-if="toNumber(comment.replies_count)>0" :id="'more-reply'+comment.id" last-reply-id="">
                <a class="small-links" v-on:click="getNextReplies(comment.id,comment.replies_count)">
                    <abbr class="glyphicon glyphicon-arrow-right" /> comment.replies_count
                    <span v-if="toNumber(comment.replies_count)==1"> reply</span>
                    <span v-else>replies</span>
                </a>
            </div>


        </div>
    </div>
</template>


This template represent a single comment properties and replies count. Reply template is the same as comment so <Comment v-for="reply in comment.reply"... will iterate the same component for replies list.

6. Comment script function

comment.js
import newCommentComponent from '../../components/comment'

import Vue from 'vue'
var _replyBoxID=0;
var _postID;
var _replyBoxComponentParent;
var _parent;


//component to iterate itself for replies
var replyComponent={
  name: 'Comment',
  template: '<div></div>',
  props:{ comment:Object},
  data() {
     return {

     }
  }
}


//component for the new comment added
var addedReplyComponent={
  name: 'addedCom',
  components:{newCommentComponent},
  template: '<component :is="component" v-bind:comment="comment"></component>',
  props:{commentJSON:Object},
  data() {
     return {
       comment:this.commentJSON,
       component:null
     }
  },
  created() {
    this.component=newCommentComponent
  }
}

//component for the reply box
var replyBoxComponent={
  name: 'replyBox',
  template: `<div>
  <div class="form-group" id="reply-input">
    <textarea ref="reply" placeholder="Write a reply..." id="replyText" class="form-control" maxlength="500" rows="1"></textarea>
    <button  class="btn btn-default read-more" v-on:click="callParent">Reply</button>
    <button  class="btn btn-default read-more" v-on:click="removeComponent">Cancel</button>
    </div>
    </div>`,

  props:{ commentId:Number },
  data() {
     return {

     }
  },
  methods:
  {
    removeComponent:function(){
    document.getElementById('reply-input').remove();
    _replyBoxID=0
    },
    callParent:function() {
          _replyBoxComponentParent.addCommentDiv(this.commentId);
        }
  },
  created:function(){
    if (!_replyBoxComponentParent){
    _replyBoxComponentParent=this;

    }
  },
  mounted:function(){
    if(this.$refs.reply){
    this.$refs.reply.focus();
  }
  }
}




export default {
name: 'Comment', //this is the name of the component
 components: {
'reply': replyComponent
 },
 extends:replyBoxComponent,
 props: { comment:Object

  },
 data() {
    return {
      isReply:true,
      factor:0,
    }
},
methods:{
 
  addReplyBox:function(commentId){
    if(_replyBoxID != 0){
       document.getElementById('reply-input').remove();
    }
    var mainDiv=document.getElementById('cmntContent'+commentId);
    var replyBoxContainer = document.createElement('div');
    replyBoxContainer.setAttribute("id", "reply-input-box");
    mainDiv.appendChild(replyBoxContainer);
    _replyBoxID=commentId;

   const h=Vue.extend(replyBoxComponent);
    const vm=new h({
      propsData:{commentId}
    }).$mount('#reply-input-box');


  },

  addCommentDiv:function(commentId){

    var cmntText=document.getElementById('replyText');
    var commentDiv= document.getElementById('reply-box'+commentId);
    if(cmntText.value.trim() == ''){ alert("please fill the comment");}
    else{
    var args = {
        "post": _postID,
        "parent": commentId,
        "body": cmntText.value};
    var extras = {
        "parent": commentId,
        "commentDiv": commentDiv};

    //add new comment to server then return comment
          if(comment){
            
              this.handleAdd(comment,extras);
            }
         
  }
},
  handleAdd : function (commentJSON, extras) {

  var addedCommentDiv = document.createElement('div');
  addedCommentDiv.setAttribute("id", "added-comment");
  extras.commentDiv.insertBefore(addedCommentDiv, extras.commentDiv.childNodes[0]);

 const rc=Vue.extend(addedReplyComponent);
   const v=new rc({
     propsData:{commentJSON}
   }).$mount('#added-comment');

  document.getElementById('reply-input').remove();
  _replyBoxID= 0;


},
getNextReplies:function(commentId,cmt_count){

  var args = {"factor": this.factor,
      "parent": commentId};
        //send arguments to server and return replies as json data
        this.handleGetReplies(commentId,data,cmt_count);
    


},
handleGetReplies : function (commentId,replies,cmt_count) {

this.factor++;
var commentDiv= document.getElementById('reply-box'+commentId);
var addedCommentDiv = document.createElement('div');
addedCommentDiv.setAttribute("id", "added-comment");


const rc=Vue.extend(addedReplyComponent);
for (var i=0; i<replies.length;i++){
 commentDiv.appendChild(addedCommentDiv);

var commentJSON=replies[i];

  const v=new rc({
    propsData:{commentJSON}
  }).$mount('#added-comment');
}
        if(this.toNumber(cmt_count)>0){
     document.getElementById('more-reply'+_postID).innerHTML ='<a  v-on:click="getNextReplies('+commentId+');">View more replies</a>';
        }else{document.getElementById('more-reply'+_postID).innerHTML='';}

    },
    toNumber:function(data){
      return parseInt(data)-(this.factor*2)
    }

},
created:function(){
  if(!_postID){
    _parent=this.$parent;
  _postID=this.$parent.getPostId();
}
},

}
}


This script enable to iterate replies , open reply box for any comment and load more replies.

7. Add some css

comment.css
#added-comment{background-color: #777799}
.commentBox{
  min-height:20px;
  border:1px solid #C1C0C0;
  border-radius:3px;
   padding:5px;}
#comment-container{padding:15px;}
div.cmntBox{
    background-color: #f9f9f9;
   margin-bottom:10px;
    border-top:thin dotted #eee;
    border-bottom:thin dotted #eee ;
}
h5.author:first-letter,.media-body:first-letter,.cmntBox:first-letter{
  text-transform: capitalize
}
.media-body{padding-top: 3px}

div.cmntText {
    margin-top:10px;
  word-break: break-word
}

a.upVote ,a.downVote{
    cursor: pointer;
    text-decoration:none;
    display: block;
    position: relative;
    top: 1px;
    font-family: 'Glyphicons Halflings';
    font-style: normal;
    font-weight: normal;
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;

}
img.cmntPPic {

    height: 24px;
    width: 24px;
    border: thin solid;
}
a.cmntPPic{
    float:left;
    padding-right:10px;
}

Congradulation it is done!

Comments