Building a Hacker News Clone in AngularJS – Part 5

tutorial

Hi Everyone! Welcome to the next post in our series on building a Hacker News Clone in AngularJS! In this post, we’re going to focus on using nested components to display threaded comments on individual posts. In the last few posts, we’ve covered a bunch of topics.

In Part 1, we covered creating an Angular service to connect to the Hacker News API.
In Part 2, we covered creating a custom Angular directive to display individual stories.
In Part 3, we covered AngularJS pagination with a library developed by Michael Bromley
In Part 4, we covered creating a custom Angular component to display individual comments, using Angular Filters to help out.

In Part 5 of our series, we’re going to complete our custom Angular component that will display a comment. Specifically, we’re going to focus on expanding its functionality by allowing it to display nested comments (threaded comments). This will be a good example of how to implement nested components in AngularJS.

To build out this functionality, we’ll need to cover the following tasks:

  1. Accessing the 'kids' property on each comment object retrieved by our story-comment component
  2. Adding an ng-repeat to display all child comment ids in an unordered list within the story-comment component’s template
  3. Adding nested story-comment components within each list item, passing the child comment ids inside each iteration of the ng-repeat
  4. Adding an ng-repeat to display all comments attached to a story’s show page

Accessing the 'kids' Property on Each Comment Object Returned from Hacker News API

First, we’ll want to access and capture the kids property on each comment object returned from the Hacker News API so that we can display replies to a comment within the story-comment component’s template. To do that, first we’ll add a commentKids property to the vm object on our StoryComment Component’s controller. To that property, we’ll assign a value of the 'kids' array that we get from the Hacker News API’s response data:

// js/app/components/StoryComment.js
...
  StoryCommentController.$inject = ['TopStoriesService', 'getHoursAgoFilter']
  function StoryCommentController (TopStoriesService, getHoursAgoFilter) {
    var vm = this;
    vm.comment;
    vm.commentText;
    vm.commentAuthor;
    vm.commentTime;
    vm.commentKids;

    TopStoriesService
      .getStory(vm.id)
      .then(function(res){
        vm.comment = res.data;
        vm.commentText = res.data.text;
        vm.commentAuthor = res.data.by;
        vm.commentTime = getHoursAgoFilter(res.data.time);
        vm.commentKids = res.data.kids;
      });
  }

  angular
    .module('app')
    .component('storyComment', StoryComment);

Now, we can add a reference to this vm.commentKids value in within the story-comment.html template for our component. This way, we’ll be able to see the array of IDs that point to comments added in response to a given comment.

<!-- views/story-comment.html -->
<div>{{ vm.commentAuthor }} {{ vm.commentTime }}</div>
<div ng-bind-html="vm.commentText"></div>
{{ vm.commentKids }}

After we’ve made these changes, let’s reload http://localhost:8080/#/post?id=1 in the browser and see what we get:

Hacker News Clone Nested Components Child Comment Ids Visible

Notice that we have an array of Ids corresponding to the nested comments below the comment text.

As you can see, we now have the array of IDs of comment replies. This array will give us what we need to display nested components for our comments. Let’s add our changes and commit them to our repository:

git add . 
git ci -m "adds vm.commentKids to Story Comment component"

Displaying the Comment IDs within an ng-repeat

Now that we have our child comment IDs, let’s display them within an unordered list using an ng-repeat. We’ll do that by making an adjustment to our story-comment.html template:

<!-- views/story-comment.html -->
<div>{{ vm.commentAuthor }} {{ vm.commentTime }}</div>
<div ng-bind-html="vm.commentText"></div>
<ul>
  <li ng-repeat="commentId in vm.commentKids">
    {{ commentId }}
  </li>
</ul>

Now, when we reload the app in our browser, we can see the child comment IDs displayed in an unordered list below the comment:

Hacker News Clone Nested Components for comments Child Comment IDs displayed in unordered list

Now we have the Child Comment IDs displayed in an unordered list, indented from the comment content.

Notice that our child comment IDs are indented from our comment. This will work nicely when we add nested components for our comments below.

Adding Nested Components to Display Child Comments

