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 thetemplate
andcontroller
from the state definition. - Instead of putting variables and functions on
$scope
, put them directly on the controller usingthis
. - In the template, reference the controller using
$ctrl
. This convention for components is effectively the same ascontrollerAs: '$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 theusers
component. - After the state’s
users
resolve is fetched, the transition touserlist
is successful. - The
users
component is created. The resolved value forusers
is bound to the component’susers
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
- AngularJS 1.5
.component()
docs - UI-Router
component:
docs - UI-Router
bindings:
docs - UI-Router
views:
docs - UI-Router sample application built with components
- Todd Motto’s Angular Style Guide