Guide: Route to Component

UI-Router v1.0 for AngularJS (1.x) introduced the ability to route directly to AngularJS 1.5+ components.

Although UI-Router still allows views to be defined as combinations of template and controller, we highly recommend that the AngularJS 1.5 component model is adopted instead.

In this guide, we’ll cover:

  • AngularJS 1.5 component concepts
  • how to route to AngularJS 1.5 components
  • how to supply components with resolved state data
  • how to communicate between routed components

AngularJS 1.5 .component()

An AngularJS 1.5 component is a reusable piece of a web page, which encapsulates both structure (markup) and behavior. A component is self-contained. Because it uses an isolate scope, it does not access $scope values from parent elements, nor does it provide values on $scope for children components to access.

A component gets its input data from a parent component using an explicit bindings to properties through an attribute in the parent’s template. Any outputs (for communication to the parent) are provided as event callbacks, likewise as attributes in the parent template.

<some-component
    input-binding="$ctrl.parentData"
    output-binding="$ctrl.handleOutput()"
></some-component>

In addition to inputs and output bindings, a component also has well defined lifecycle hooks.

The component model from AngularJS 1.5 aligns closely with the component model from Angular (2+). At a high level, it even has much in common with React or Web Component models. This parity between frameworks allows you to reason about application structure in a similar way, no matter which framework you are using.

To learn more about AngularJS 1.5 components, refer to the official docs and read a blog or two.

UI-Router Legacy

In legacy UI-Router (version 0.x.x), views can only be defined using a template and a controller. The template defines the DOM layout. The controller injects any resolve data and/or services, and implements logic required to make the view work.

A typical legacy (non-component) state definition might look like this:

.state('userlist', {
  url: '/users',
  template: '/partials/users.html',
  controller: 'UsersController',
  resolve: {
    users: function(UserService) {
      return UserService.list();
    }
  }
});

In userController.js, the resolved users list is injected and placed onto the $scope for the template to use. Additional services are also injected to wire up desired behavior in the view.

//                                    resolve,  scope, service
.controller('UsersController', function(users, $scope, SomeService) {
  $scope.users = users;

  $scope.clickHandler = function() {
    return SomeService.someFn();
  }
});

In /partials/users.html template, the data and functions bound to the $scope are used to render the page.

<h1>Users</hi>

<button ng-click="clickHandler()">Do something</button>

<ul>
  <li ng-repeat="user in users">
    <a ui-sref="users.detail({ userId: user.id })"
        ng-class="{ active: user.active }">
      {{ user.name }}
    </a>

    <button ng-click="user.active = !user.active">
      {{ user.active ? "Deactivate" : "Activate" }}
    </button>
  </li>
</ul>

<div ui-view></div>

Legacy templates and controllers can directly access and mutate scope variables from parent controllers. In the same way, a controller’s $scope variables are accessible by its nested children. You may be tempted to use this pattern to share data up and down the DOM tree. However, sharing in this way makes it difficult understand where the data comes from, or know what other code might modify the data.

It’s also common to see a poorly designed view built as a single template with lots of markup, and a single controller with too many responsibilities.

Example Plunker

The legacy example is embedded as a plunker:

Migrate to components

To begin routing to components, the view is converted from a template and controller to an AngularJS 1.5 component. Creating a component from a template and controller will explicitly couple the markup and logic as a single, cohesive unit. The component can then be used as a logical building block for the application.

The component model enforces separation of concerns and encapsulation by using an isolate scope. Data from parent scopes can no longer be directly accessed. Instead, it needs to be explicitly wired in. This makes it easier to understand where the data comes from.

Create a component

  • Create a .component which uses the template and controller from the state definition.
  • Instead of putting variables and functions on $scope, put them directly on the controller using this.
  • In the template, reference the controller using $ctrl. This convention for components is effectively the same as controllerAs: '$ctrl'.
  • Instead of injecting resolve data into the controller, use a one-way component input binding, i.e., <.

Note that global services such as SomeService are still injected into the component’s controller.

