Adventures in AngularJS: Routing

As with many frameworks, AngularJS gives you a lot of architectural responsibility, which means you should be making a lot of decisions before you ever sit down to build your application. As you approach using Angular for projects in your organization, you need to wear the Architect hat to implement the building blocks of the framework that will support your app, your development process, and your team. By doing this, you can can define best practices up front, so your team doesn’t have create conventions on the fly.

This is the second in a series of posts sharing the conventions our own Business Engineering team has adopted as we’ve learned to work with AngularJS. Last time we covered how our team is using Computed Properties to transform the state into new values while the user interacts with the interface. Now we’ll turn how routes can make your life easier while building client-side apps in Angular.

First, let me introduce ¡Hola, Apps! (vicentereig/hola-apps), a learning aid and sample Angular app I built to search iOS apps in the iTune Store.

hola, apps

In this post, I’ll drive you through the most relevant modules in ¡Hola, Apps! to build a Master-Detail view that searches on the server-side while the user types the name of an application. We’ll cover:

  1. How to define a state machine that represents the user’s experience,
  2. How to serialize the application state in the URL,
  3. How routes make your life easier and your controllers skinnier when loading data from remote sources,
  4. What nested views are and how often we encounter them in our digital life.

An Introduction to Routes

Routes, also known as the Mediating Controller, stitch parts of your app together — data, controller, and a view — while keeping the data loading concern and the user interface separate. They answer questions such as “what is my application state?”:

  • What are my favorite songs in ¡Hola, Playlist!? You click on /starred
  • What applications are related to fitness in ¡Hola, Apps!? You search for “fitness” /apps?term=fitness

Single-Page Application development aims to bring the fluid user experience we find in native or desktop application to web apps. However, we are building websites, where we have the big advantage of providing deep links to our app through the URL. Routes do precisely that: they help us materialize the state in the URL so we can later reconstruct it when a user navigates to a page.

Routes also help you organizing your codebase into smaller feature-oriented modules, distributing different concerns among your architecture components.

Nested Views and States: Building a Master-Detail View

It’s hard to experience the web and mobile apps these days without using Master-Detail views: navigation bars, chat rooms or search engines. The Master view usually displays a collection of items, and the detail view is responsible for showing an augmented version of the selected item.

¡Hola, Apps! uses a master view to show a text field to let the user type an iOS app they want to find, and a detail view showing the results for the current search. The Detail view is only concerned about loading a bunch of records from the server-side and displaying them. The Master view governs its lifecycle by telling it when to reload data from the server and re-render the detail view, showing a loading indicator in the meantime. That’s what routes are meant for.

The root state #/ is initially the master view containing a text field to let the user type the name of an app, accompanied by an empty detail view.

Master/detail view

As the user types, the application transitions to the state in charge of retrieving the search results from the server side, updating the URL in turn and showing a loading indicator while the search results are delivered.

Once the results arrive, the same state renders them into the Detail View.

master/detail view 2

Today we’ll be using dotJEM/angular-routing in conjunction with angular-rails-templates. This is the best routing library I have found after using ng-route, and ui-router. (Although nothing compares to the Ember Router.)

The State Machine

The user experience is often modeled as state machine. Whenever the user clicks a link, types a URL in the browser, or performs an action within your app, it evolves to a new state to deliver the answer or content they’re looking for.

Each state is associated with an URL and is composed of a resolve section and a set of views. Each of these views are made out of a controller and a template, and they expect to be rendered in an outlet in the enclosing template.

All this starts by initializing our Angular app in the HTML generated on the server side:


<!-- app/views/ng_application/index.html.erb -->
<div class="container" ng-app="HolaApps">
 <div jem-view="application"></div>
</div>

The main outlet is where the whole client-side app will be rendered.

Hola, Apps.</h1>
       <p>A search engine for the iTunes App Store. <a class="btn btn-primary pull-right" href="https://github.com/vicentereig/hola-apps" target="_blank">Fork it.</a></p>
       <div class="clearfix"></div>
       <hr/>
   </div>
</div>

<div class="row">
   <div class="col-md-12">
       <form role="form">
           <div class="form-group">
               <input type="text" focus="true" ng-model="term" placeholder="Enter an iOS app name..." class="input-lg form-control"/>
           </div>
       </form>
   </div>
</div>

<div jem-view="index" loader="ng_application/templates/loading.html"></div>

The main route, application, is responsible for filling the main outlet.


