In this article, we review the traditional approach to UI Router states organization, and an opiniated alternative using AngularJS constants.

Hello, UI Router!

UI Router is a very well known AngularJS module. It is used by Ionic instead of AngularJS’ router by default, but you can (and should probably try to) use it for your Single-Page Applications.

Instead of routing by URL, you organize your controllers by state. States can be nested and share local services known as “resolves”. By the way, states can have multiple views (e.g. your root state app would have a menu and a content view).

1
+-------------- app --------------+
| +- menu -+  +---- content ----+ |
| |        |  |                 | |
| |        |  |                 | |
| |        |  |                 | |
| +--------+  +-----------------+ |
+---------------------------------+

Traditionally, we would declare this state as such:

config/routes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular
.module('app.config')
.config(routes);

function routes ($stateProvider) {
$stateProvider
.state('app', {
url: '/',
templateUrl: '/templates/app.html',
controller: 'AppController as app',
abstract: true,
resolve: {
user: 'CurrentUser'
}
});
}
controllers/app/app.controller.js
1
2
3
4
5
6
7
8
angular
.module('app.controllers')
.controller('AppController', AppController);

// `user` is the resolved `CurrentUser` service
function AppController (user) {
this.user = user;
}
templates/app.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="app">
<aside class="menu">
<div class="menu--greetings">
Hello, {{ app.user.fullname }}!
</div>

<!-- a menu that's customisable by a child state -->
<ui-view name="menu"></ui-view>
</aside>

<!-- main content -->
<main
class="content"
ui-view>

</main>
</div>

Note that we don’t name the content view intentionally. This is so that we can refer to it in child states without having to specify a view object:

config/routes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
angular
.module('app.config')
.config(routes);

function routes ($stateProvider) {
$stateProvider
.state('app', {
url: '',
templateUrl: '/templates/app.html',
controller: 'AppController as app',
abstract: true,
resolve: {
user: 'CurrentUser'
}
})
.state('app.profile', {
url: '/profile',

// since the content view is unnamed, we can do this:
templateUrl: '/templates/profile.html',
controller: 'ProfileController as profile'

// instead of this:
views: {
content: {
templateUrl: '/templates/profile.html',
controller: 'ProfileController as profile'
}
}
});
}

Feeling bloated?

Of course, the more your application grows, the more your routes function grows. Moreover, by declaring states like this, we kind of ruin the modularization: we’re puttin’ somewhat-but-not-always related code in one place, and at the same time resolves are not easily visible in the middle of this mess.

Instead of shoving my states in a config block, I’d rather have them sitting with their controller, and injected into the config function so that we can improve readibility:

config/routes.js
1
2
3
4
5
6
7
8
9
angular
.module('app.config')
.config(routes);

function routes ($stateProvider, AppState, ProfileState) {
$stateProvider
.state('app', AppState)
.state('app.profile', ProfileState);
}

Turns out there’s only two types of AngularJS objects that are injectable to a config block: service providers and constants. Guess how we’re going to declare our states?

states/app/app.state.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular
.module('app.states')
.controller('AppController', AppController)
.constant('AppState', {
url: '',
templateUrl: '/templates/app.html',
controller: 'AppController as app',
abstract: true,
resolve: {
user: 'CurrentUser'
}
});

function AppController (user) {
this.user = user;
}

This way, we can keep our state object right there with its controller and keep routing configuration in another file. It also allows us to see every declared state at a glance rather than having to grep \.state config/routes.js.

Sidenotes

Using $injector

AngularJS’ Dependency Injection system gets quite verbose, and nobody wants to maintain a 6+ arguments function. Luckily for us, the $injector service can be used to inject dependencies on the fly. So we can rewrite our config file:

config/routes.js
1
2
3
4
5
6
7
8
9
angular
.module('app.config')
.config(routes);

function routes ($stateProvider, $injector) {
$stateProvider
.state('app', $injector.get('AppState'))
.state('app.profile', $injector.get('ProfileState'));
}