Now that we’ve got our child comment IDs nested underneath our comment, let’s pass them to nested StoryComment components. To do that, all we have to do is place another <story-comment> element within the ng-repeat in our story-comment.html template and pass the comment ID as an argument.

<!-- views/story-comment.html -->
<div>{{ vm.commentAuthor }} {{ vm.commentTime }}</div>
<div ng-bind-html="vm.commentText"></div>
<ul>
  <li ng-repeat="commentId in vm.commentKids">
    <story-comment id="commentId"></story-comment>
  </li>
</ul>

Having another <story-comment> component within the story-comment.html template is what makes this an example of nested components. Now, when we reload the page in our browser, this is what we see:

Hacker News Clone Nested Components displaying nested comments for the first time

We have nested comments!

Great! This is exactly the kind of thing we wanted to do. Notice that we actually have comments nested 2 levels deep. The reason this works is that each StoryComment component’s template contains additional nested components for each of the comments that are in the kids array we get back from the Hacker News API. As long as a comment has child comments, our template will display them within the ng-repeat.

We’ll make some styling changes so that it displays cleaner in the next step. But, before we move on, let’s add our changes and commit them to version control:

git add . 
git ci -m "adds nested comments to StoryComment component"

Cleaning up the Styles

For the next few steps we’ll be using the Tachyons CSS library. You should have already linked to Tachyons by adding the following link to the top of index.html. If you haven’t for some reason, do that now:

<link rel="stylesheet" href="https://npmcdn.com/tachyons@4.0.1/css/tachyons.min.css">

There are three things I notice looking at these nested comments that I think we could improve:

First, the spacing is uneven. It would look nicer if each comment was separated from other comments by the same amount. To make that happen, we’ll need to remove the top margin from each of the unordered lists and add top padding to each of the list items containing comments. The Tachyons classes we’ll use are mt0 to remove the top margin and pt2 to add the top padding.

Second, It would be nice to have a bigger distinction between the comment author on the top line and the comment itself. In this case, I’m going to make the top line bold and give it a gray color. The Tachyons classes we add to get this effect are: b for bold and mid-gray to add the gray color.

Third, We don’t want the bullet points in front of each nested comment. To do that we’ll need to add the list class to the <ul> tag containing the nested comments.

Here’s what our template looks like when we’re done:

<!-- views/story-comment.html -->
<div class="b mid-gray">{{ vm.commentAuthor }} {{ vm.commentTime }}</div>
<div ng-bind-html="vm.commentText"></div>
<ul class="list mt0">
  <li class="pt2" ng-repeat="commentId in vm.commentKids">
    <story-comment id="commentId"></story-comment>
  </li>
</ul>

After we’ve made those changes, let’s reload http://localhost:8080/#/post?id=1 in the browser and see what our nested comments look like:

Hacker News Clone Nested Components to Display Comments after Tachyons styles are applied

Note that the comments look much more like threaded comments now!

Awesome! This is really starting to look like threaded comments now! Remember, though, that we’re just using one of this story’s comments as an example within our post view. So, we’ve got one final step to complete.

git add .
git ci -m "fixes styling issues in nested comments"

Adding the Rest of our Comments to the Post using another ng-repeat

All right, so we’re displaying one comment on the post and all of the comment replies to it. But, what about all of the other comments for the post? When we visit http://localhost:8080/#/post?id=1 in the browser, we want to see all of the comments related to the post with an id of 1. To display those, all we have to do is add another ng-repeat!

Before we do that, though, we need to add a property to our StoryController so that we have the array of comments belonging to the post matching the id parameter in our url. Once we have that array, we can create our ng-repeat to display all of the comments using our custom component.

// js/app/controllers/StoryController.js
(function() {
'use strict';

  angular
    .module('app')
    .controller('StoryController', StoryController);

  StoryController.$inject = ['TopStoriesService', '$stateParams'];
  function StoryController(TopStoriesService, $stateParams) {
    var vm = this;
    vm.id = $stateParams.id
    vm.story;
    vm.comments;

    activate();

    ////////////////

    function activate() { 
      
      TopStoriesService 
        .getStory(vm.id)
        .then(function(res) {
          vm.story = res.data; 
          vm.comments = res.data.kids
        })
      

    }
  }
})();

