Build a Comments Component with AngularJS and dotCMS

$URLMapContent.title

Build a Comments Component with AngularJS and dotCMS Posted: 03.30.2015

The dotCMS Content API is very powerful and can be used to make your site more engaging and responsive. In this example, we will build a componenet that will allow site visitors to make requests, comments, and display user generated content instantly, without reloading the page. It's a great way to provide a more dynamic and engaging user experience.

In this tutorial we'll show you how to get and post comments to dotCMS on an html page with AngularJS. The example is built to work with our dotCMS demo site at: http://demo.dotcms.com. With minor modifications you'll be able to make it work in your own custom dotCMS set-up. Let's get started!

First, you may wish to download the components and sample code:

angular-comments-component.zip

Tools we are going to use:

Files we're going to work with

| demo.dotcms.com
|- /tutorial/
|-- index
|-- comments-list.html
|-- comments-form.html
|-- comments.js

Part I: Get the Comments

1. Create a folder

Make sure you create a folder in the demo site or on your dotCMS system called: "tutorial" We'll be working in that folder quite a bit.

NOTE: all the paths are we going to use will be related to the /tutorial/ folder so if your folder has any other name, make sure to update your paths as you go through these examples.

2. Create a home page

Inside the "tutorial" folder, create a page called "index", use the "blank template", publish the Page, and create a Simple Widget with the following basic code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
  </head>
  <body>
    <h1>DotCMS Comment List with AngularJS</h1>
    <div ng-app="commentsApp"></div>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
    <script src="comments.js"></script>
  </body>
</html>

On the index page above:

  1. We call the Bootstrap framework css for styling
  2. We define the AngularJS module or app: <div ng-app="commentsApp"></div>
  3. Then we call AngularJS
  4. And call a file called comments.js that we'll create next.

3. Now some Javascript

Now let's create a file called: comments.js and write the following code in it:

var commentsApp = angular.module('commentsApp', []);

Now we just create the Angular App and make the reference to the module in the HTML. Yep, it is just that easy.

Before we go further, there are three main AngularJS concepts we focus on in this tutorial:

  1. Custom Directives allow you to extend the vocabulary of the browser, in other words, create custom HTML tags and attributes. A good explaination is available here.
  2. Services are singleton objects or functions that carry out specific tasks (like fetch comments for example). They hold some business logic. More info.
  3. Controllers are a JavaScript constructor function that are used to augment the Angular Scope. More info.

3.1. Next, let's create our custom directive:

In the same comments.js file, let's add:

commentsApp.directive('commentList', [
  function () {
    return {
      restrict: 'E',
      scope: {
        contentId: '=',
        contentTitle: '='
      },
      templateUrl: 'comments-list.html',
      controller: 'CommentsCtrl',
      controllerAs: 'comments'
    };
  }]);
  1. Create the commentList directive --> <comment-list></comment-list>.
  2. Restrict 'E' states that we want this to be an element directive (or it could be an attribute directive, more info).
  3. Scope gets the attributes we are going to pass to this directive, in this case, news identifier and title. The '=' sign in each is because we want to pass this attribute to the child directive (we will see more of this when we build the comment form to post comments).
  4. TemplateUrl is basically the HTML code that the directive is going to use to display the content.
  5. Controller binds the data to the templateUrl
  6. ControllerAs is the alias for accessing the controller.

Creating the directive commentList:

 <comment-list></comment-list>

The HTML tag is the name of the directive. The directive name should be camel cased, in this instance "commentList" and the tag created should be <comment-list>.

3.2. Now we need to create a Service

This service will allow us to fetch and/or post comments to/from dotCMS.

commentsApp.service('CommentsService', ['$http', function($http) {}]);

We created our custom service CommentsService and are injecting the $http service that is a core Angular service. This will facilitate communication with the remote HTTP servers via the browser's XMLHttpRequest object or via JSONP.

