I want to show a loading animation during state transitions in a Angular ui-router application.
Check out the following code
$stateProvider
.state("app",
{
abstract: true,
url: "",
template: "<ui-view/>",
controller: "appController",
controllerAs: "vm",
resolve: {
result_data_abstract: function ($q, $timeout))
{
var deferred = $q.defer();
$timeout(function () {
deferred.resolve("from abstract");
}, 500);
return deferred.promise;
}
}
})
.state("app.real",
{
url: "/real/{id}",
templateUrl: "somepath/template.html",
controller: "realController",
controllerAs: "vm",
resolve: {
result_data_real: function ($q, $timeout))
{
var deferred = $q.defer();
$timeout(function () {
deferred.resolve("from real");
}, 500);
return deferred.promise;
}
}
}
.state("app.real2",
{
url: "/real2/{id}",
templateUrl: "somepath2/template.html",
controller: "real2Controller",
controllerAs: "vm",
// Note no resolve
})
So both concrete states inherit from the abstract one - the abstract class has a resolve.
However, only one of the real (concrete) states has a resolve of its own.
My goal is to show a loading animation during transitions using the following code:
$scope.$on("$stateChangeStart", function (event, toState) {
console.log("stateChangeStart", toState);
if (toState.resolve) {
showSpinner();
}
});
$scope.$on("$stateChangeSuccess", function (event, toState) {
console.log("stateChangeSuccess", toState);
if (toState.resolve) {
hideSpinner();
}
});
Since the abstract state the concrete states inherit from has a resolve, I expected 'toState.resolve' to be true every time, getting the resolves from the abstract parent state it inherits from.
That however is not the case - when I load app.real2, 'toState.resolve' is null.
Is there any way to make this work?
Are you loading an entire "page"? If so I do this by calling it in run.
app.run(function($rootScope) {
$rootScope.$on("$stateChangeStart", function () {
console.log("stateChangeStart");
showSpinner();
});
$rootScope.$on("$viewContentLoaded", function () {
console.log("stateChangeSuccess");
hideSpinner();
});
});
I don't use $stateChangeSuccess and toState but it looks like it might work as well.
Related
I'm using UI Bootstrap's $uibModal to create a modal. I'm also using UI Router 0.2.15, so what I want is a state opening in a new modal.
This is what I have in my config function:
$stateProvider
.state("mystate.substate1", {
url: '...',
template: '<div ui-view></div>',
onEnter: showFirstCustomModal
})
.state("mystate.substate2", {
url: '...',
onEnter: showSecondCustomModal
});
// End of calling code
function showFirstCustomModal($uibModal) {
var options = {
backdrop: 'static',
templateUrl: '...',
controller: 'Controller1',
controllerAs: 'controller'
};
$uibModal.open(options);
}
function showSecondCustomModal($uibModal) {
var options = {
backdrop: 'static',
templateUrl: '...',
controller: 'Controller2',
};
$uibModal.open(options);
}
The two modal methods above are very similar. I would like to replace them with a generic method:
$stateProvider
.state("mystate.substate1", {
url: '...',
onEnter: showGenericModal('some_template','SomeController1', 'alias1')
})
.state("mystate.substate2", {
url: '...',
onEnter: showGenericModal('some_other_template', 'SomeController2')
});
// End of calling code
function showGenericModal(templateUrl, controller, controllerAlias, $uibModal) {
var options = {
backdrop: 'static',
templateUrl: templateUrl,
controller: controller
};
if(!!controllerAlias) {
options.controllerAs: controllerAlias;
}
$uibModal.open(options);
}
I put the $uibModal as the last argument to avoid it getting reassigned. But I can't get this to work. The error I get is
Cannot read property 'open' of undefined
Also, I've been reading this and I know that you'll have to use the $injector in order to allow your service to be injected. But I supposed that's already handled by UI-Bootstrap.
Since $stateProvider is defined in config block, $uibModal can't be passed from there as a reference.
It is not possible to mix dependencies and normal arguments in Angular DI. For onEnter it should be a function that accepts the list of dependencies.
The code above translates to:
onEnter: showGenericModal('some_other_template', 'SomeController2')
...
function showGenericModal(templateUrl, controller, controllerAlias) {
return ['$uibModal', function ($uibModal) {
...
$uibModal.open(options);
}];
}
Or a better approach:
onEnter: function (genericModal) {
genericModal.show('some_other_template', 'SomeController2');
}
...
app.service('genericModal', function ($uibModal) {
this.show = function (templateUrl, controller, controllerAlias) {
...
$uibModal.open(options);
}
});
#estus answer correct, I don't know how I didn't saw the state: "For onEnter it should be a function that accepts the list of dependencies.".
However, I will let my answer here to provide another perspective. You can define a service to wrap up and organize correctly your code, in order to call a customized modal on onEnter state event:
angular.module('app').service('AppModals', AppModals);
// or use /** #ngInject */ aswell
AppModals.$inject = ['$uibModal'];
function AppModals($uibModal) {
this.open = function _generateModal(options) {
var defaultOptions = {
backdrop: 'static'
// Any other default option
};
return $uibModal.open(angular.extend({}, defaultOptions, options);
};
}
On the state definition:
$stateProvider
.state('app.state', {
url: '/state-modal',
template: '<ui-view></ui-view>',
controller: 'DummyCtrl',
controllerAs: 'dummy',
onEnter: appState_onEnter
});
// or use /** #ngInject */ aswell
appState_onEnter.$inject = ['$uibModal'];
function appState_onEnter(AppModals) {
AppModals.open({
templateUrl: 'modals/state-modal.html',
controller: 'DummyCtrl',
controllerAs: 'dummy'
});
}
This is what my ui-router looks like:
.state('colleague', {
url: "/colleague",
templateUrl: "views/colleague.html",
resolve: {
typeEmployee: function ($q, $timeout) {
var deferred = $q.defer();
$timeout(function () {
deferred.resolve('manager');
}, 200);
return deferred.promise;
}
,
controller: 'colCtrl'
}
})
The issue is that I can't go to the collegue page:
<a ui-sref="colleague">colleague</a>
This is the controller code:
function colCtrl() {
debugger;
console.log('type of employee is:', typeEmployee);
if (typeEmployee === 'colleague') {
console.log('not allowed to view this');
}
if (typeEmployee === 'manager') {
console.log('allowed to view this');
}
}
app.controller('colCtrl', colCtrl);
When I grab the code from the controller and paste this directly into the router it works. What do I need to fix in the code so I can use 'controller:colCtrl' in my router?
You are using controller inside the resolve. You should move that to top level of state config object.
.state('colleague', {
url: "/colleague",
templateUrl: "views/colleague.html",
controller: 'colCtrl', // Notice its same level as resolve
resolve: {
typeEmployee: function ($q, $timeout) {
var deferred = $q.defer();
$timeout(function () {
deferred.resolve('manager');
}, 200);
return deferred.promise;
}
}
})
Here is working plunkr with your example.
Problem is that, you need to mention the Controller as a variable, not as a string.
i.e.
controller: colCtrl
not
controller: 'colCtrl'
I am using ui-router to load differents subviews on a given state. Some views require resources which take a long time to resolve so I'd like to display my other views as soon as they are ready.
Here is the way I am fetching my resources:
.config(['$stateProvider', '$routeProvider',
function ($stateProvider, $routeProvider) {
$stateProvider
.state('test', {
abstract: true,
url: '/test',
views: {
'main': {
template: '<h1>Hello!!!</h1>' +
'<div ui-view="view1"></div>' +
'<div ui-view="view2"></div>'
}
}
})
.state('test.subs', {
url: '',
views: {
'view1#test': {
template: "Im View1: {{ item }}",
controller: 'View1Ctrl',
resolve: {
test1: function () { return 'yo' }
}
},
'view2#test': {
template: "Im View2: {{ item }}",
controller: 'View2Ctrl',
resolve: {
// This takes a lot of time to resolve
test2: function ($q, $timeout) {
var deferred = $q.defer()
$timeout(function () { deferred.resolve('boom') }, 5000)
return deferred.promise
}
}
}
}
});
}])
I created a JSFiddle to exemplify my issue:
http://jsfiddle.net/o76uq5oe/6/
Is there a way not to wait for all the promises to be resolved?
This happens because resolve promises should be resolved in order for the state transition to be completed, that's why resolve dependencies are there. It doesn't matter if they were defined for the state itself or for its particular view. From the manual:
If all the promises are resolved successfully, the $stateChangeSuccess
event is fired and the values of the resolved promises are injected
into any controllers that reference them. If any of the promises are
rejected the $stateChangeError event is fired.
It can be done if the views belong to separate states. The workaround is possible with sticky states from ui-router-extras, like that:
...
.state('test.subs', {
views: {
'view1#test': {
template: "Im View1: {{ item }}",
controller: 'View1Ctrl',
resolve: {
test1: function () { return 'yo' }
}
},
},
sticky: true,
onEnter: function ($state) {
$state.transition.then(function () {
$state.go('test.subs.view2')
});
}
})
.state('test.subs.view2', {
views: {
'view2#test': {
template: "Im View2: {{ item }}",
controller: 'View2Ctrl',
resolve: {
test2: function ($q, $timeout) {
var deferred = $q.defer()
$timeout(function () { deferred.resolve('boom') }, 1000)
return deferred.promise
}
}
}
}
});
Otherwise the dependency should be resolved in view controller, like it would be done without router and resolve.
I have some routes defined like this :
$stateProvider
.state('app', {
url: '/',
abstract: true,
views: {
'menuContent': {
templateUrl: 'templates/home.html'
}
}
})
.state('app.restricted', {
url: '/restricted',
views: {
'content': {
templateUrl: 'templates/restricted/restricted-dashboard.html',
controller: 'RestrictedController as vmRestricted'
}
},
resolve: {
isGranted: 'isGranted'
}
})
.state('app.restricted.pending', {
url: '/pending',
views: {
'tabsView': {
templateUrl: 'templates/restricted/restricted-manage-pending.html',
controller: 'RestrictedPendingController as vm'
}
},
resolve: {
isGranted: 'isGranted'
}
})
.state('app.restricted.devices', {
url: '/devices',
views: {
'tabsView': {
templateUrl: 'templates/trusted/restricted-manage-devices.html',
controller: 'RestrictedDevicesController as vm'
}
},
resolve: {
isGranted: 'isGranted'
}
})
.state('app.grant', {
url: '/grant-access',
views: {
'content': {
templateUrl: 'templates/grant-access.html',
controller: 'GrantAccessController as vm'
}
}
})
;
In these routes I have a restricted area and a grant access page to grant access to the restricted area.
When the isGranted resolve provider is rejected I redirect to the app.grant route.
This is the code doing this :
$rootScope.$on(AngularEvents.STATE_CHANGE_ERROR, _onStateChangeError);
function _onStateChangeError(event, toState, toParams, fromState, fromParams, error){
switch (error) {
case 'accessRejected':
$state.go('app.grant');
break;
}
}
Here is the code of my isGranted provider :
(function() {
'use strict';
angular.module('app')
.provider('isGranted', isGrantedProvider);
isGrantedProvider.$inject = [];
function isGrantedProvider() {
this.$get = isGranted;
isGranted.$inject = ['$q', '$log', 'grantService'];
function isGranted($q, $log, grantService){
$log.log('isGrantedProvider');
if (grantService.isGranted()) {
return $q.when(true);
} else {
return $q.reject('accessRejected');
}
}
}
})();
(grantService.isGranted() just returns a boolean value)
The first time I go to the app.restricted route with $state.go('app.restricted') the provider is executed.
The route is rejected because the access is not granted and we are redirected to the app.grant route.
In this page, the user can log in and have access to the restricted area. Once the user is logged in we redirect him to the app.restricted.pending route but the resolve is not called and the route is rejected and we are redirected to the app.grant route again, whereas the access was granted.
Why is the resolve not called?
Is there a way to force it?
EDIT
I have new information after some testing.
I saw that the resolve is not called the second time only when it is a service:
This resolve is always executed when we enter the state:
state('app.restricted', {
url: '/restricted',
views: {
'content': {
templateUrl: 'templates/restricted/restricted-dashboard.html',
controller: 'RestrictedController as vmRestricted'
}
},
resolve: {
isGranted: ['$log', function($log) {
$log.log('RESOLVE');
}]
}
})
But this resolve is only executed once even when I enter again to the state:
state('app.restricted', {
url: '/restricted',
views: {
'content': {
templateUrl: 'templates/restricted/restricted-dashboard.html',
controller: 'RestrictedController as vmRestricted'
}
},
resolve: {
isGranted: 'isGranted'
}
})
angular.module('app')
.provider('isGranted', isGrantedP);
isGrantedP.$inject = [];
function isGrantedP() {
this.$get = isGranted;
isGranted.$inject = ['$q', '$log'];
function isGranted($q, $log){
$log.log('RESOLVE');
}
}
Why isn't this service called each time? Is it because a service is a singleton? How should I proceed?
After a lot of investigations and testing I found the solution!
First, let's see why it is not working
As mentioned in the docs (http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$stateProvider), if the resolve is a string, then it corresponds to a service
factory - {string|function}: If string then it is alias for service.
Otherwise if function, it is injected and return value it treated as
dependency. If result is a promise, it is resolved before its value is
injected into controller.
And as mentioned in the angularjs docs (https://docs.angularjs.org/guide/providers), all services are singletons, meaning that it will be instantiated only once
Note: All services in Angular are singletons. That means that the
injector uses each recipe at most once to create the object. The
injector then caches the reference for all future needs.
Why is it important?
Because resolves do not call a function inside our service. They just use the return value of the instantiated service. BUT because our service will be instantiated only once, the return value will always be the same! (because our service initialization is only called once)
What can we do?
From my tests I could see that a resolve defined like this:
resolve: {
myResolve: ['$log', function($log) {
$log.log('My Resolve!');
}]
}
is always executed, so we can write them this way to make it work correctly.
But how can I do if I want to use my service?
The best working solution I found to be able to use my service and have a syntax that looks similar to this one: myResolve: 'myResolveService' is to declare my resolve like this:
resolve: {
myResolve: ['myResolveService', function(MyResolveService) {
myResolveService.log();
}]
}
And my service like this:
angular.module('app')
.factory('myResolve', myResolve);
myResolve.$inject = ['$log'];
function myResolve($log) {
function service(){
this.log = log;
function log() {
$log.log('My resolve!');
}
}
return new service();
}
This code can also be adapted for resolves that return a promise:
Resolve:
resolve: {
myResolve: ['myResolveService', function(MyResolveService) {
return myResolveService.check();
}]
}
Service:
angular.module('app')
.factory('myResolve', myResolve);
myResolve.$inject = ['$q', 'myService'];
function myResolve($q, myService) {
function service(){
this.check = check;
function check() {
var defer = $q.defer();
if (myService.check()) {
defer.resolve(true);
} else {
defer.reject('rejected');
}
return defer.promise;
}
}
return new service();
}
Angular UI Router Question
When $state.go("main.loadbalancer.readonly"); is activated in the resolve block, the main.loadbalancer.vips state controller VipListCtrl (the controller only) still loads after the resolve.
Since the state main.loadbalancer.readonly is activated, how can I keep make the controller VipListCtrl cancel and not load?
I tried using a promise and never having the promise resolve, but then the UI Router seems to stay sit at that resolve forever.
angular.module("main.loadbalancer", ["ui.bootstrap", "ui.router"]).config(function($stateProvider) {
return $stateProvider.state("main.loadbalancer", {
url: "device/:id",
views: {
"content#": {
templateUrl: "loadbalancer/loadbalancer.html",
controller: "LoadBalancerCtrl"
}
}
}).state("main.loadbalancer.vips", {
resolve: {
isDeviceReadOnly: function($state) {
if (!$state.current.data['deviceId']) {
$state.go("main.loadbalancer.readonly"); //THIS IS RAN...NEED CONTROLLER
//VipListCtrl TO NOT RUN AFTERWARDS
}
}
},
url: "/vips",
templateUrl: "loadbalancer/vip-table.html",
controller: "VipListCtrl"
}).state("main.loadbalancer.nodes", {
url: "/nodes",
templateUrl: "loadbalancer/node-table.html",
controller: "NodeListCtrl"
}).state("main.loadbalancer.admin", {
url: "/admin",
templateUrl: "loadbalancer/admin.html",
controller: "AdminCtrl"
}).state("main.loadbalancer.readonly", {
url: "/readonly",
templateUrl: "loadbalancer/readonly.html",
controller: "ReadonlyCtrl"
});
});
resolve: {
isDeviceReadOnly: function($state, $q, $timeout) {
if (!$state.current.data['deviceId']) {
$timeout(function() { $state.go("main.loadbalancer.readonly"); });
return $q.reject("rejection message"); // <-- Gotta reject your resolve
}
}
},
plunk which demonstrates $q.reject cancelling the transition: http://plnkr.co/edit/njJtyVbKD4rDY3OckAF6?p=preview