// This component can be used like: <users></users>
.component('users', {
  bindings: {
    // one-way input binding, e.g.,
    // <users users="$parentCtrl.userlist"></users>
    // automatically bound to `users` on the controller
    users: '<'
  },

  controller: function() {
    this.clickHandler = function() {
      alert('something');
    };

    this.toggleActive = function(userId) {
      var user = this.users.find(user => user.id == userId);
      if (!user) return;
      user.active = !user.active;
    };
  },

  // implicit controllerAs: '$ctrl',
  template: `
    <h1>Users</h1>

    <button ng-click="$ctrl.clickHandler()">Do something</button>

    <ul>
      <li ng-repeat="user in $ctrl.users" ui-sref-active="userselected">
        <a ui-sref="userlist.detail({ userId: user.id })"
            ng-disabled="!user.active"
            ng-class="{ deactivated: !user.active }">
          {{ user.name }}
        </a>

        <button ng-click="$ctrl.toggleActive(user.id)">
          {{ user.active ? "Deactivate" : "Activate" }}
        </button>
      </li>
    </ul>

    <div ui-view></div>
  `,
});

Notice that the component renders the list of users and also places a nested <ui-view>. When a nested states is activated, its component will populate the <ui-view>.

We use ES6 multi-line strings in this example for convenience. We also show the controller and template in-line (you can choose to use templateUrl and globally registered controllers, i.e., controller: 'FooController').

Components should avoid injecting $scope. However, injecting $scope may still be necessary in some cases, such as using $scope.$watch or $scope.$on.

Update the state definition

In the state definition, replace the template/controller properties with a component property which refers to the name of the new component.

.state('userlist', {
  url: '/users',
  component: 'users', // The component's name
  resolve: {
    users: function(UserService) {
      return UserService.list();
    }
  }
});
  • When activating userlist, UI-Router begins routing to the users component.
  • After the state’s users resolve is fetched, the transition to userlist is successful.
  • The users component is created. The resolved value for users is bound to the component’s users input binding.
  • Finally, the component controller is injected and instantiated by angular.

Resolve values are bound to the routed component’s controller using the component’s input bindings. Global services are still injected into the controller.

Resolve Bindings

UI-Router automatically binds resolve data to a matching component input. In the example, the userlist state has a resolve called users. When ready, the resolved users data is bound to the component’s users input binding.

Look at the state and component definitions again:

