Building a Hacker News Clone in AngularJS – Part 6

tutorial

Hi Everybody! Welcome to the final installment of our series on building a Hacker News clone in AngularJS. In this post, we’re going to discuss routing resolves and when we might want to add them to our application. For a quick review, let’s take a look at what we covered in the previous posts:

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, we covered displaying threaded comments by using nested components.

In Part 6 of our series, we’re going to be adding routing resolves to our application to make sure that we don’t transition to a story’s show page before we get the story’s data from the Hacker News API.

To do that, we’ll be going through how to create routing resolves to preload data from a service before hitting the controller. There are certain instances where this is more useful than others. Specifically, when we are wary of a promise failing, using resolves can be a better bet as we won’t load a page before we have the data needed to display it properly. If a promise within a routing resolve fails, the page stays in its current state.

In terms of user experience, it really feels much quicker not to use routing resolves if we can avoid it. The reason for this is that, taken to its extreme, the user won’t see anything until all of the data is returned from the API. If we happen to be making hundreds of API calls, we risk wasting a lot of user time.

For this app, we’ve made the majority of our API calls from within the directives and components that are actually displaying the data for each individual story and comment. The affect of adding routing resolves to this application is therefore quite minimal. Even though routing resolves don’t create a noticeable difference in our application, there are some other cases where it might be useful. The main reason to use a routing resolve is when we know that there is certain information which is vital to a particular application state. In this case, it’s better not to change state at all if we don’t get that information from the API.

If you do have a use case for preloading data in routing resolves, this post will be a good overview of how you might put them into place.

