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.