An Introduction to the Futuristic New Router in AngularJS

Ravi
Share


AngularJS is one of the most popular JavaScript MV* frameworks and is widely used to build single-page applications (SPA). One of the challenging features in SPAs is routing. Routing on the client side involves changing a portion of the view and creating an entry in the browser’s navigation history. As a fully featured client-side framework, AngularJS has always had support for routing via the the ngRoute module. Although this is good enough for basic routes, it doesn’t support more complex scenarios, such as nested views, parallel views or a sequence of views.

A new router for Angular 2 is currently in progress and it will be back-ported to Angular 1.4. In this article, we will see how the new router can be used to define routes and how it solves some of the problems that ngRoute couldn’t.

As already stated, work on the new router is still in progress at the time of writing this article and some of the APIs may later change. The Angular team hasn’t thought of a name for the new router yet, so for now it is called the futuristic router.

Limitations of ngRoute

ngRoute was not created with complex enterprise applications in mind. I have personally seen applications where certain portions of a page need to be loaded in several steps. Such applications can be built using ngRoute, but it is almost impossible to have a URL state for every single change applied to the view.

The ng-view directive can be used only once inside an instance of the ng-app directive. This prevents us from creating parallel routes, as we cannot have two parallel views loading at the same time.

The view template to be rendered inside ng-view cannot contain another ng-view directive. This prevents us from creating nested views.

The new router addresses these issues and provides a flexible way of defining and using routes. The new router also embraces the Controller as syntax . I highly recommend using the Controller as syntax, as it is one of the conventions to be followed today to get ready for Angular 2.

Creating Simple Routes

The new router is being created with Angular 2 in mind. Angular 2 will simplify dependency injection by eliminating the module config phase, which means that we don’t need to write a config block to define routes—we can define them anywhere.

Every route to be added to the new router consists of two parts:

  • path: URL of the route’s template
  • component: a combination of a template and a controller. By convention, both controller and template have to be named after the component

Routes are configured with the $router service . As $router is a service, we can define the routes anywhere in the app (other than in a provider or, config block). However, we need to make sure that the block of code defining routes is executed as soon as the app is loaded. For example, if the routes are defined in a controller (as we will do shortly), the controller has to be executed on page load. If they are defined in a service, the service method has to be executed in a run block.

Let’s define two simple routes and navigate between them using the new router. If you want to follow along with this code, you’ll need to grab a copy of the new router. Show me how.

You can install the new router on a per project basis via npm.

mkdir new-router && cd new-router
npm install angular-new-router

This will create a folder called node_modules in your project directory. The new router can be found at node_modules/angular-new-router/dist/router.es5.min.js. Include it in your project after AngularJS itself.

First off, let’s define a module and configure the routes:

angular.module('simpleRouterDemo', ['ngNewRouter'])
  .controller('RouteController', ['$router', function($router){
    $router.config([
      { path:'/', redirectTo:'/first' },
      { path:'/first', component:'first' },
      { path:'/second/:name', component:'second' }
    ]);

    this.name='visitor';
  }])

The controller in the above snippet defines three routes. Notice that the root route redirects to our first template and that the third route accepts a parameter in the URL. As you see, the syntax of specifying the parameter is same as it is for ngRoute.

As already mentioned, every component requires a corresponding view template and a controller. By convention, the name of controller should be name of the component suffixed with “Controller” (so firstController and secondController in our case). The name of the view template has to be the same as the name of the component. It also has to reside in a folder with the same name as the component, inside a folder named components. This would give us:

projectRoot/
  components/
    first/
      first.html
    second/
      second.html

These conventions can be overridden using $componentLoaderProvider. We will see an example of that later, but for now let’s stick to the conventions.

Next come the views for the components first and second used above. We’re defining them in-line using the ng-template directive (so that we can recreate a runnable demo), but ideally they should be in separate HTML files:

<script type="text/ng-template" id="./components/first/first.html">
  <h3>{{first.message}}</h3>
</script>

<script type="text/ng-template" id="./components/second/second.html">
  <h3>{{second.message}}</h3>
</script>

As the views are very simple, so are the controllers:

angular.module('simpleRouterDemo')
  .controller('FirstController', function(){
    console.log('FirstController loaded');
    this.message = 'This is the first controller! You are in the first view.';
  })
  .controller('SecondController', function($routeParams){
    console.log('SecondController loaded');
    this.message = 'Hey ' + $routeParams.name + 
      ', you are now in the second view!';
  });

As both of these controllers are created to be used with the Controller as syntax, they don’t accept $scope. The $routeParams service is used to retrieve the values of the parameters passed in the route.