Now let's make the actual Request:

For this particular example we'll get News comments. The dotCMS demo site already has a relationship between the News and Comments content types called News-Comments. (To learn more about creating relationships between content types in dotCMS click here.)

So the dotCMS url that we will use is something like this: (your host id will be different)

/api/content/render/false/type/json/query/+structureName:Comments%20+(conhost:48190c8c-42c4-46af-8d1a-0cd5db894797%20conhost:SYSTEM_HOST)%20+News-Comments:[identifier]%20+languageId:1%20+deleted:false%20+working:true/orderby/modDate%20desc

If you read this url carefully, you'll notice that we are "pulling" all the comments from the +structureName:Comments but only those related to a specific news item identifier: +News-Comments:[identifier]

Let's add a getComments method to our CommentsService:

commentsApp.service('CommentsService', ['$http', function($http) {
  this.getComments = function(id) {
    return $http.get('/api/content/render/false/type/json/query/+structureName:Comments%20+(conhost:48190c8c-42c4-46af-8d1a-0cd5db894797%20conhost:SYSTEM_HOST)%20+News-Comments:' + id + '%20+languageId:1%20+deleted:false%20+working:true/orderby/modDate%20desc')
      .success(function(data) {
        angular.forEach(data.contentlets, function(item) {
          item.datePublished = new Date(item.datePublished);
        });
      })
      .error(function(data, status) {
        console.log('ERROR: ' + status + '. We can\'t get the comments right now, please try again later');
      });
  };
}]);