To add routing resolves to our application, we’ll need to complete the following tasks. We’ll go through how to do each of these tasks in this post:

  1. Adding a resolve property to the configuration object we pass to our state definition within app.js
  2. Defining a property in that resolve object that has a function as a value
  3. Passing necessary dependencies to our function and returning the information we need (in our case TopStoriesService and $stateParams
  4. Including the property (key) we defined in our resolve object within our StoryController as a dependency
  5. Accessing the return values of our routing resolve within our StoryController
  6. Defining properties on our view model object and assigning them the values we received from our routing resolves

Branching Out and Adding our Routing Resolves

In order to really tell the difference made by adding routing resolves, let’s check out a new branch before we start refactoring here:

git add. 
git co -b use-routing-resolves

Now that we’ve got a new branch created, it will be easy to switch back to master to compare the behavior of our app before and after the change. So, let’s get to it! Our first step is to add the resolve property to our state configuration object. We’ll set the value of this property equal to an empty object:

// js/app/app.js
  ...
  .state('post', {
    url: '/post/:id', 
    templateUrl: 'views/story-comments.html', 
    controller: 'StoryController as vm',
    resolve: {
      
    }
  })

Now, within this object we’re passing to our routing resolve, let’s add in another property. This property (or key) will be the name that we use within our StoryController to access the information we need later on. Let’s call this property story and we’ll pass it a function as a value:

// js/app/app.js
  ...
  .state('post', {
    url: '/post/:id', 
    templateUrl: 'views/story-comments.html', 
    controller: 'StoryController as vm',
    resolve: {
      story: function(){

      }
    }
  })

Within this function, we’re going to make our calls to the Hacker News API and the response will be stored in a variable called story. We’ll be able to access our story variable from within our StoryController later. In order to make our calls to the Hacker News API, we’ll need to use our TopStoriesService. Because we’re getting data from an individual story, we’ll need the story’s ID that we get from the URL. To access this value, we’ll also need to pass $stateParams as a dependency to our function. After we’ve included both dependencies, we can invoke the .getStory() function defined in our TopStoriesService and pass it $stateParams.id as a parameter. We’ll also need to make sure that we return the result:

// js/app/app.js
  ...
  .state('post', {
    url: '/post/:id', 
    templateUrl: 'views/story-comments.html', 
    controller: 'StoryController as vm',
    resolve: {
      story: function(TopStoriesService, $stateParams){
        return TopStoriesService.getStory($stateParams.id);
      }
    }
  })

Working with Data received From Routing Resolves

Now that we’ve added our routing resolves, we need to access the data they retrieved within our StoryController so that we can pass it to the story-comments.html view. Before we go about making our updates to StoryController.js, let’s take a look at its current state:

// js/app/controllers/StoryController.js
angular
  .module('app')
  .controller('StoryController', StoryController);
  
  StoryController.$inject = ['TopStoriesService', '$stateParams'];
  function StoryController(TopStoriesService, $stateParams) {  
    var vm = this;
    
    activate();
    
    //////////////////////////
    
    function activate() {
      vm.id = $stateParams.id;
      vm.story;
      vm.comments;

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

Notice within our call to the TopStoriesService that we’re setting vm.story equal to the value accessed by using dot notation on our response object. We use res.data to access the data property on our response object. We’ll need to keep this in mind when we access data from the response object associated with our routing resolve.

To refactor our controller to use data from our routing resolves, the first thing we need to do is to add the keys on our resolve object to our array of dependency injections. Recall that we used story as a key to access the function we defined within our routing resolve. So, we’ll need to add story to our array of dependencies:

// js/app/controllers/StoryController.js
angular
  .module('app')
  .controller('StoryController', StoryController);
  
  StoryController.$inject = ['TopStoriesService', '$stateParams', 'story'];
  function StoryController(TopStoriesService, $stateParams, story) {  
    var vm = this;
    
    activate();
    
    //////////////////////////
    
    function activate() {
      vm.id = $stateParams.id;
      vm.story;
      vm.comments;

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

Now, we need to reassign the values of vm.story and vm.comments to use the story variable that points to the value of our routing resolve object. After that, we can remove our call to the TopStoriesService from our controller:

// js/app/controllers/StoryController.js
angular
  .module('app')
  .controller('StoryController', StoryController);
  
  StoryController.$inject = ['TopStoriesService', '$stateParams', 'story'];
  function StoryController(TopStoriesService, $stateParams, story) {  
    var vm = this;
    
    activate();
    
    //////////////////////////
    
    function activate() {
      vm.id = $stateParams.id;
      vm.story = story.data;
      vm.comments = story.data.kids;
    }
  }

Because we’ve kept the same property names on our view model object (vm), the app should behave nearly identically when we reload one of the post pages. But, now that we’ve made these changes, We are preloading the data retrieved from the first call to the Hacker News API before the state changes.

Because there are many calls to the Hacker News API made after the first one has resolved, there isn’t a dramatic difference in the way the app feels after adding the resolve. The main difference, though, is that the post show page will never load before the data for that post has been retrieved. This is a plus, because without the data for that post, none of the other content on the page would be meaningful.

When to Use Routing Resolves

Again, this is not the best use case for seeing dramatic effects as a result of adding routing resolves, because most of our calls to the API are still being triggered after the view has loaded. This is because our Story directive and our story-comment component are both designed to make calls to the Hacker News API to fetch their own required data.

Still, adding this routing resolve ensures that we won’t change state to the post show page if we don’t get a response from the Hacker News API for that post. And, this is the real use case for adding routing resolves to an application. Within the resolve, we want to put any information that is absolutely essential to have before changing state to the new page. This way, we can ensure that the application won’t move to that page unless the absolutely required information is already loaded.

I would say it’s definitely worth reading John Papa’s thoughts on Route Resolves vs Controller Activate. The basic lesson from that article is to use a routing resolve when we want to cancel the route before ever transitioning to the view if the data is not retrieved. For our Hacker News Clone, we don’t want to load the show page if we can’t retrieve the story data. In that case, we would rather stay on the Top Stories Page.

All of that said, John Papa does also say that it is more common to place these sorts of calls within the controller’s activate function than within a routing resolve:

The controller activate makes it convenient to re-use the logic for a refresh for the controller/View, keeps the logic together, gets the user to the View faster, makes “busy” animations easy on the ng-view or ui-view, and feels snappier to the user. I find this to be the more common scenario, but both are valid depending on how the set of behavior fits your situation.

-John Papa, “Route Resolve and Controller Activate in AngularJS”

Another case where using resolves is essential is when we’re dealing with nested routes that require access to multiple properties within the $stateParams object. This example is taken from the angular-ui ui-router repository on github. Here’s what happens if we try to access more than one property on the $stateParams object that was defined in another state:

   
$stateProvider.state('contacts.detail', {
   url: '/contacts/:contactId',   
   controller: function($stateParams){
      $stateParams.contactId  //*** Exists! ***//
   }
}).state('contacts.detail.subitem', {
   url: '/item/:itemId', 
   controller: function($stateParams){
      $stateParams.contactId //*** Watch Out! DOESN'T EXIST!! ***//
      $stateParams.itemId //*** Exists! ***//  
   }
})
})

In this situation, they suggest using a resolve statement in the parent route:

$stateProvider.state('contacts.detail', {
   url: '/contacts/:contactId',   
   controller: function($stateParams){
      $stateParams.contactId  //*** Exists! ***//
   },
   resolve:{
      contactId: ['$stateParams', function($stateParams){
          return $stateParams.contactId;
      }]
   }
}).state('contacts.detail.subitem', {
   url: '/item/:itemId', 
   controller: function($stateParams, contactId){
      contactId //*** Exists! ***//
      $stateParams.itemId //*** Exists! ***//  
   }
})

As with all of the new concepts and tools you’ll learn about, the important thing is to keep practicing at developing your sense of which tools and concepts to apply to which problems. This sense, above all others, will save you the most time and make you exceedingly more efficient than simply knowing a lot!

Final Thoughts

Thank you for following along with me! This has been our first extended tutorial together and I hope you enjoyed learning about how to use AngularJS to build an application that interacts with an API. Please do let me know if there’s anything you’d like me to cover in a future series.

Kind Regards,

Dakota Lee Martinez

Leave a Reply

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