Now, we need to load this controller to have the routes registered:

<body ng-app="simpleRouterDemo" ng-controller="routeController as route">
</body>

Finally, we need to link these routes and load them into the page. The new router brings the ng-link directive and ng-viewport directive, which link views and load templates respectively. The ng-viewport directive is similar to ng-view; it’s a placeholder for part of your app loaded dynamically based on the route configuration.

The following snippet shows the usage of these directives:

<div class="col-md-3">
  <ul class="nav">
    <li>
      <a ng-link="first">First</a>
    </li>
    <li>
      <a ng-link="second({ name:route.name })">Second</a>
    </li>
  </ul>
</div>
<div class="col-md-9">
  <ng-viewport></ng-viewport>
</div>

See the Pen Navigating Between Templates with Angular’s New Router by SitePoint (@SitePoint) on CodePen.

Dealing with Parallel Views

The directive ng-viewport can be used any number of times inside an application. Consequently, it is possible to define multiple parallel views on a page. The viewports have to have unique identifiers, so as to load components into them through the route definition.

The following HTML snippet shows how to include multiple viewport directives:

<div class="col-md-3">
  <ul class="nav">
    <li><a ng-href="#/{{route.name}}">First and Second</a></li>
    <li><a ng-href="#/swap/{{route.name}}">Second and First</a></li>
  </ul>
</div>
<div class="col-md-9">
  <div class="col-md-6" ng-viewport="left"></div>
  <div class="col-md-6" ng-viewport="right"></div>
</div>

Imagine we wanted to place this code into a folder called parallel and the view templates inside parallel/components. This would give us:

projectRoot/
  parallel/
    components/
      first/
        first.html
      second/
        second.html

As the organization of the code would be different from the convention (by default Angular looks for the the components folder in the project root), we need to tell the router to look for the views in a new folder. The following config block does this:

angular.module('parallelRouterDemo', ['ngNewRouter'])
  .config(['$componentLoaderProvider', function($componentLoaderProvider){
    $componentLoaderProvider.setTemplateMapping(function (name) {
      return 'parallel/components/' + name + '/' + name + '.html';
    });
  }])

Routes for this page have to load two components and display them in the different viewports. The configuration object uses the viewports’ unique identifiers to specify which view template is rendered where.

$router.config([
  {
    path: '/:name', component: {
      left: 'first',
      right: 'second'
    }
  },
  {
    path: '/swap/:name', component: {
      left: 'second',
      right: 'first'
    }
  },
  {
    path: '/', 
    redirectTo: '/there'
  }
])

See the Pen Dealing with Parallel Views Using Angular’s New Router by SitePoint (@SitePoint) on CodePen.

Managing the Lifecycle of Components

The new routing system allows us to intercept the lifecycle of components and control navigation. The interception can be applied by adding one of the following functions to an instance of the controller. These functions may return either Boolean values or, promises. A Boolean true or a resolved promise would pass through the lifecycle event and a Boolean false or a rejected promise would cancel further operation.

  • canReactivate: Indicates if a view can be re-activated. It can be used to persist the state of the view and optimize the loading time of the view upon subsequent requests.
  • canActivate: Runs before activating a component. Activates the component when a resolved promise or true is returned and cancels otherwise.
  • canDeactivate: Runs before deactivating a component. Unloads the component when a resolved promise or true is returned and cancels otherwise.

Using the new router’s lifecycle methods of makes life easier than it was in Angular 1.x, as we don’t need to handle the aggregated events on $scope to detect a lifecycle event.

Let’s examine how the canActivate method works by means of an an example. This method can be used to check if a user can access a view before loading it.

this.canActivate = function(){
  var hasAccess =  userAccessInfo.hasAccessToSecondComponent;
  if(!hasAccess){
    $window.alert('You don\'t have access to this view. 
      Redirecting to previous view ...');
  }
  return hasAccess;
};

The same goes for the canDeactivate method. This could be used to restrict a user from navigating away from a page with unsaved changes.

this.canDeactivate = function () {
  if (this.sampleText) {
    var alertResult = $window.confirm('You have unsaved changes. 
      Do you want to leave the page?');
    return alertResult;
  }

  return true;
};

See the Pen Managing the Lifecycle of Components by SitePoint (@SitePoint) on CodePen.

Note: You will notice that in some circumstances the alert and the confirm dialogue fires twice. This appears to be a bug in the router and I have opened an issue on GitHub.

Conclusion

Angular’s new router has a promising set of features which make it a perfect fit for almost any complex scenario. Although work on the router is still in progress, it is worth experimenting with it today as, in addition to all the nice features it brings, the new router promises to make the transition to Angular 2 easier.