What this method is doing:

  1. The method receives an id, which is the news identifier that we need to build our url.
  2. We make the request with the $http AngularJS built-in service.
  3. When we return the $http service in our method, we are returning a "promise" (this will be helpful when we make the actual call to the getComments method within our service.
  4. If the request is success, we iterate for each piece of content to replace the string datePublished for an actual date format.
  5. If the request returns an "error," we just log the error.

3.3. It's time to create our first controller:

Controllers must be responsible for binding the model data to views. It does not contain logic to fetch the data or manipulate it. In this tutorial, we use the CommentServices to fetch data.

commentsApp.controller('CommentsCtrl', ['$scope', 'CommentsService', function($scope, CommentsService) {
  CommentsService.getComments($scope.contentId)
    .then(function(response) {
      $scope.comments = response.data.contentlets;
    });
}]);
  1. We created the CommentsCtrl
  2. We injected the $scope (we will talk more about this in a bit)
  3. We injected our custom service: CommentsService
  4. From our CommentsService we invoke the getComments method, and because this returns a "promise", we can perform a ".then()" and assign the result contentlets (the actual comments data) to $scope.comments.

The $scope

The "scope" is an object that refers to the application model. It is the execution context for expressions. Scopes are arranged in hierarchical structures which mimic the DOM structure of the application. Scopes can watch expressions and propagate events.

So now that we have data in the $scope.comments object we can access the comments data from the HTML


4. Building the Template

Create a file in your favorite text editor called: comments-list.html and write the following html code there:

<a href="" class="btn btn-default btn-post-comment pull-right">Post a comment</a>
<h3 class="comments-title">
  <span class="bagde"></span>
  Comments
</h3>
<ul class="media-list comment-detail-list">
  <li class="media">
    <article>
      <div class="pull-left comment-info">
        <a href="#" class="author"></a><br />
        <time></time>
      </div>
      <div class="media-body">
        <div class="comment-body">
          <p></p>
        </div>
      </div>
    </article>
  </li>
</ul>

We have our title where we'll be putting the comments count and we have our comments list with:

  • Author information (name and icon)
  • Publish date
  • Comment body

All the HTML is Bootstrap ready.

Angular built-in directives:

Because $scope.comments is an array of comment contentlets that we get from our CommentsService we need to iterate over each to "print" them out in our comments-lists.html file. To do that, AngularJS has some built-in directives that are called ng-repeat.

So, when we do:
<li class="media" ng-repeat="comment in comments">

We are saying: "Hey, AngularJS, print one <li> for each comment we have in the list of comments.

Angular Expressions:

Angular expressions are JavaScript-like code snippets. We are using it to print the comments data:

  1. First we display the comments quantity that we get <span class="bagde">{{comments.length}</span>. Because comments is an array of objects we can use .length to get the amount.
  2. And then, we just print the content for each comment:

<p>{{ comment.comment }}</p>

<a href="#" class="author">{{ comment.author }}</a>

And so on...

Angular Filters:

A filter formats the value of an expression to display to the user.

So when we do:

<time>{{ comment.datePublished | date:"MM/dd/yyyy" }}</time>

We are formatting the date into: "MONTH/DAY/YEAR". Ex: 12/31/2015

Our final comment-list.html HTML will look like this:

<a href="" class="btn btn-default btn-post-comment pull-right">Post a comment</a>
<h3 class="comments-title">
  <span class="bagde">{{comments.length}}</span>
  Comments
</h3>
<ul class="media-list comment-detail-list">
  <li class="media" ng-repeat="comment in comments">
    <article>
      <div class="pull-left comment-info">
        <a href="#" class="author">{{ comment.author }}</a><br />
        <time>{{ comment.datePublished | date:"MM/dd/yyyy" }}</time>
      </div>
      <div class="media-body">
        <div class="comment-body">
          <p>{{ comment.comment }}</p>
        </div>
      </div>
    </article>
  </li>
</ul>

5. Now Let's test this thing

Open the page /tutorial/index.html. Edit the simple widget we created and call the custom directive that we created inside the <div ng-app="commentsApp"></div> tag:

<div ng-app="commentsApp">
    <comment-list content-id="'7a3d042f-aae4-4e60-8385-7fc5320f572f'"></comment-list>
</div>

We need to pass a valid news content identifier and make sure it has some comments related to it.

Open index.html in the browser and, if everything is ok, you should see the comments.

Part II: Post the Comments

1. We need some HTML for the form:

Create the file comments-form.html and write the following code:

<form ng-submit="submitComment()" id="commentsForm" name="commentsForm">
  <fieldset>
    <legend>Post a comment</legend>

    <div class="alert alert-danger" ng-show="c.alert">There was an error posting your comment, please try again later</div>
    <div class="form-group name-group">
      <label for="name" class="control-label">Name</label>
      <div class="control-field">
        <input class="form-control" id="name" name="name" type="text" value="" tabindex="1" ng-model="c.author">
      </div>
    </div>
    <div class="form-group email-group">
      <label for="email" class="control-label">Email</label>
      <div class="control-field">
        <input class="form-control" id="email" name="email" type="email" value="" tabindex="2" ng-model="c.email">
      </div>
    </div>
    <div class="form-group comment-group">
      <label for="comment" class="control-label">Comment</label>
      <div class="control-field">
        <textarea class="form-control" cols="30" rows="5" name="comment" id="comment-textarea" tabindex="3" ng-model="c.comment" required></textarea>
      </div>
    </div>
    <div class="form-group button-group">
      <div class="control-field">
        <button type="submit" class="btn btn-primary btn-comment" tabindex="4">Post comment</button>
        <a href="" class="btn btn-danger btn-post-comment">Cancel</a>
      </div>
    </div>
  </fieldset>
</form>

New AngularJS built-in directives

As you can see, there are some new AngularJS directives in this HTML code.

  1. ng-submit: Enables binding angular expressions to "onsubmit" events. In this case, when the form is submitted, we are going to call the submitComment() function that we'll be creating in our commentsForm controller soon.
  2. ng-show: shows or hides the given HTML element based on the expression provided.
  3. ng-model: Binds an input, select, textarea (or custom form control) to a property on the scope. So all the values in our form fields will be accessible from our commentsForm.

2. After HTML, we need some Javascript

2.1. New directive form - the comment form:

In the comments.js file, lets add the commentForm directive right below our commentList directive:

commentsApp.directive('commentForm', [
  function () {
    return {
      restrict: 'E',
      templateUrl: 'comments-form.html',
      controller: 'CommentsFormCtrl',
      controllerAs: 'commentsForm'
    };
  }]);

We are creating the commentForm directive the same way we did with the commentList.

2.2. Expanding our service

We built a service with the method getComments now we need a method to post comments, let's call it postComments:

In the same comments.js inside the CommentsService let's add the new method:

this.postComment = function(data) {
  return $http.post('/api/content/publish/1', data)
    .success(function() {
      console.log('Comment was post successfully');
    })
    .error(function() {
      console.log('There was an error posting the comment');
    });
};

What the new method is doing:

  1. The method receives an data object, that's the info we'll be using to create the comment content in dotCMS.
  2. Once again, we made the POST request with the $http AngularJS built-in service.
  3. When we return the $http service in our method, we are returning a "promise" (this will be helpful when we make the actual call to the getComments method within our service.
  4. If the request is success, we just log a message.
  5. If the request returns an error we just log the error.

2.3. Now the controller

commentsApp.controller('CommentsFormCtrl', ['$scope', 'CommentsService', function($scope, CommentsService) {
  $scope.c = {};
  $scope.c.alert = false;

  $scope.submitComment = function(comments) {
     var submitDate = new Date();
    $scope.c.alert = false;

    var data = {
      'author': $scope.c.author,
      'comment': $scope.c.comment,
      'datePublished': submitDate.getFullYear() + '-' + (submitDate.getMonth() + 1) + '-' + submitDate.getDate() + ' ' + submitDate.getHours() + ':' + submitDate.getMinutes() + ':' + submitDate.getSeconds(),
      'email': $scope.c.email,
      'languageId': 1,
      'News-Comments': '+structureName:News +identifier:' + $scope.contentId,
      'stName': 'Comments',
      'title': 'Comment re: ' + $scope.contentTitle
    };

    CommentsService.postComment(data)
      .then(function() {
        $scope.commentsForm.$setPristine();
        $scope.c = {};
      }, function() {
        $scope.c.alert = true;
      });
  };

  $scope.toggleCommentForm = function() {
    $scope.commentForm.hide = !$scope.commentForm.hide;
  };
}]);

This is a little more complex than our first controller, so let's explain some of the details:

  1. We create the CommentsFormCtrl
  2. We inject the $scope
  3. We inject our custom service CommentsService
  4. $scope.c = {}; creates an empty object. .c is bound to our form fields through the ng-model directive.
  5. $scope.c.alert = false; by default we hide the alert message we have in the form.
  6. We create the method $scope.submitComment which is called from the HTML when the form is submitted.
  7. Inside $scope.submitComment we build the data with the values of our form fields.
  8. Then we just call the postComment in our CommentsService passing the data (comment information).
  9. If the request is a success, we clear the form and make the .c object empty again.
  10. If the request is an error, we display our alert message.

2.4. Last part of the puzzle

Open the comments-list.html and add the <comment-form></comment-form> tag when you want to display the comment form. If everything was set correctly, you should be able to see the form and submit content using the form.

Congratulations! It was a little work, but worth it to add some user generated content to your site and create a more dynamic user experience. If you have not already, save some time and download the sample code and upload it to your dotCMS instance (remember to modify the paths for your set-up) comments-component.zip.

If you need additional help or have comments about Building a Comments Component with AngularJS and dotCMS, share your thoughts and make a posting on the dotCMS Community Forum.



New report from Digital Clarity Group notes that a majority of digital marketing teams don’t or can’t use all the software capabilities they have, creating “shelfware” or “underused” software. Gain fresh perspective on the challenges facing digital marketers. Download Now.