angular-loading-bar and images/views/states loading - angularjs

I am using ui-router for my angular app. I have some views that contain images. I would like the angular-loading-bar to track also when these images are finished loading. Or more generally, until the view has finished rendering.
When I stumbled upon that post on how to do SEO with angularJS, it indicated that we must create static html snapshots of every "page" and serve them to search engine bots so that they can be crawled. To do that, we must ourselves crawl over all pages in our application with a headless browser and store the html into a file that will be served. But, with angular having to load views over ajax and all, we must wait for our page to be loaded before storing what the headless browser html. Otherwise we get empty html with empty views.
I have written a small script that can check for the ready status of our views. When the $rootScope.status property equals 'ready', I know I can store my headless browser's html as it has finished loading.
var app = angular.module("app", ["ui.router", 'ngAnimate','angular-loading-bar','angular-images-loaded','angular-google-analytics']);
app.run(['$rootScope', function($rootScope){
$rootScope.loadingCount = 0;
$rootScope.changeSuccess = false;
$rootScope.ready = function(){
$rootScope.loadingCount--;
if (($rootScope.loadingCount == 0) && ($rootScope.changeSuccess == true)){
$rootScope.status = 'ready';
}
};
$rootScope.loading = function(){
$rootScope.loadingCount++;
$rootScope.status = 'loading';
};
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
$rootScope.loadingCount = 0;
$rootScope.changeSuccess = false;
});
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
$rootScope.changeSuccess = true;
});
$rootScope.$on("$viewContentLoading", function(){
$rootScope.loadingCount++;
});
$rootScope.$on("$viewContentLoading", function(){
$rootScope.loadingCount--;
});
}]);
Then, in each of our controllers, we must call
$rootScope.loading();
and when the controller is ready
$rootScope.ready()
With this, we'll be able to check if all our controllers have rendered their views. It's not super elegant for now, but it does the job.
This script can be well integrated with angular-loading-bar as it tracks the readiness of the overall application. The progress bar could be an indicator of that progress. The drawback of this is that it has conflicts with the natural behaviour of angular-loading-bar tracking XHR requests.
For example, in my controllers I use this :
app.controller("WorksController", [
"$scope", "cfpLoadingBar",
function ($scope, cfpLoadingBar) {
cfpLoadingBar.start();
$scope.imgLoadedEvents = {
always: function () {
cfpLoadingBar.complete();
}
};
}
]);
This code should be migrated right in the $rootScope script that tracks the readiness of the views.
$rootScope.$watch('status', function(newValue, oldValue){
if (newValue == 'loading'){ cfpLoadingBar.start() }
else if (newValue == 'ready') { cfpLoadingBar.complete() }
})
Though, angular-progress-bar still works in the background. I let active the XHR interceptor. Though, if an XHR requests is completed before the image has loaded, the progress bar disappears even if the views have not finished. As well, if an image has loaded before the XHR request is completed, the progress bar disappears.
How can I integrate the XHR interception capabilities of the angular-loading-bar with this view readiness interception capabilities?