HolaApps.config(['$stateProvider', function($stateProvider){
   $stateProvider.state('application', {
       route: '/',
       views: {
           application: {
               controller: 'ApplicationController',
               template: 'ng_application/templates/application.html'
           }
       }
   });
}]);

These outlets are defined by the directive jem-view.


<jem-view name="application"></jem-view>

States are organized into a hierarchy, where the parent coordinates what children are going to render. Children’s controllers can access the parent’s $scope, along with the computed properties defined in it.


HolaApps.config(['$stateProvider', function($stateProvider){
   $stateProvider.state('application.index', {
       route: '/apps',
       views: {
           index: {
               controller: 'AppsIndexController',
               template: 'ng_application/templates/apps/index.html'
           }
       }
   });
}]);

Finite state machines often have an initial state that acts as the entry point. In the web, however, every state is potentially an initial state: any URL can be an entry point. That’s where the hierarchy comes in handy. If you happen to land directly in a child view, and the parents hasn’t been rendered yet, it will be automatically rendered. For example, when you browse to /apps?term=futurestack, both states, application and its child application.index, are rendered. Once the app has been loaded, if you keep searching for iOS applications, only the child state will be reloaded and rendered.

Routes and the Resolve Section

One of the ways a route mediates in your application is by loading data. You can define dependencies in the resolve section that will be injected normally in the controller in charge of rendering the view.


HolaApps.config(['$stateProvider', function($stateProvider){
   $stateProvider.state('application.index', {
       route: '/apps',
       views: {
           index: {
               controller: 'AppsIndexController',
               template: 'ng_application/templates/apps/index.html'
           }
       },
       resolve: {
           // 3. we trigger a search every time we transition to this state
           apps: ['$to', 'DataStore', function($to, store){
               store.cancelAll('software', {term: $to.$params.term});

               if (!$to.$params.term) {
                   return [];
               }

               return store.findAll('software', {term: $to.$params.term});
           }]
       }
   });
}]);

The state hierarchy comes again to the rescue if you happen to land in a child state for the first time: the parent’s resolve section will retrieve and resolve any dependency you have defined, and the child state’s resolve section will do the same before rendering their views.

How ¡Hola, Apps! Is Laid Out

I tried to materialize all our learnings from this year into ¡Hola, Apps!, which demonstrates how routing changed the way we name things, the way we show data to the user, and the way we structure our files. Let’s walk through how we’ve handled each of these in the app (plus one bonus search feature!).

Hierarchy and Naming Conventions

Relying on structured naming conventions is extremely useful to define the hierarchy, so make sure to think about how you’ll handle naming things beforehand. Here’s a list of states in our sample app where’re we’re putting naming conventions to user:

naming conventions

You can see where the hierarchy is going. Let’s assume we want to implement a view to details about a particular app. You would at least add the follow route, controller, and template, as below:

naming conventions 2

The root state in our hierarchy from above, application, just contains a view of the root controller and the template defining the application layout. Here’s how you would implement it:


HolaApps.config(['$stateProvider', function($stateProvider){
   $stateProvider.state('application', {
       route: '/',
       onEnter: ['$state', function($state){
           $state.goto('application.index');
       }],
       views: {
           application: {
               controller: 'ApplicationController',
               template: 'ng_application/templates/application.html'
           }
       }
   });
}]);

Any child state defined under application will be rendered into that outlet. In this case, we’ll render the application.index state, which will provide us with a text field to search on the server side, and a nested view named index. This nested view will show the search results.

A route would be in charge of contacting the server to retrieve the details about a certain app, while the controller would modify the model and provide the user with a view exposing the data and possible paths of interaction. In this case, the application.index will retrieve the result set from a REST API based on the contents of the query params, as we will see later on.

Hierarchy and Displaying Data

Now we’ll take a look at how we use the dotJEM/angular-routing library to drive the state throughout our apps; it’s pretty awesome! Using the jem-view directive, it allows you to define states per route and define the outlets in your templates where each child state is going to be rendered.

Our client-side app defines the layout in the application.html template:


// application.html
<div class="row">
   <div class="col-md-12">
       <h1>Hola, Apps.</h1>
       <p>A search engine for the iTunes App Store.</p>
       <div class="clearfix"></div>
       <hr/>
   </div>
</div>

<div class="row">
   <div class="col-md-12">
       <form role="form">
           <div class="form-group">
               <input type="text"
                      focus="true"
                      ng-model="term"
                      placeholder="Enter an iOS app name..."
                      class="input-lg form-control"/>
           </div>
       </form>
   </div>