.state('userlist', {
  url: '/users',
  component: 'users', // The component's name
  resolve: {
    users: function(UserService) {
      return UserService.list();
    }
  }
});
.component('users', {
  bindings: {
    // input binding from the component's `users` attribute
    // to the internal `users` property of the controller
    users: '<'
  }
  ...

The resolve named users is automatically bound to the users input of the component and placed on the controller.

Resolves can be used to bind parameter values to a component:

.state('userdetail', {
  url: '/user/:userId',
  component: 'user',
  resolve: {
    userId: function($transition$) {
      return $transition$.params().userId;
    }
  }
});
.component('user', {
  bindings: {
    userId: '<'
  }
  ...

Or if you prefer fetching parameter based data using a resolve:

.state('userdetail', {
  url: '/user/:userId',
  component: 'user',
  resolve: {
    user: function($transition$) {
      return UserService.getUser($transition$.params().userId);
    }
  }
});
.component('user', {
  bindings: {
    user: '<'
  }
  ...

In some cases, the resolve name may not exactly match the component input name. The mapping between resolve name and component input can be customized using a bindings: object on the state (or view) declaration.

.state('userlist', {
  url: '/users',
  component: 'users',
  // the `users` input binding on the component receives
  // the `userlist` resolve value (from the state)
  bindings: { users: 'userlist' },
  resolve: {
    userlist: function(UserService) {
      return UserService.list();
    }
  }
});
.component('users', {
  bindings: {
    users: '<'
  },
  ...

The bindings: object on the state customizes the mapping of a resolve value to a component input. An AngularJS 1.5 .component() also has a bindings: object which maps component inputs to controller properties. Both these bindings properties map values, but be aware of the differences.

Example Plunker

The route-to-component example is embedded as a plunker:

Accessing $transition$

The UI-Router Transition object contains all the information about the transition which activated a routed component. When routing to a legacy style template/controller (not routing to components), the transition could be injected as $transition$.

// legacy
function MyController($transition$) {
  let to = $transition$.to();
  let toParams = $transition$.params("to");
  let from = $transition$.from();
  let fromParams = $transition$.params("from");
  // ... do some stuff
}

We learned when routing to components, instead of injecting resolve values, you bind them using bindings: { myResolve: '<' }. In the same way, to access the Transition on a routed component, you should bind to $transition$ in your component.

.component('myComponent', {
  bindings: { $transition$: '<' },
  controller: function() {
    this.$onInit = () => {
      let to = this.$transition$.to();
      let toParams = this.$transition$.params("to");
      let from = this.$transition$.from();
      let fromParams = this.$transition$.params("from");
      // do some init stuff
    }
  },
  template: 'my template'
}

Binding to resolves (and $transition$) only works if the component is a routed component. That is, the component must be referenced in a state definition. If you need to pass resolve data down to non-routed child components, propagate the data using standard component bindings.

Named views/View Targeting

A named ui-view can be targeted by a component. As usual, the views: property on a state declaration is used to target the named ui-view(s). Each key on views: targets a specific named ui-view. The value for a key is the component that should be loaded into that named ui-view. See View Targeting Details in the API docs for details.

.state('fooState.barState', {
  views: {
    // When `fooState.barState` is active, `fooComponent`
    // is loaded into the <ui-view name="content" />
    // which was created by `fooState`.
    "content@fooState": "fooComponent"
  }
  ...
});

The component receives resolve data bindings, just like with unnamed components. To customize bindings for named views, replace the name of the component with an object containing the component name and a bindings object.

.state('fooState.barState', {
  views: {
    // When `fooState.barState` is active, `fooComponent`
    // is loaded into the <ui-view name="content" />
    // which was created by `fooState`.
    // The component's `foo` input receives `fooData` resolve
    "content@fooState": {
      component: "fooComponent",
      bindings: { foo: 'fooData' }
    }
  },
  resolve: {
    fooData: FooService => FooService.get()
  }
  ...
});

Break out more components

You are routing to components! However, don’t stop there!

AngularJS 1.5 components offer a simpler mental model for composing a UI out of nested components. If your existing code has a monolithic template and controller, take this opportunity to break it down into smaller components.

In our example, the users components renders a list of users, renders a toggle button, and links to each user’s details. We can extract the link rendering and logic from the users into a separate userLink component.

The userLink component will be a “dumb component”, meaning it only renders its inputs and emits events. It doesn’t “own” any of its data. Because of this, it also should not mutate the input data. Instead, it exposes an output, in the form of a event callback.

Our new userLink component looks like this:

.component('userLink', {
  bindings: { user: '<', onToggleActive: '&' },
  template: `
    <li ui-sref-active="userselected">
      <a ui-sref="userlist.detail({ userId: $ctrl.user.id })"
          ng-disabled="!$ctrl.user.active"
          ng-class="{ deactivated: !$ctrl.user.active }">
        {{ $ctrl.user.name }}
      </a>

      <button ng-click="$ctrl.onToggleActive({ userId: $ctrl.user.id })">
        {{ $ctrl.user.active ? "Deactivate" : "Activate" }}
      </button>
    </li>
`,
})

The new userLink component has an input binding for the user value, and exposes a callback called onToggleActive. The parent component (users) is responsible to bind the user data, and handle the on-toggle-active event.

It may have been easier to allow the userLink component to mutate the user.active property. However, by writing userLink as a “dumb component”, the users component retains ownership of the data.

Note that the userLink component has no controller. Instead of mutating the user object, it calls the onToggleActive output bindings (&) when the button is clicked.

.component('users', {
  bindings: { users: '<' },
  controller: function() {
    this.clickHandler = function() {
      alert('something');
    }

    this.toggleActive = function(userId) {
      var user = this.users.find(user => user.id == userId);
      if (!user) return;
      user.active = !user.active;
    };
  },
  template: `
    <h1>Users</h1>

    <button ng-click="$ctrl.clickHandler()">Do something</button>

    <ul>
      <user-link ng-repeat="user in $ctrl.users" user="user"
        on-toggle-active="$ctrl.toggleActive(userId)">
     </user-link>
    </ul>

    <div ui-view></div>
`,
})

Now the users component binds the current user to each userLink component. It also wires up its toggleActive controller method to the userLink’s onToggleActive output.

Note that attribute bindings always use kebab-case. The onToggleActive binding is wired as on-toggle-active.

Example Plunker

The route-to-component example with the userLink “dumb component” is embedded as a plunker:

Routed parent/child component communication

Now we have the userlist state which routes to the users component. The users component renders a list of userLink components and their wires inputs and outputs. The userLink component accepts a users input, emits onToggleActive events, and renders the user link.

The userlist.detail nested state routes to the userDetail component. The userDetail component is placed inside the <ui-view> that the users component created. The userDetail component accepts a user input (from a resolve) and renders the details of the user.


Let’s add a nested state which allows editing of a user. The userEdit component will also be a “dumb component”. When a user is edited, it will emit an event to inform the parent component (users). The users component will handle the event by updating the list of users with the new data, then activating the userlist.detail state.

.component('userEdit', {
  bindings: {
    originalUser: '<',
    onUserUpdated: '&',
    onEditCancelled: '&'
  },

  controller: function() {
    this.$onInit = function() {
      // make a copy of the user
      // (don't live edit the parent's model)
      this.user = angular.copy(this.originalUser);
    }
  },

  template: `
    <h3>User {{ $ctrl.user.id }}</h3>

    Name: <input ng-model="$ctrl.user.name"><br>
    Active: <input type="checkbox" ng-model="$ctrl.user.active"><br><br>

    Address: <input type="text" ng-model="$ctrl.user.address"><br>
    Phone: <input type="text" ng-model="$ctrl.user.phone"><br>
    Email: <input type="text" ng-model="$ctrl.user.email"><br>
    Company: <input type="text" ng-model="$ctrl.user.company"><br>
    Age: <input type="text" ng-model="$ctrl.user.age"><br><br>

    <button type="button" ng-click="$ctrl.onUserUpdated({ user: $ctrl.user })">Update user</button>
    <button type="button" ng-click="$ctrl.onEditCancelled()">Cancel</button>
`,
})

The userEdit component binds the input data as originalUser, then makes a copy for editing. It has two output bindings: onUserUpdated and onEditCancelled. The parent state’s users component needs to wire those bindings to its own code.

When users wires up the inputs/outputs to the userLink components, it does it directly on the child components it creates:

<user-link user="$ctrl.user" on-toggle-active="$ctrl.toggleActive(userId)"></user-link>

However, we now we need to wire a callback for a routed component (userEdit) to the users component. The <ui-view> that the users component created is filled by the userEdit component when the edit state is active. To wire inputs and outputs, wire the callback bindings to the <ui-view> itself.

<ui-view on-user-updated="$ctrl.handleUserUpdated(user)"></ui-view>

When the routed component is activated, its declared bindings are inspected. If a binding declared on the routed component is also wired on the <ui-view>, then the routed component will receive the binding.

The users component is a “smart component” because it is responsible for handling the events from the “dumb components”. Its controller contains the business logic to manage the application state.

.component('users', {
  bindings: { users: '<' },
  controller: function($state) {
    this.clickHandler = function() {
      alert('something');
    }

    this.findUser =   function(userId) {
      return this.users.find(user => user.id == userId);
    }

    this.toggleActive = function(userId) {
      let user = this.findUser(userId);
      if (!user) return;
      user.active = !user.active;
    };

    this.showUserDetail = function() {
      // Parameter values (userId) are kept
      // because `.go()` uses `{ inherit: true }`
      $state.go('userlist.detail')
    }

    this.handleUserUpdated = function(updatedUser) {
      let currentUser = this.findUser(updatedUser.id);
      let idx = this.users.indexOf(currentUser);
      if (idx !== -1) {
        this.users[idx] = updatedUser;
        // Go go `detail` state.
        // Reload the state, so the `user` resolve is refreshed
        $state.go("userlist.detail", null, { reload: 'userlist.detail' })
      }
    }
  },
  template: `
    <h1>Users</h1>

    <button ng-click="$ctrl.clickHandler()">Do something</button>

    <ul>
      <user-link ng-repeat="user in $ctrl.users" user="user"
        on-toggle-active="$ctrl.toggleActive(userId)">
     </user-link>
    </ul>

    <div ui-view
      on-user-updated="$ctrl.handleUserUpdated(user)"
      on-edit-cancelled="$ctrl.showUserDetail()"
    ></div>
`,
})

Example Plunker

The full final example is embedded as a plunker:

Resources

Updated: