Create a commenting Application using Angular 2

Create a commenting Application using Angular 2

  • 2016-09-23
  • 1356

In This Tutorial , we’re going to create a commenting Application using angular 2. That will enable users to write comments in various areas of our application. Each comment itself is encapsulated into a Comment component. Comment components themselves use an editor that enables users to edit comments once they are created.

Building the comment component

Let’s start building our commenting system by fleshing out the Comment component first. In addition to the comment itself, we’d like to display the user’s profile who commented, and of course, the time of the comment.

To display the time, we will make use of relative time formatting, as this will give our users a better feel of time. Relative time formatting displays timestamps in the format “5 minutes ago” or “1 month ago”, in contrast to absolute timestamps, such as “25.12.2015 18:00”. Using the Moment.js library, we’ll create a pipe that we can use within component templates to convert timestamps and dates into relative time intervals.

Let’s create a new pipe within a new folder named pipes. The pipe needs to be created within a file named from-now.js, which is created under the pipes folder:

import {Pipe} from [email protected]/core';
// We use the Moment.js library to convert dates to relative times
import Moment from 'moment';

@Pipe({
  // Specifying the name to be used within templates
  name: 'fromNow'
})
// Our pipe will transform dates and timestamps to relative times 
// using Moment.js
export class FromNowPipe {
  // The transform method will be called when the pipe is used 
  // within a template
  transform(value) {
    if (value && (value instanceof Date || 
        typeof value === 'number')) {
      return new Moment(value).fromNow();
    }
  }
}

This pipe can now be used within the templates of components to format timestamps and dates into relative time intervals.

Let’s use this pipe and the Editor component we created in the previous topic to create our Comment component. Within a file named comment.html, which is located within a new comment folder in the comments folder, we’ll create the template for our Comment component:

<div class="comment__l-meta">
  <div class="comment__user-picture">
    <img [attr.src]="user.pictureDataUri" src="">
  </div>
  <div class="comment__user-name">{{user.name}}</div>
  <div class="comment__time">
    {{time | fromNow}}
  </div>
</div>
<div class="comment__l-main">
  <div class="comment__message">
    <ngc-editor [content]="content"
                [showControls]="true"
                (editSaved)="onContentSaved($event)">
    </ngc-editor>
  </div>
</div>

From the user object, we will get the user’s profile image as well as the username. To display the time of the comment in a relative format, we’ll use the fromNow pipe that we created earlier.

Finally, we will make use of the in-place editor component to display the content of the comment and make it editable at the same time. We will bind the comment content property to the content input property of the editor. At the same time, we will listen for the editSaved event of the editor and call the onContentSaved method on our comment component class. If you look at our component code again, you’ll notice that we are re-emitting the event within the method so that the outside world is also notified about the change in the comment.

Let’s take a look at the component class that we will create in a file named comment.js:

import {Component, Input, Output, ViewEncapsulation, EventEmitter} from [email protected]/core';
import {Editor} from '../../ui/editor/editor';
import template from './comment.html!text';
// We use our fromNow pipe that converts timestamps to relative 
// times
import {FromNowPipe} from '../../pipes/from-now';

@Component({
  selector: 'ngc-comment',
  host: {
    class: 'comment'
  },
  template,
  encapsulation: ViewEncapsulation.None,
  directives: [Editor],
  pipes: [FromNowPipe]
})
export class Comment {
  // The time of the comment as a timestamp
  @Input() time;
  // The user object of the user who created the comment
  @Input() user;
  // The comment content
  @Input() content;
  // If a comment was edited this event will be emitted
  @Output() commentEdited = new EventEmitter();

  onContentSaved(content) {
    this.commentEdited.next(content);
  }
}

The component code is pretty straightforward. The only noticeable difference to other components we’ve created so far is the pipes property within the component’s annotation. Here, we specify that we’d like to use the FromNowPipe class that we’ve just created. Pipes always need to be declared within the component; otherwise, they can’t be used within the component’s template.

As input, we expect a user object that is passed along with the user input property. The content input property should be filled with the actual comment as a string, while the time input property should be set to a timestamp that reflects the actual time of the comment.

We also have an output property called commentEdited, which we will use to notify the changes on the comment. The onEditSaved method will be called by the event binding on our Editor component, which will then emit an event using the commentEdited output property.

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 [email protected]/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;
}

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:

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

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.

Our Comments component will hold the list of all the comments, and its view will create a Comment component for each comment in that list. Each Comment component will use an Editor component to provide in-place editing of its content. These editors work autonomously using their own controls, and they emit an event if the content is changed or altered in any way. To take care of this, we need to re-emit this event with the name commentEdited from the Comment component. Now we only need to catch this event within our Comments component in order to update the list of comments with the changes. This is illustrated in the following part of the code:

onCommentEdited(comment, content) {
  const comments = this.comments.slice();
  if (content.length === 0) {
    comments.splice(comments.indexOf(comment), 1);
  } else {
    comments.splice(comments.indexOf(comment), 1, {
      user: comment.user,
      time: comment.time,
      content
    });
  }
  this.commentsUpdated.next(comments);
}

This method will be called for each individual Comment component that is repeated using the NgFor directive. From the view, we pass a reference to the comment object concerned, as well as the edited content we would receive from the Comment component event.

The comment object will only be used to determine the position of the updated comment within the comment list. If the new comment content is empty, we will remove the comment from the list. Otherwise, we will just create a copy of the previous comment object, change the content with the new edited content, and replace the old comment object in the list with the copy.

Finally, since we wanted to communicate the change in the comment list, we emitted an event using the commentUpdated output property.

With this, we have completed our commenting system, and now it’s time to make use of it. We already have an empty tab prepared for our project comments, and this is going to be the spot where we will add commenting capabilities using our commenting system.

First, let’s amend our Project component template, project/project.html, to include the commenting system:

...
<ngc-tabs>
  <ngc-tab name="Tasks">...</ngc-tab>
  <ngc-tab name="Comments">
    <ngc-comments [comments]="comments"
                  (commentsUpdated)="updateComments($event)">
    </ngc-comments>
  </ngc-tab>
  <ngc-tab name="Activities"></ngc-tab>
</ngc-tabs>

This is as easy as it gets. Since we are paying attention to a clean component architecture, the inclusion of our commenting system really works like a breeze. The only thing we now need to ensure is that we provide a property on our project with a list of comments. We also need a way to react to changes if comments are updated within our Comments component. For this purpose, we will create an updateComments method.

Let’s look at the component class changes within the project/project.js file:

export class Project {
  ...
  @Input() comments;
  ...
    updateComments(comments) {
      this.projectUpdated.next({
        comments
      });
  }
}

Since we are already dealing with project updates in a general way and our Project component is emitting directly to our App component, where projects data will be persisted, the only thing we need to implement is an additional input property, as well as a method to handle comment updates

Recap

Within this Tutorial , we have successfully created a full-fledged commenting system that can be placed in various areas of our application to enable commenting. Users can interact with in-place editors to edit the content in comments, which gives them a great user experience.

While writing the code for our commenting system, we learned about the following topics:

  1. Implementing a simple pipe using the @Pipe annotation and the Moment.js library to provide relative time formatting
  2. Using the OnChanges life cycle hook to prevent unwanted or invalid values within input properties
  3. Using @ViewChild to obtain a reference to the components within the component sub-tree in order to establish direct communication
  4. Reusing the Editor component as an input field replacement and as an autonomous in-place editor within the Comment component

Source via: dunebook.com

Suggest

Angular 2 with TypeScript for Beginners: The Pragmatic Guide

The Complete Angular 2 With Typescript Course - Update RC 6

Angular 2 and NodeJS - The Practical Guide to MEAN Stack 2.0

Angular 2 - The Complete Guide (Updated to Final Version!)

Learn Angular 2 Development By Building 10 Apps