An agnostic, re-usable loading state with ui-router and Angular 1

I wrote this almost a year ago but never actually posted about it. Better late than never, and if you’re using Angular 1, it might be helpful.

Let’s say you want a state in ui-router that’s only purpose is to be an intermediate between two different states: a “loading” state. This is useful for when you’re waiting for an AJAX request to complete, or waiting for a few Promises and calls to get back to you.

A common thing to utilize here would be ui-router’s resolve, which waits for one or more Promises to resolve before loading a state. You could certainly have your “main” state load and resolve those promises asynchronously, which works. But if you’d like something more robust, it might not be enough. What if you want to have a game of old-school Snake be available while your user is waiting for your gigantic AJAX call to complete? And if you’re going to want to show a ‘loading’ state anyway, why not make it reusable?

Recap of the situation:

  1. I have a ‘from’ state and a ‘to’ state (let’s call the latter the ‘target’ state)
  2. I want to have an intermediate ‘loading’ state that my router will go to on a certain condition and/or if Promises have not been resolved
  3. Once these Promises resolve, I want to transition to the next state and preserve all my parameters

Let’s build it:

1. loading.js: Our state definition

// loading.js -- state configuration
angular.module('myApp')
.config(function($stateProvider) {
$stateProvider.state('loading', {
params: {
resolves: null,
toState: null,
toParams: null
},
templateUrl: 'application/loading/loading.html',
controller: 'LoadingCtrl'
});
});

Check out the `params` object, you need to have the properties you’re passing defined here and initialized:

  1. `resolves`: This is your array of Promises that need to be resolved before `loading` can transition to the next state [ Array ]
  2. `toState`: This is a the state you want to transition to after the Promises resolve [ String ]
  3. `toParams`: These are the state params you want to pass on. [ Object ]

Sidenote: Read about the differences between $stateParams and $state.params here.

You can have your `params` object contain whatever properties you want to pass in to `loading`. The main idea is that you’re setting your `loading`’s parameters to have the information it needs to transition to the target state and preserve the original parameters you wanted for the target state. For ui-router, if you want a state to be passed in parameters as `$stateParams`, you need to initialize them when you’re defining the state, just like in the above example.

2. loading.controller.js

This is the controller for your loading state, and where you can add your logic. Another advantage of having a separate ‘loading’ state is that all the logic for loading will be separated and contained. For example, you can add logic for an ‘always’ block that will execute whether your promises are rejected or fulfilled, you can add a client-side timeout, error handlers, etc.

// loading.controller.js
angular.module('myApp')
.controller('LoadingCtrl', ['$q', '$state', '$stateParams',
function($q, $state, $stateParams) {
    var resolves = $stateParams.resolves;
    if (resolves && resolves.length > 0) {
$q.all(resolves).then(function() {
$state.go($stateParams.toState, $stateParams.toParams);
})
} else {
$state.go($stateParams.toState, $stateParams.toParams);
}
}])

In the above snippet, we transition to the `$stateParams.toState` state once all the promises passed to us have been resolved, and if there are none passed to us, we just immediately transition to `$stateParams.toState`.

3. loading.jade

Here’s an example template for the loading state. It’s literally just a spinning circle, but you can make it whatever you like.

// loading.jade
.col-md-12
md-progress-circular.md-accent(mode='indeterminate', md-mode='indeterminate')

Example Usage:

You can use this in any way you want, here’s just one example.

Let’s say somewhere in my app initialization, I have this:

if (AuthService.inited && !AuthService.getCurrentUser()) {
$state.go('login');
} else {
$state.go('main', { messageText: message.text });
}

In the above snippet, I choose to transition to the `login` state if you’re not logged in but the auth service has been initialized, otherwise show them the home (`main`) state, and pass in a message.

In the `$stateChangeStart` handler, I can have this:

$rootScope.$on('$stateChangeStart', 
function(event, toState, toParams, fromState, fromParams) {

// ... other handlers ... //
  // Make sure we're not going to or already in loading
if (!AuthService.inited && !$state.includes('loading')
&& toState.name !== 'loading') {
    targetState = toState.name; 
targetStateParams = toParams;
    event.preventDefault();
    loadingStateParams = {
resolves: [ APIWrapperPreloader, authServicePreloader ],
toState: targetState,
toParams: tartgetStateParams
}
    if (!AuthService.inited || !APIService.inited) {
$state.go('loading', loadingStateParams);
} else {
$state.go(targetState, targetStateParams, {
notify: true
})
}
}
  // ... maybe some more handlers after this ... //
}

The point of this is that I want the app to intercept ANY state change and redirect it to `loading` if the app is not initialized. If it’s a more specific use case, you’d probably not want to put it in `$stateChangeStart`, which gets fired at every transition.

In this specific example, `toState` is an to be an Object, and `$state.go` takes in a String, so if `toState` is not null or undefined, we’ll pass in `toState.name` to `targetState`.

In the above example, you might notice that once the Promises resolve, you’ll be taken to ‘main’ instead of ‘login’, which doesn’t seem correct if you’re logged out. Don’t fret, we have an intercept for that as well, it’s just not in that snippet. :) If you’re curious, here’s the official recommendation on how to prevent state changes based on rules.

So now, when my app initializes, it will first check if my AuthService and APIService have their `inited` properties set to true. If not, it will transition to the ‘loading’ state, and pass in the Promises I need resolved. Once those are resolved, I can then transition to the state name it was passed. 🎉 🎉

🎉

Mission accomplished!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.