This isn't possible to do without modifying angular-loading-bar's code because the $http interceptor does not have any kind of API you can hook into.
You should fork the project on Github and modify the cfb.loadingBarInterceptor. Basically what you'll need to do is remove the code that hides and shows the loading bar. Leave the code in that broadcasts events on $rootScope. This is what you'll hook into.
return {
'request': function(config) {
// Check to make sure this request hasn't already been cached and that
// the requester didn't explicitly ask us to ignore this request:
if (!config.ignoreLoadingBar && !isCached(config)) {
$rootScope.$broadcast('cfpLoadingBar:loading', {url: config.url});
}
return config;
},
'response': function(response) {
if (!response || !response.config) {
$log.error('Broken interceptor detected: Config object not supplied in response:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
return response;
}
if (!response.config.ignoreLoadingBar && !isCached(response.config)) {
$rootScope.$broadcast('cfpLoadingBar:loaded', {url: response.config.url, result: response});
}
return response;
},
'responseError': function(rejection) {
if (!rejection || !rejection.config) {
$log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
return $q.reject(rejection);
}
if (!rejection.config.ignoreLoadingBar && !isCached(rejection.config)) {
$rootScope.$broadcast('cfpLoadingBar:loaded', {url: rejection.config.url, result: rejection});
}
return $q.reject(rejection);
}
};
Now, in your run block, simply add these lines at the bottom:
app.run(['$rootScope', function($rootScope){
// Previous code ...
$rootScope.$on("cfbLoadingBar:loaded", $rootScope.ready);
$rootScope.$on("cfbLoadingBar:loading", $rootScope.loading);
});
In case it's not clear what this does, this uses the events from your new $http interceptor which results in ready and loading being called in the same way that your controllers do.

The blogpost you are referring to is legacy.
Google will index your single page application without any html snapshots.
If you want to create Snapshots (e.g. for use of canonical tag which is not supported yet!) you should NOT do this manually but integrate a task in your build process. With Grunt/Gulp this is very simple and can be done in 10 minutes. There is no need to specify a "ready" Event - phantomjs will recognize when your script is done.
The proper way would be to move the function that loads your images in your states resolve function. Then you dont have to worry about sending ready events to phantom. ;-) To use this with a loading bar:
$rootScope.$on('$stateChangeStart', function () {
ngProgress.start();
// or your: cfpLoadingBar.start();
});
$rootScope.$on('$stateChangeSuccess', function () {
ngProgress.complete();
// or your: cfpLoadingBar.complete();
});

As #anid-monsur specified, impossible to do without modifying the loading-bar code. Though, I wanted to keep the initial functionality of angular-loading-bar so I followed a different path that suggested by him.
Instead of removing the hide-show code, I added custom event handlers like these in the angular-loading-bar interceptor :
$rootScope.$on("cfpLoadingBar:customRequest", function(event, args){
if (!args) {
args = {};
}
if (!args.url){
args.url = 'custom';
}
loading(args);
});
$rootScope.$on("cfpLoadingBar:customResponse", function(event, args){
if (!args) {
args = {};
}
if (!args.config){
args.config = {};
}
if (!args.config.url){
args.config.url = 'custom';
}
response(args);
});
Check the code here
So, when my ui-router changes state I can write :
app.run(['$rootScope', 'cfpLoadingBar', function($rootScope, cfpLoadingBar){
$rootScope.$broadcast("cfpLoadingBar:customRequest");
$rootScope.ready = function(){
$rootScope.$broadcast("cfpLoadingBar:customResponse");
};
$rootScope.loading = function(){
$rootScope.$broadcast("cfpLoadingBar:customRequest");
};
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
$rootScope.$broadcast("cfpLoadingBar:customRequest");
});
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
$rootScope.$broadcast("cfpLoadingBar:customResponse");
});
}]);
Notice the added $rootScope.ready() and $rootScope.loading()
They are used when a controller has a deferred loading process
For example, with angular-imagesloaded :
app.controller("WorksController", [
"$scope", '$rootScope',
function ($scope, $rootScope) {
$rootScope.loading();
$scope.imgLoadedEvents = {
done: function () {
$rootScope.ready();
}
};
}
]);
This way, I can still use the native interceptor provided by angular-loading-bar and register my own "custom" requests.

This issue is related to the Plugin as Anid Monsur said. You can solve the issue just upgrading angular-loading-bar to latest version or at least 7.0.1+.

Related

Is there a way to create a dismissible button that goes to the page where the 404 page was blocking?

In my project using ui-router, I have a 404 page that occurs whenever the a certain component does not exist ( in the otherwise). So instead of displaying, broken JSON, a 404 page is triggered. Is there a way to create a button on my 404 page that when clicked takes the user to the page of the broken JSON?
You can catch params and states with $rootScope , $stateChangeStart
app.run(function($rootScope, SpinnerService) {
$rootScope.$on('$stateChangeStart', function(evt, toState, toParams, fromState, fromParams) {
console.log("$stateChangeStart " + fromState.name + JSON.stringify(fromParams) + " -> " + toState.name + JSON.stringify(toParams));
if(toState.name === 'errorPage'){
toParams.latestState = fromState;
}
});
})
Create a state which includes your error page. And add state action which is working with latest params and latest state.
$stateProvider.state('errorPage', {
templateProvider: function ($timeout, $stateParams) {
return $timeout(function () {
return '<button ui-sref="'+$stateParams.latestState+'"></button>'
}, 100);
}
})
You can do that with template, controller, templateUrl, controllerProvider or storing the latest variable inside $rootScope.
There is a lot of way to do it. I think read this article carefully https://github.com/angular-ui/ui-router/wiki