</div>

<div jem-view="index"
    loader="ng_application/templates/loading.html"></div>

On every routing state you need to define which controller and template is going to be rendered in the index outlet. You can also specify which template to render while the state is waiting for data to arrive from the server:


HolaApps.config(['$stateProvider', function($stateProvider){
   $stateProvider.state('application.index', {
       route: '/apps',
       views: {
           index: {
               controller: 'AppsIndexController',
               template: 'ng_application/templates/apps/index.html'
           }
       },
       resolve: {
           apps: ['$to', 'DataStore', function($to, store){
               store.cancelAll('software', {term: $to.$params.term});

               if (!$to.$params.term) {
                   return [];
               }

               return store.findAll('software', {term: $to.$params.term});
           }]
       }
   });
}]);

Additionally, you can define each state resolve section. In the code above, I’m hiding all the $http handling behind a Data Store. Long story made short, DataStore.findAll returns a promise which will be resolved once /software?term=new+relic sends back the response. In the snippet above, we are:

  1. Defining a new dependency that can be injected in the AppsIndexController: apps.
  2. Injecting the $to object, which represents the destination state to be able to access the parameters in the URL associated to it.
  3. When the term query param isn’t present, we are returning a resolved value: an empty Array.

At this point you can visit /apps?term=facebook and find all the apps that are related to Facebook! The route will contact the server asking for the list of apps, and they’ll be rendered as soon as they arrive. In the meantime, the loading.html view will be rendered indicating the user to wait till the final state is rendered.

Hierarchy and Project Directory Structure

It’s also important to pay attention to your file structure; AngularJS doesn’t actually tell you how to structure the files in your project, so you have to find your own solution. The state hierarchy can offer you guidance on how to separate concerns (routes, templates, and controllers), leading to smaller files and components. Here are the guidelines we worked within:

  • Let the routes load the essential models for a given state.
  • Let the controllers manipulate those models.
  • Let the controllers governing a directive life-cycle request data asynchronously that is not essential and shouldn’t block the rendering cycle for the current state.

To see this in action, here’s ¡Hola, App!’s file structure:

hola apps file structure

You could go even further and group into modules directives, controllers, and routes by the feature they belong to, just like any third-party library provides functionality at different levels: data store, model definition, or complex directives. To some extent, GUI applications are made out of smaller building blocks that also follow the MVC architecture.

What about implementing a search-as-you-type feature?

hola-apps-search-as-you-type
Good news is that we’re almost there. The application.index state knows how to load the result sets from the server and trigger Angular’s render loop, so the only thing left to do is to reload application.index with the updated query params every time the user types a character into the search field.

We can use a computed property bound to the input field to keep track of what the user’s typing: $scope.term. We’ll use Angular’s ng-model.


<input class="input-lg form-control" type="text" placeholder="Enter an iOS app name..." />

You can see there also the AppsIndexController contains a computed property, aggregateSelectedApps, that feeds the directive that lists the selected apps. Here’s how I am passing the computer property to the follow-apps directive in the context of the AppsIndexController:


<follow-apps apps="selectedApps"/>

Now you’ve got a search engine that updates the application state as you type!

Putting it all together

Hopefully you’ve gotten a sense of how using routes can make your life easier when you’re building that client-side AngularJS app. I’ve also driven home the point that you should be making architectural decisions upfront to be implemented as part of the custom framework you’ll build your apps upon. But how can you adopt approach this in your organization?

Before jumping into any code with a particular app — actually, during our standard UX/UI review — our team defines the routing hierarchy and structured naming convention to organize our routes, controllers, and templates. This is also when we’ll decide on the essential information that needs to be loaded by the route, deferring all the non-essential pieces of information to directives contained in that page. We’ve also found it helpful to sketch different project directory structures that will conform to the architecture decisions we have made. Doing things up front is really the key, so look to build processes into your usual project review that include architectural decision-making as early as possible.

Now that we’ve covered conventions to use with Computed Properties, and now Routing, our last Angular adventure will cover communication mechanisms between controllers, services, and directives. See you next time!

Vicente Reig Rincón de Arellano is a senior engineer in New Relic's Business Engineering team. Prior to joining New Relic, he worked building massively concurrent systems, data integration architectures, and GUIs in a wide variety of technologies such as Ruby, Java, Objective-C, and Javascript. View posts by .

Interested in writing for New Relic Blog? Send us a pitch!