Using ui-router as a Component Router

Intro

In Angular 1.5 we got the Component, it is an improvement on our old pal the Directive. In fact a Component is just a Directive with some default settings, and a nicer API.

Here is a great blog post by Todd Motto which explains Components in dept, and compares them to ordinary directives.

A tale of two Components

Components have one other nice feature, they are closely aligned to the way Angular 2.0 will work. In fact if you squint your eyes you cannot really see the difference:

Here is a nameCard Component in Angular 1.x:
 
 angular.module('myApp',[])
  .component('nameCard', {
    bindings: {
      name: "@"
    },
    controllerAs: 'nameCardController',
    template: `
      <h1>Hi my name is</h1>
      <h2>{{ nameCardController.name }}</h2>
    `
  });
 
Here is a nameCard Component in Angular 2.x:
 
@Component({
  selector: 'name-card',
  template: `
    <h1>Hi my name is</h1>
    <h2>{{ name }}</h2>
  `
})
export class NameCardComponent {
  @Input() name: String;
}
 
Here is how you use both of them:
 
 <name-card name="Maarten"></name-card>
 

Why use Components in Angular 1.5

So using Components in Angular 1.5 gets you pretty close the way Angular 2.0, works, which makes migrating to 2.0 a lot easier.

That is why we at 42 decided to use Components instead of Controllers and Element Directives (with restrict E), to prepare us for the migration ahead.

ui-router as a Component Router

But we hit one problem: we use ui-router, which is not geared towards using Components... or is it?

Turns out you can get ui-router to work with components quite easily:
 
 angular.module('myApp', ['ui.router'])
  .config(function($stateProvider) {
    $stateProvider.state('home', {
      url: '/greetings',
      template: '<name-card name="Maarten"></name-card>'
    })
  })
  .component('nameCard', {
    bindings: {
      name: "@"
    },
    controllerAs: 'nameCardController',
    template: `
      <h1>Hi my name is</h1>
      <h2>{{ nameCardController.name }}</h2>
    `
  });
 
Basically what we do is say that the template of the 'home' state is simply the use of a single Component. This way there is no need to define a controller and a separate template HTML file for the state.


Dealing with $stateParams

The first example hard-coded my name into the Component, what if we want to take a name from the URL instead, to make it more dynamic:
 
 angular.module('myApp', ['ui.router'])
  .config(function($stateProvider) {
    $stateProvider.state('home', {
      url: '/greetings/:name',
      template: '<name-card></name-card>'
    })
  })
  .component('nameCard', {
    controllerAs: 'nameCardController',
    controller: function($stateParams) {
      const nameCardController = this;

      nameCardController.name = $stateParams.name;
    },
    template: `
      <h1>Hi my name is</h1>
      <h2>{{ nameCardController.name }}</h2>
    `
  });
 
In this example we simply inject the $stateParams into the controller
for the nameCard Component, and use it to get the name from the URL.


Dealing with resolves

What if we have resolves in your states how do we deal with that? The answer is by using a 'route' controller like this:
 
 angular.module('myApp', ['ui.router'])
  .config(function($stateProvider) {
    $stateProvider.state('home', {
      url: '/greetings',
      template: '<name-card name="homeRouteController.name"></name-card>',
      controllerAs: "homeRouteController",
      controller: function(name) {
         const homeRouteController = this;
         homeRouteController.name = name;
      },
      resolve: {
        name: function() {
          /*
            Of course in real life this would get
            some data from some REST API.
          */
          return "Maarten";
        }
      }
    })
  })
  .component('nameCard', {
    bindings: {
      name: "<"
    },
    controllerAs: 'nameCardController',
    template: `
      <h1>Hi my name is</h1>
      <h2>{{ nameCardController.name }}</h2>
    `
  });
 
The job of a 'route' controller is acting purely as glue between the resolve and the Component. Its job is to inject all the 'resolves' and them on the scope, so that the template can access them.

Also note that the binding in the Component uses a one way binding (<) now instead of an interpolation binding (@), because name is now a variable instead of a string.


A Nuclear Weapon

There is one final weapon in our arsenal: the templateProvider, it allows you to dynamically create a template which ui-router will use.

We can implement the example from 'Dealing with $stateParams' with templateProvider like this:
 
 angular.module('myApp', ['ui.router'])
  .config(function($stateProvider) {
    $stateProvider.state('home', {
      url: '/greetings/:name',
      templateProvider: function($stateParams) {
        const name = $stateParams.name
        return `<name-card name="${name}"></name-card>`;
      }
    })
  })
  .component('nameCard', {
    bindings: {
      name: "@"
    },
    controllerAs: 'nameCardController',
    template: `
      <h1>Hi my name is</h1>
      <h2>{{ nameCardController.name }}</h2>
    `
  });
 

In the templateProvider you have access to all resolves and you can inject any service / factory that you need.

In this example we use it to create a route which passes the 'name' from the $stateParams as a string. This way the nameCard does not need to use a one way binding, but has an interpolation binding instead.

The reason I think this is a nuclear weapon is because you do not really need it. TemplateProviders hide the 'router' from your Component and I feel that this is dishonest. A Component should be aware that it represents a 'state' by injecting the $stateParams in its controller.

In Angular 2.0 when a Component needs a parameter from the route it will get it himself, see this blog post by Victor Savkin under the heading "Using Params". That is why I advocate always injecting the $stateParams in the Component so it is closer to best practices in Angular 2.0.

ui-router 1.0.0

In ui-router 1.0.0 it is going to be even easier to route to a Component, because it will be directly supported by ui-router. See this page: https://ui-router.github.io/tutorial/ng1/hellosolarsystem on how to do this.

Conclusion

Using Components in Angular 1.5 will make migrating easier.

Using ui-router as a Component router is really not that hard.

That is it, happy routing!