Now that we have vm.comments defined, let’s add it to our story-comments.html template to make sure we’re getting the array in our view when we visit http://localhost:8080/#/post?id=1:

<!-- views/story-comments.html -->
<h2>These are the comments</h2>
<story id="vm.id"></story>

<form>
  <p>
    <textarea name="comment" id="comment" cols="50" rows="6"></textarea>
  </p>
  <p>
    <button>Add Comment</button>
  </p>
</form>

{{ vm.story }}
<h3>Comments</h3>
{{ vm.comments }}
<story-comment id="15"></story-comment>

After we’ve added {{ vm.comments }} to our template file. Let’s make sure that we’re getting the array passed to the view. If it’s working, it should appear right below the Comments header and above the first comment:

Hacker News Clone Nested Components for Comments Comment IDs shown in Array

Notice that we have our array of comments right between the comments header and the first comment.

Great! So now that we have the array of comment ids in our post view, let’s replace our example StoryComment component (<story-comment id="15"></story-comment>) with an ng-repeat to loop through the array of comment IDs so we can display all of the post’s comments:

<!-- views/story-comments.html -->
<h2>These are the comments</h2>
<story id="vm.id"></story>

<form>
  <p>
    <textarea name="comment" id="comment" cols="50" rows="6"></textarea>
  </p>
  <p>
    <button>Add Comment</button>
  </p>
</form>

{{ vm.story }}
<h3>Comments</h3>
<ul>
  <li ng-repeat="commentId in vm.comments">
    <story-comment id="commentId"></story-comment>
  </li>
</ul>

Now, when we reload http://localhost:8080/#/post?id=1 in the browser, take a look!

Hacker News Clone Nested Components for Comments All Comments Displayed for First Time

We have all of the comments!

Now that we’ve added our second ng-repeat, we’re seeing all of the comments! To review, on a post’s show page, we have an ng-repeat displaying all of the comments belonging to that post. Then, within the template for each StoryComment component, we have an ng-repeat that displays all of the comments belonging to that comment. That way, we can have as many nested comments as we want!

git add . 
git ci -m "adds all related comments to post show page"

Fixing the Styles in our Comments

Notice that we have the same styling issues as we did in our nested comments within our story-comment.html template. Thankfully, we can reuse the same styles within the post show page template at story-comments.html to fix the problem:

<!-- views/story-comments.html -->
<h2>These are the comments</h2>
<story id="vm.id"></story>

<form>
  <p>
    <textarea name="comment" id="comment" cols="50" rows="6"></textarea>
  </p>
  <p>
    <button>Add Comment</button>
  </p>
</form>

{{ vm.story }}
<h3>Comments</h3>
<ul class="list mt0">
  <li class="pt2" ng-repeat="commentId in vm.comments">
    <story-comment id="commentId"></story-comment>
  </li>
</ul>

Let’s reload the page in our browser to make sure it’s looking how we expect:

Hacker News Clone Nested Components for Comments All Comments displaying and styled correctly

Now we’ve got our nested comments looking great!

Now that we have nested comments working let’s commit our code

git add . 
git commit -m "cleans up styles of nested comments on post show page"

Hooking the Top Stories Page up to the Post Show Page

Finally, let’s hook up our Top Stories Page to the Post Show page by creating some links using the ui-sref attribute from Angular UI Router.

First, we’ll want to add a link from the post show page back to the top stories page. To do that, let’s replace ‘Here are the comments’ within the <h2> tag at the top of the page with an anchor tag with the anchor text ‘Hacker News’:

<!-- views/story-comments.html -->
<h2><a href="" ui-sref="top">Hacker News</a></h2>

And now, we can click on the Hacker News link at the top of the page to return to the Top Stories view/state.

Next, we’ll want to add links from within the top stories page to each story’s show page where the comments will be visible. More specifically, we want to turn the number of comments into a link to the story’s show page. To do that, we can wrap our ng-pluralize directive that displays the number of comments in an anchor tag using the ui-sref attribute again.

This time, however, the syntax is a bit trickier. We want to pass in the post state as a value to our ui-sref attribute (ui-sref="post"). But, we also need to pass in the ID of the post that we’re linking to. Without the ID, the post show page won’t know which post’s data to display.