Angular reload service when state change or jQuery run for all state (ui.router lib)

I have two issues.
The First one is:
I have try jQuery code and angular.service code for the same thing. To clear input fields from blank spaces dynamically.
$('input[type="text"]').change(function () {
if (!this.value.replace(/\s/g, '').length) {
this.value = "";
}
});
or
angular.module('app').service('nospace', function () {
$('input[type="text"]').change(function () {
if (!this.value.replace(/\s/g, '').length) {
this.value = "";
}
});
});
When i try to use jQuery globally for all angular pages is not working.
When i try to use .service is working, but when i change state is stop working, till i refresh page. I try to add to
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {}); and call the service from there when state is changed to fix this problem, but still don`t work properly.
I added to controller like this
angular.module('app').controller('channelAoiEditor', function (..,.., nospace) { ... some code ... }
Second issue is with parsing the url to state properly.
I have this state:
.state('edit', {
url: '/EditCustomer/:id',
templateUrl: 'Client/app/customers/editCustomer/editCustomer.html',
controller: 'editCustomer'
})
When I'm in "Edit Customer" page and try to change the user directly from the url. Page reload, but retains last used user instead of reloaded the new user, but the URL is the same as new one.
I try with changing the state with:
url: '/EditCustomer/{id}'
or
url: '/EditCustomer/{id:[a-zA-Z0-9/.]*}'
On $rootScope.$on('$locationChangeSuccess', function (event, url, oldUrl, state, oldState) {..} I get the changes but how to parse it properly to change the content.
Thank You!
Perhaps you need to $scope.$apply() and (or) $scope.$digest() for reloading the page... it happens that jQuery and Javascript code is turn based so in order to "repaint" the page you need to:
$apply and $digest
That step that checks to see if any binding values have changed actually has a >method, $scope.$digest(). That’s actually where the magic happens, but we >almost never call it directly, instead we use $scope.$apply() which will call >$scope.$digest() for you.
so I would encapsulate the code as this:
$scope.$apply(function(){
$('input[type="text"]').change(function () {
if (!this.value.replace(/\s/g, '').length) {
this.value = "";
}
});
That way you ensure that you are applying and updating the values.
There is some documentation here http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
See ya :)
I managed to fix it I had forgotten an important difference on jQuery, which is the following
$(document).on('change', 'input[type="text"]', AddVerification)
and
$('input[type="text"]').change(AddVerification);
This is how I fix it
function AddVerification() {
if (!this.value.replace(/\s/g, '').length) {
this.value = "";
}
}
angular.modul('app',[....])
.service('nospace', function () {
//jQuery trim input fields
this.clearWhiteSpaces = function myfunction() {
$(document).off("change", 'input[type="text"]', AddVerification)
.on('change', 'input[type="text"]', AddVerification);
}
}).run(
['$rootScope', '$state', '$stateParams','nospace',
function ($rootScope, $state, $stateParams,nospace) {
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
nospace.clearWhiteSpaces();
});
})
and for the controller:
angular.module('app').controller('channelAoiEditor', function ($scope, datastore, $cookieStore, modalWindow, nospace) {
Remained the problem with passing properly the URL to $stateProvider.state

Load data SQLite in ionic bootstrap

I'm looking for solution for load data from sqlite in cordova bootstrap, for more specifically in $ionicPlatform.ready but until here I don't have a solution.
See a piece of my code (I'm using ionic with $cordovaSQLite):
app.js file
...
app.run(function($DB, $log, params...){
$ionicPlatform.ready(function() {
...
$DB.init().then(function(res){
// here I need load a user
$rootScope.user = res;
$log.debug('1 step = user loaded');
});
$rootScope.$on('$stateChangeStart', function(event, toState, toParams){
$log.debug('2 step = use user loaded!');
var _requireAuth = toState.data.requireAuth;
if(_requireAuth && !$rootScope.user){
event.preventDefault();
$state.go('login');
}
});
});
...
DB.js file
app.factory('$DB', function($q, $cordovaSQLite, params...){
...
_self.init = function(){
var deferred = $q.defer();
$cordovaSQLite.execute($ndDB.open(), "SELECT * FROM user LIMIT 1", [])
.then(function(res){
if(res.rows.length > 0){
var _user = {
id: res.rows.item(0).id,
name: res.rows.item(0).name,
...
};
deferred.resolve(_user);
}
}, function(err){
deferred.reject(err);
});
return deferred.promise;
...
};
...
});
config.js file
app.config(function($urlRouterProvider, params...){
...
$urlRouterProvider.otherwise('/home');
...
});
The problem is, I need $rootScope.user before all, because I need authenticate and redirect, but always I go direct to home, because $rootScope.user is already late and $stateChangeStart no apply redir. If put it in $DB.init() callback, the behaviour is the same because default route is /home and stateChangeStart load after Db.init().
Now I'm using a bad method, in home I check $rootScope.user and redirect to /login, but it's so bad because first we see the transition between home and login.
If I use the localStorage, it works fine, but I really need to use the sqlite.
What the best approach for this?
Thanks

Cordova/Phonegap + Angular: promise not resolving until next digest cycle

I'm using Angular + JQMobile inside a Cordova mobile app, and I'm having some difficulty with promises. I have a cordovaReady service that returns a promise and I'm looking to hide a splashscreen (no lectures please) when the promise is resolved. The trick comes when I try to use an if-then condition to resolve the promises at different times. My code:
app.factory('cordovaReady', function($q) {
var deferred = $q.defer();
return {
ready: function() {
document.addEventListener(
"deviceready",
function () {
deferred.resolve();
},
false
);
return deferred.promise;
}
}
});
app.run(['$rootScope', '$location', 'TagTemplates', 'cordovaReady', function($scope, $location, TagTemplates, cordovaReady) {
// If there is a user logged in, --fire camera-- and go to tag template
if ($scope.currentUser) {
// fire camera here, make sure cordova is ready first.
// alert('found user');
var promise = cordovaReady.ready();
promise.then(function() {
// alert('about to capture');
console.log('firing capture');
$scope.captureImage();
navigator.splashscreen.hide(); // this one fires.
});
console.log($scope.currentUser);
$scope.templates = $scope.currentUser.attributes.templates[0];
$scope.$apply();
$location.url("/#main");
} else {
var promise = cordovaReady.ready();
promise.then(function() {
alert('no user logged in, hide splashscreen');
navigator.splashscreen.hide(); // this one does not.
$scope.$apply(); // even with this.
});
}
}
In the case of an existing user, the splashscreen is hidden as desired, I thought because of scope updates that happen within this if statement. However, in the event of no existing user, the splashscreen does not fire until the next digest cycle (clicking on a button or link in my app). I verified this with the alert.
I've tried all manner of scope apply, include variants of setTimeout with 0 delay, etc.
Any thoughts?
Thanks.

stop angular-ui-router navigation until promise is resolved

I want to prevent some flickering that happens when rails devise timeout occurs, but angular doesn't know until the next authorization error from a resource.
What happens is that the template is rendered, some ajax calls for resources happen and then we are redirected to rails devise to login. I would rather do a ping to rails on every state change and if rails session has expired then I will immediately redirect BEFORE the template is rendered.
ui-router has resolve that can be put on every route but that doesn't seem DRY at all.
What I have is this. But the promise is not resolved until the state is already transitioned.
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
//check that user is logged in
$http.get('/api/ping').success(function(data){
if (data.signed_in) {
$scope.signedIn = true;
} else {
window.location.href = '/rails/devise/login_path'
}
})
});
How can I interrupt the state transition, before the new template is rendered, based on the result of a promise?
I know this is extremely late to the game, but I wanted to throw my opinion out there and discuss what I believe is an excellent way to "pause" a state change. Per the documentation of angular-ui-router, any member of the "resolve" object of the state that is a promise must be resolved before the state is finished loading. So my functional (albeit not yet cleaned and perfected) solution, is to add a promise to the resolve object of the "toState" on "$stateChangeStart":
for example:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
toState.resolve.promise = [
'$q',
function($q) {
var defer = $q.defer();
$http.makeSomeAPICallOrWhatever().then(function (resp) {
if(resp = thisOrThat) {
doSomeThingsHere();
defer.resolve();
} else {
doOtherThingsHere();
defer.resolve();
}
});
return defer.promise;
}
]
});
This will ensure that the state-change holds for the promise to be resolved which is done when the API call finishes and all the decisions based on the return from the API are made. I've used this to check login statuses on the server-side before allowing a new page to be navigated to. When the API call resolves I either use "event.preventDefault()" to stop the original navigation and then route to the login page (surrounding the whole block of code with an if state.name != "login") or allow the user to continue by simply resolving the deferred promise instead of trying to using bypass booleans and preventDefault().
Although I'm sure the original poster has long since figured out their issue, I really hope this helps someone else out there.
EDIT
I figured I didn't want to mislead people. Here's what the code should look like if you are not sure if your states have resolve objects:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if (!toState.resolve) { toState.resolve = {} };
toState.resolve.pauseStateChange = [
'$q',
function($q) {
var defer = $q.defer();
$http.makeSomeAPICallOrWhatever().then(function (resp) {
if(resp = thisOrThat) {
doSomeThingsHere();
defer.resolve();
} else {
doOtherThingsHere();
defer.resolve();
}
});
return defer.promise;
}
]
});
EDIT 2
in order to get this working for states that don't have a resolve definition you need to add this in the app.config:
var $delegate = $stateProvider.state;
$stateProvider.state = function(name, definition) {
if (!definition.resolve) {
definition.resolve = {};
}
return $delegate.apply(this, arguments);
};
doing if (!toState.resolve) { toState.resolve = {} }; in stateChangeStart doesn't seem to work, i think ui-router doesn't accept a resolve dict after it has been initialised.
I believe you are looking for event.preventDefault()
Note: Use event.preventDefault() to prevent the transition from happening.
$scope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){
event.preventDefault();
// transitionTo() promise will be rejected with
// a 'transition prevented' error
})
Although I would probably use resolve in state config as #charlietfl suggested
EDIT:
so I had a chance to use preventDefault() in state change event, and here is what I did:
.run(function($rootScope,$state,$timeout) {
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){
// check if user is set
if(!$rootScope.u_id && toState.name !== 'signin'){
event.preventDefault();
// if not delayed you will get race conditions as $apply is in progress
$timeout(function(){
event.currentScope.$apply(function() {
$state.go("signin")
});
},300)
} else {
// do smth else
}
}
)
}
EDIT
Newer documentation includes an example of how one should user sync() to continue after preventDefault was invoked, but exaple provided there uses $locationChangeSuccess event which for me and commenters does not work, instead use $stateChangeStart as in the example below, taken from docs with an updated event:
angular.module('app', ['ui.router'])
.run(function($rootScope, $urlRouter) {
$rootScope.$on('$stateChangeStart', function(evt) {
// Halt state change from even starting
evt.preventDefault();
// Perform custom logic
var meetsRequirement = ...
// Continue with the update and state transition if logic allows
if (meetsRequirement) $urlRouter.sync();
});
});
Here is my solution to this issue. It works well, and is in the spirit of some of the other answers here. It is just cleaned up a little. I'm setting a custom variable called 'stateChangeBypass' on the root scope to prevent infinite looping. I'm also checking to see if the state is 'login' and if so, that is always allowed.
function ($rootScope, $state, Auth) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if($rootScope.stateChangeBypass || toState.name === 'login') {
$rootScope.stateChangeBypass = false;
return;
}
event.preventDefault();
Auth.getCurrentUser().then(function(user) {
if (user) {
$rootScope.stateChangeBypass = true;
$state.go(toState, toParams);
} else {
$state.go('login');
}
});
});
}
as $urlRouter.sync() doesn't work with stateChangeStart, here's an alternative:
var bypass;
$rootScope.$on('$stateChangeStart', function(event,toState,toParams) {
if (bypass) return;
event.preventDefault(); // Halt state change from even starting
var meetsRequirement = ... // Perform custom logic
if (meetsRequirement) { // Continue with the update and state transition if logic allows
bypass = true; // bypass next call
$state.go(toState, toParams); // Continue with the initial state change
}
});
To add to the existing answers here, I had the exact same issue; we were using an event handler on the root scope to listen for $stateChangeStart for my permission handling. Unfortunately this had a nasty side effect of occasionally causing infinite digests (no idea why, the code was not written by me).
The solution I came up with, which is rather lacking, is to always prevent the transition with event.preventDefault(), then determine whether or not the user is logged in via an asynchronous call. After verifying this, then use $state.go to transition to a new state. The important bit, though, is that you set the notify property on the options in $state.go to false. This will prevent the state transitions from triggering another $stateChangeStart.
event.preventDefault();
return authSvc.hasPermissionAsync(toState.data.permission)
.then(function () {
// notify: false prevents the event from being rebroadcast, this will prevent us
// from having an infinite loop
$state.go(toState, toParams, { notify: false });
})
.catch(function () {
$state.go('login', {}, { notify: false });
});
This is not very desirable though, but it's necessary for me due to the way that the permissions in this system are loaded; had I used a synchronous hasPermission, the permissions might not have been loaded at the time of the request to the page. :( Maybe we could ask ui-router for a continueTransition method on the event?
authSvc.hasPermissionAsync(toState.data.permission).then(continueTransition).catch(function() {
cancelTransition();
return $state.go('login', {}, { notify: false });
});
The on method returns a deregistration function for this listener.
So here is what you can do:
var unbindStateChangeEvent = $scope.$on('$stateChangeStart',
function(event, toState, toParams) {
event.preventDefault();
waitForSomething(function (everythingIsFine) {
if(everythingIsFine) {
unbindStateChangeEvent();
$state.go(toState, toParams);
}
});
});
I really like the suggested solution by TheRyBerg, since you can do all in one place and without too much weird tricks. I have found that there is a way to improve it even further, so that you don't need the stateChangeBypass in the rootscope. The main idea is that you want to have something initialized in your code before your application can "run". Then if you just remember if it's initialized or not you can do it this way:
rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) {
if (dataService.isInitialized()) {
proceedAsUsual(); // Do the required checks and redirects here based on the data that you can expect ready from the dataService
}
else {
event.preventDefault();
dataService.intialize().success(function () {
$state.go(toState, toParams);
});
}
});
Then you can just remember that your data is already initialized in the service the way you like, e.g.:
function dataService() {
var initialized = false;
return {
initialize: initialize,
isInitialized: isInitialized
}
function intialize() {
return $http.get(...)
.success(function(response) {
initialized=true;
});
}
function isInitialized() {
return initialized;
}
};
You can grab the transition parameters from $stateChangeStart and stash them in a service, then reinitiate the transition after you've dealt with the login. You could also look at https://github.com/witoldsz/angular-http-auth if your security comes from the server as http 401 errors.
I ran in to the same issue Solved it by using this.
angular.module('app', ['ui.router']).run(function($rootScope, $state) {
yourpromise.then(function(resolvedVal){
$rootScope.$on('$stateChangeStart', function(event){
if(!resolvedVal.allow){
event.preventDefault();
$state.go('unauthState');
}
})
}).catch(function(){
$rootScope.$on('$stateChangeStart', function(event){
event.preventDefault();
$state.go('unauthState');
//DO Something ELSE
})
});
var lastTransition = null;
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams, options) {
// state change listener will keep getting fired while waiting for promise so if detect another call to same transition then just return immediately
if(lastTransition === toState.name) {
return;
}
lastTransition = toState.name;
// Don't do transition until after promise resolved
event.preventDefault();
return executeFunctionThatReturnsPromise(fromParams, toParams).then(function(result) {
$state.go(toState,toParams,options);
});
});
I had some issues using a boolean guard for avoiding infinite loop during stateChangeStart so took this approach of just checking if the same transition was attempted again and returning immediately if so since for that case the promise has still not resolved.

Resources