Building the comments component

We now have all the components ready in order to finish building our commenting system. The last missing piece of the puzzle is the Comments component, which will list all the comments and provide an editor to create new comments.

First, let’s take a look at the template of our Comments component that we will create in a file named comments.html within a folder named comments:

<div class="comments__title">Add new comment</div>
<div class="comments__add-comment-section">
  <div class="comments__add-comment-box">
    <ngc-editor [editMode]="true"
                [showControls]= "false"></ngc-editor>
  </div>
  <button (click)="addNewComment()"
          class="button" >Add comment</button>
</div>

<div *ngIf="comments?.length > 0">
  <div class="comments__title">All comments</div>
  <ul class="comments__list">
    <li *ngFor="let comment of comments">
      <ngc-comment [content]="comment.content"
              [time]="comment.time"
              [user]="comment.user"
              (commentEdited)="onCommentEdited(comment, $event)">
      </ngc-comment>
    </li>
  </ul>
</div>

You can see the direct usage of an Editor component within the component’s template. We are using this in-place editor to provide an input component to create new comments. We could also use a text area here, but we’ve decided to reuse our Editor component. We will set the editMode property to true so it will be initialized in edit mode. We will also set the showControls input to false because we don’t want the editor to become autonomous. We will only use its in-place editing capabilities, but control it from our Comments component.

To add a new comment, we will use a button that has a click event binding, which calls the addNewComment method on our component class.

Below the section where users can add new comments, we will create another section that will list all the existing comments. If no comments exist, we simply don’t render the section. With the help of the NgFor directive, we could display all the existing comments and create a Comment component for each repetition. We will bind all the comment data properties to our Comment component and also add an event binding to handle updated comments.

Let’s create the component class within a new file named comments.js in the comments folder:

import {Component, Inject, Input, Output, ViewEncapsulation, ViewChild, EventEmitter} from '@angular/core';
import template from './comments.html!text';
import {Editor} from '../ui/editor/editor';
import {Comment} from './comment/comment';
import {UserService} from '../user/user-service/user-service';

@Component({
  selector: 'ngc-comments',
  host: {
    class: 'comments'
  },
  template,
  encapsulation: ViewEncapsulation.None,
  directives: [Comment, Editor]
})
export class Comments {
  // A list of comment objects
  @Input() comments;
  // Event when the list of comments have been updated
  @Output() commentsUpdated = new EventEmitter();
  // We are using an editor for adding new comments and control it 
  // directly using a reference
  @ViewChild(Editor) newCommentEditor;

  // We're using the user service to obtain the currently logged 
  // in user
  constructor(@Inject(UserService) userService) {
    this.userService = userService;
  }

  // We use input change tracking to prevent dealing with 
  // undefined comment list
  ngOnChanges(changes) {
    if (changes.comments && 
        changes.comments.currentValue === undefined) {
      this.comments = [];
    }
  }

  // Adding a new comment from the newCommentContent field that is 
  // bound to the editor content
  addNewComment() {
    const comments = this.comments.slice();
    comments.splice(0, 0, {
      user: this.userService.currentUser,
      time: +new Date(),
      content: this.newCommentEditor.getEditableContent()
    });
    // Emit event so the updated comment list can be persisted 
    // outside the component
    this.commentsUpdated.next(comments);
    // We reset the content of the editor
    this.newCommentEditor.setEditableContent('');
  }

  // This method deals with edited comments
  onCommentEdited(comment, content) {
    const comments = this.comments.slice();
    // If the comment was edited with e zero length content, we 
    // will delete the comment from the list
    if (content.length === 0) {
      comments.splice(comments.indexOf(comment), 1);
    } else {
      // Otherwise we're replacing the existing comment
      comments.splice(comments.indexOf(comment), 1, {
        user: comment.user,
        time: comment.time,
        content
      });
    }
    // Emit event so the updated comment list can be persisted 
    // outside the component
    this.commentsUpdated.next(comments);
  }
}

Let’s go through individual code parts again and discuss what each of them does. First, we declared an input property named comments in our component class:

@Input() comments;

The comments input property is a list of comment objects that contains all of the data associated with the comments. This includes the user who authored the comment and the timestamp, as well as the content of the comment.

We also need to be able to emit an event once a comment is added or an existing comment is modified. For this purpose, we used an output property named commentsUpdates:

@Output() commentsUpdated = new EventEmitter();

Once a new comment is added or an existing one is modified, we will emit an event from this output property with the updated list of comments.

The Editor component we’re going to use to add new comments will not have its own control buttons. We will use the showControls input property to disable them. Instead, we will control the editor from our Comments component directly. Therefore, we need a way to communicate with the Editor component within our component class.

We used the @ViewChild decorator for this purpose again. However, this time, we did not reference a DOM element, which contains a local view variable reference. We directly passed our component type class to the decorator. Angular will search for any Editor components within the comments view and provide us with a reference to the instance of the editor. This is shown in the following line of code:

@ViewChild(Editor) newCommentEditor;

Since the Comments component only hosts one editor directly within the component template, we can use the @ViewChild annotation to obtain a reference to it. Using this reference, we can directly interact with the child component. This will allow us to control the editor directly from our Comments component.

Let’s move on to the next part of the code, which is the Comments component constructor. The only thing we’ve done here is inject a user service that will provide us with a way to obtain information of the currently logged-in user. As of now, this functionality is only mocked, and we will receive information of a dummy user. We need this information in the Comments component, since we need to know which user has actually entered a new comment:

constructor(@Inject(UserService) userService) {
  this.userService = userService;
}



It should be an empty list in case there are no comments, but the input property comments should never be undefined. We controlled this by using the OnChange life cycle hook and overriding our comments property if it was set to undefined from outside:

ngOnChanges(changes) {
  if (changes.comments && 
          changes.comments.currentValue === undefined) {
    this.comments = [];
  }
}

This small change makes the internal handling of our comment data much cleaner. We don’t need additional checks when working for array transformation functions, and we can always treat the comments property as an array.

Since the Comments component is also responsible for handling the logic that deals with the process of adding new comments, we needed a method that could implement this requirement. In relation to this, we used some immutable practices we learned about in the previous chapter:

addNewComment() {
  const comments = this.comments.slice();
  comments.splice(0, 0, {
    user: this.userService.currentUser,
    time: +new Date(),
    content: this.newCommentEditor.getEditableContent()
  });
  this.commentsUpdated.next(comments);
  this.newCommentEditor.setEditableContent('');
}

There are a few key aspects in this part of the code. This method will be called from our component view when the Add comment button is clicked. This is when the user will have already entered some text into the editor and a new comment will have been created.

First, we will use the user service that we injected within the constructor to obtain information related to the currently logged-in user. The content of the newly created comment will be obtained directly from the Editor component we set up using the @ViewChild annotation. And, the getEditableContent method will allow us to receive the content of the editable element within the in-place editor.

The next thing we wanted to do was to communicate an update of the comment list with the outside world. We used the commentsUpdated output property to emit an event with the updated comment list.

Finally, we wanted to clear the editor used to add new comments. As the in-place editor in the view of the Comments component is only used to add new comments, we can always clear it after a comment is added. This will give the user the impression that his comment has been moved from the editor into the list of comments. Then, once again, we can access the Editor component directly using our newCommentEditor property and call the setEditableContent method with an empty string to clear the editor. And this is what we’ve done here.