In order to send this information into ui-router, we need to pass an argument to post within the ui-sref attribute, treating it almost like post is a function instead of the name of the state. This argument is an object that you can think of like the $stateParams object. Within this object that we pass as a parameter, we want to define an id key and give it a value that corresponds with the id of the post we want to link to.

But, since we need to pass an expression to this attribute to grab the value of the id of the post we want to link to, we need to add an additional set of curly braces. We need to do this: {{ vm.id }}. Or, in full context: <a href="" ui-sref="post({ id: {{ vm.id }} })"><ng-pluralize...></ng-pluralize></a>

It can be a little tricky to see how the {{ vm.id }} value is actually set. But, if we look closely, we can see that the Story directive definition defines a few properties that create this connection. First, bindToController is set to true. Next, the controllerAs: 'vm' tells us that we’ll access bound properties using vm.property. Finally, scope: { id: '=id' } specifies both that our directive element accepts an attribute id and that the value passed into this attribute will be accessible within the directive template by using {{ vm.id }}.

To clarify which is which, if we changed the scope definition to scope: { storyId: '=id' } and we would have to use {{ vm.storyId }} to access the value passed into <story id="15"> within the story.html directive template.

(function() {
  'use strict';

  angular
    .module('app')
    .directive('story', story);

  story.$inject = ['TopStoriesService', 'getHoursAgoFilter', 'getDomainFromUrlFilter'];
  function story(TopStoriesService, getHoursAgoFilter, getDomainFromUrlFilter) {
    // Usage:
    //
    // Creates:
    //
    var directive = {
        bindToController: true,
        controller: StoryDirectiveController,
        controllerAs: 'vm',
        link: link,
        templateUrl: 'views/story.html',
        restrict: 'E',
        scope: {
          storyId: '=id'
        }
    };
    return directive;
    
    function link(scope, element, attrs) {
      var hideLink = element[0].querySelector('.hide');

      hideLink.addEventListener('click', function(event){
        element.parent().remove();
      });

      scope.$on('$destroy', function(){
        element.off();
      });
    }
  }
  /* @ngInject */
  function StoryDirectiveController (TopStoriesService, getHoursAgoFilter, getDomainFromUrlFilter) {
    var vm = this;

    TopStoriesService
      .getStory(vm.storyId)
      .then(function(res){
        ...
      })
  }
})();

Anyway, to add these link between the stories in the list of stories displayed in the Top Stories Page and the individual show pages where the comments are displayed, we need to open up the views/story.html file. You may recall that this file is the template for the custom Story directive that we created earlier in the series. The reason we need to open this file is that our custom story directive is responsible for fetching and displaying the content from the Hacker News API that we see when we open http://localhost:8080/#/top. And, that’s where the code that generates and displays the number of comments lives.

This is the part we need to change:

<!-- views/story.html -->
...
by {{ vm.author }} {{ vm.time }} | <a href="" class="hide">Hide</a> | <ng-pluralize count="vm.numComments" when="{'0': 'discuss', '1': '1 comment', 'other': '{} comments'}">

After we’re done, this part of the template file should look like this:

<!-- views/story.html --> 
...
by {{ vm.author }} {{ vm.time }} | <a href="" class="hide">Hide</a> | <a href="" ui-sref="post({ id: {{ vm.id }} })"><ng-pluralize count="vm.numComments" when="{'0': 'discuss', '1': '1 comment', 'other': '{} comments'}"></a>

Now, when we open up http://localhost:8080/#/top, we can see that the number of comments has become a link that takes us to the post show page. When we click on it, we can see our page that we built with nested components to display all of the comments.

Now that we’ve created this link between the top stories page and the post show page, let’s add our changes and commit them to version control:

git add . 
git ci -m "adds links between post show page and top stories page"

Final Thoughts

In this post, we’ve covered how to add threaded comments to the post show page by creating nested components within our StoryComment component’s template. In the next post, we’ll be going through how to add Routing Resolves to the application to preload data from the Hacker News API. We’ll be discussing the pros and cons of this approach and its apparent impact on performance.

Leave a Reply

Your email address will not be published. Required fields are marked *