UI-router: understanding resolve promises flow of the root-sub states - javascript

I'm developing the angular app, using ui - router.
I created root state which is an abstract one aimed to resolve async. dependencies.
So every sub states from my perspective should be able to use those dependencies in own resolve state properties.
So if abstract root state resolving async dependencies and sub state also resolving asyc dependencies, the latter one should wait for the root dependencies to resolved, before starting its own resolve method. Right?
Here is the code example, that shows what I mean:
async, promise based methods that are used inside corresponding resolve
public iAmInTheRootState(): IPromise<any> {
let deferred = this._$q.defer();
this._$timeout(() => {
deferred.resolve();
}, 3000);
return <IPromise<any>> deferred.promise;
}
public iAmInTheSubState(): IPromise<any> {
let deferred = this._$q.defer();
this._$timeout(() => {
deferred.resolve();
}, 100);
return <IPromise<any>> deferred.promise;
}
Root abstract state:
$stateProvider
.state('app', {
url: '/',
abstract: true,
templateUrl: 'layout/app-view.html',
resolve: {
auth: function (Auth: IAuthService) {
'ngInject';
return Auth.iAmInTheRootState().then(() => {
console.log('I am the root state, so I should be first');
});
}
}
});
Sub state which is the daughter state:
$stateProvider.state('app.my-calls', {
url: '',
controller: 'MyCallsController',
controllerAs: '$ctrl',
templateUrl: 'states/my-calls/my-calls.html',
resolve: {
secondAuth: (Auth: IAuthService) => {
'ngInject';
return Auth.iAmInTheSubState().then( () => {
console.log('although I am faster I should be second because i am in the sub state');
});
}
}
})
But the output differs from my expectations:

In your example, the 'app.my-calls' is indeed created after the 'app' state (you can verify this by logging the onEnter callback.)
From Ui-router wiki
Resolve
You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
The resolve callback is not used to delay the state creation, but used to delay the controler creation.
To understand the full flow, you can log the $stateChangeStart & $stateChangeSuccess events.

Although Tim's answer can be considered as a direct response to my question, one may want to understand how to make sub states to wait for their parent's resolve method.
Check out the github issue regarding to this topic.
So in short: sub states should have parent state as a dependency:
Parent State:
$stateProvider
.state('app', {
url: '/',
abstract: true,
templateUrl: 'layout/app-view.html',
resolve: {
ParentAuth: function (Auth: IAuthService) {
'ngInject';
return Auth.iAmInTheRootState().then(() => {
console.log('I am the root state, so I should be first');
});
}
}
});
Child state:
$stateProvider.state('app.my-calls', {
url: '',
controller: 'MyCallsController',
controllerAs: '$ctrl',
templateUrl: 'states/my-calls/my-calls.html',
resolve: {
SubAuth: (ParentAuth, Auth: IAuthService) => {
'ngInject';
return Auth.iAmInTheSubState().then( () => {
console.log('although I am faster I should be second because i am in the sub state');
});
}
}
})

Related

watch parent controller value from ui router state resolve

In my AngularJS app using ui-router I have three states. The parent state in the controller resolves a promise and on a successful request, executes the code.
In the child state portfolio.modal.patent, using $stateProvider and the resolve method, I make another request. The issue is that I need to make the resolve method wait until the promise is returned in the parent controller portfolio.
States
$stateProvider
.state('portfolio', {
url: '/portfolio',
templateUrl: 'app/templates/portfolio/portfolio.tpl.htm',
controller: 'portfolioCtrl',
controllerAs: '$ctrl',
})
.state('portfolio.modal', {
abstract: true,
views: {
"modal": {
templateUrl: "app/templates/patent/modal.html"
}
}
})
.state('portfolio.modal.patent', {
url: '/:patentId',
resolve: { //NEED TO MAKE THIS RESOLVE AFTER PORTFOLIO CONTROLLER HAS RETURNED ITS PROMISE
patent: ['$stateParams', 'patentsRestService', function($stateParams, patentsRestService) {
return patentsRestService.fetchPatentItem($stateParams.patentId);
}],
}
}
Portfolio controller
function portfolioCtrl() {
$scope.promise
.then(function(response) {
//WHEN PROMISIS RETURNED, THEN RESOLVE DATA IN PORTFOLIO.MODAL.PATENT
})
}
Question
How do I resolve the child state portfolio.modal.patent data after the promise has been returned from the parent state controller portfolioCtrl
Here is a fast possible workaround how you can try to solve your problem using service as a bridge between requests
Example:
// service
const bridge = $q.defer();
const resolveBridgePromise = data => bridge.resolve(data);
const getBridgePromise = () => bridge.promise;
const fetchDataInParentCtrl = () => {
$timeout(() => {
console.log('data fetched in parent');
resolveBridgePromise({ key: 'fetched data' });
}, 2000);
};
// controller
fetchDataInParentCtrl();
// resolver
getBridgePromise()
.then(data => {
console.log('Do http request here for your child data: ', data);
});

Resolve promise in abstract parent before $stateChangeStart fires

I'm attempting to resolve some server-side data in an abstract parent state before the Home child state is loaded. I want to make sure I have the user's full name for examination in the state's data.rule function. However, using console.log statements, I can see the userFullName prints as an empty string to the console before the "done" statement in the factory.privilegeData method.
EDIT
The data in the parent resolve is security data from the server that needs to be available globally before any controllers are initialized. Only one child state is listed here (for the sake of readability), but all states, outside of login are children of the abstract parent. Once the parent's resolves are complete the data is stored in the $rootScope and used to determine access rights via the data.rule function in each state.
config.js
UPDATE: per answer below I've updated the parent/child states as such, howeve I'm still running into the same issue:
.state('app', {
url:'',
abstract: true,
template: '<div ui-view class="slide-animation"></div>',
resolve:{
privileges: ['$q', 'privilegesService', function($q, privilegesService){
console.log('from parent resolve');
return privilegesService.getPrivileges()
.then(privilegesService.privilegesData)
.catch(privilegesService.getPrivilegesError);
}]
}
})
.state('app.home', {
url: '/home',
templateUrl: 'app/components/home/home.html',
controller: 'HomeController',
controllerAs: 'vm',
parent: 'app',
resolvePolicy: {when:'LAZY', async: 'WAIT'},
authenticate: true,
data:{
rule: function($rootScope){
console.log("from home state data rule");
return true;
}
}
})
privilegesService.js
factory.getPrivileges = function () {
console.log('from server call');
var queryString = "&cmd=privilege&action=user";
return $http.get(config.serviceBaseUri + queryString);
};
factory.privilegesData = function(priv){
console.log('from privilege data');
if(priv && priv.data) {
$rootScope.userFullName = priv.data.firstName + ' ' + priv.data.lastName;
}
console.log('done');
};
Based on the console statments above I'm getting the following output. I need the from home state data rule to occur last...
...since I'm using the the results of the data.rule function for authorization for each state. Below is the $stateChangeStart from my .run method
app.route.js
$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
if(toState.authenticate){
if(authService.authKeyExists()){
if(toState.data.rule($rootScope)){
navigationService.addNavObject("activity", {
summary : "Page navigation",
page : $location.absUrl().replace("#/", "")
});
} else {
$state.go('app.home');
}
} else {
event.preventDefault();
$state.go('login');
}
}
});
The child states need the parent state as a dependency in order for it to wait to load, even if its not used directly. Source
you have to try something like the following instead :
.state('app.home', {
url: '/home',
templateUrl: 'app/components/home/home.html',
controller: 'HomeController',
controllerAs: 'vm',
parent: 'app',
authenticate: true,
data:{
rule: function($rootScope){
console.log($rootScope.userFullName);
return true;
}
}
})
You can use the property resolvepolicy and set it to LAZY as well, a state's LAZY resolves will wait for the parent state's resolves to finish before beginning to load.

angularjs throws Error: [ng:areq] in resolving the lazyloading the scripts and adding a view controller separately

I don't know why this first state works but the second one doesn't:
Working state:
.state('app.pages.invoice', {
url: '/invoice',
templateUrl: "assets/views/pages_invoice.html",
title: 'Invoice',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
Not working state, throwing the Error: [ng:areq]:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
.state('app.form.validation', {
controller: "validationCtrl",
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
This second one works only when the controller is injected via this:
resolve: loadSequence('validationCtrl')
that is (controller is moved into the resolve and there is not currentAuth anymore):
.state('app.form.validation', {
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: loadSequence('validationCtrl')
})
and then I don't know how to integrate my currentAuth element into the resolve again. How can I inject the validationCtrl.js via resolve and add the currentAuth element also into resolve?
here is the loadsequence function:
// Generates a resolve object previously configured in constant.JS_REQUIRES (config.constant.js)
function loadSequence() {
var _args = arguments;
return {
deps: ['$ocLazyLoad', '$q',
function ($ocLL, $q) {
var promise = $q.when(1);
for (var i = 0, len = _args.length; i < len; i++) {
promise = promiseThen(_args[i]);
}
return promise;
function promiseThen(_arg) {
if (typeof _arg == 'function')
return promise.then(_arg);
else
return promise.then(function () {
var nowLoad = requiredData(_arg);
if (!nowLoad)
return $.error('Route resolve: Bad resource name [' + _arg + ']');
return $ocLL.load(nowLoad);
});
}
function requiredData(name) {
if (jsRequires.modules)
for (var m in jsRequires.modules)
if (jsRequires.modules[m].name && jsRequires.modules[m].name === name)
return jsRequires.modules[m];
return jsRequires.scripts && jsRequires.scripts[name];
}
}]
};
}
My first state doesn't have any controller, so I am fine resolving the currentAuth alone. But when the view has a controller, add the currentAuth causes the controller not to work anymore.
note:
my currentAuth is taken from here.
update:
herer is the validationCtrl.js:
app.controller('ValidationCtrl', ["$scope", "$state", "$timeout", "SweetAlert", "$location",
function ($scope, $state, $timeout, SweetAlert, $location) {
...
update 2:
basically the question is to allow only currently signed in users to view the pages which are children of app.; so my parent view is like this: so basically I am looking to inject the currentAuth factory into the main parent view and the children should inherit this. They cannot be viewed unless the currentAuth in the parent is resolved.
$stateProvider.state('app', {
url: "/app",
templateUrl: "assets/views/app.html",
resolve: loadSequence('modernizr', 'moment', 'angularMoment', 'uiSwitch', 'perfect-scrollbar-plugin', 'toaster', 'ngAside', 'vAccordion', 'sweet-alert', 'chartjs', 'tc.chartjs', 'oitozero.ngSweetAlert', 'chatCtrl'),
abstract: true
})
edit 1:
I have put the question in other words as well here and trying to find an answer to multiple resolve states.
edit 2:
here is the main.js: pastebin url
and the validationCtrl.js pastebin url.
actually, the validationCtrl is just an example controller among other controllers I have.
Question is how to block view permission for child views unless the parent currentAuth is resolved? given that I don't know how to handle multiple resolve with loadsequence and a singleton factory.
Assuming you are using ui router as a routing framework for your SPA app.
Error: [ng:areq]
the error you get:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
is due to the declaration of the controller within the state, the name of the controller function is not resolved because "ValidationCtrl" is not equal to "validationCtrl" then correct state is:
.state('app.form.validation', {
controller: "ValidationCtrl",
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
resolve: {
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
Abstract States - Nested States
to answer the second question, a useful example for your case may be this:
$stateProvider.state('app', {
url: "/app",
templateUrl: "assets/views/app.html",
resolve: {
scripts: loadSequence('modernizr', 'moment', 'angularMoment', 'uiSwitch', 'perfect-scrollbar-plugin', 'toaster', 'ngAside', 'vAccordion', 'sweet-alert', 'chartjs', 'tc.chartjs', 'oitozero.ngSweetAlert', 'chatCtrl').deps,
currentAuth: function(Auth){ return Auth.$requireSignIn();}
},
abstract: true
})
.state('app.pages.invoice', {
// url will become '/app/invoice'
url: '/invoice',
templateUrl: "assets/views/pages_invoice.html",
title: 'Invoice'
})
.state('app.form.validation', {
controller: "ValidationCtrl",
// url will become '/app/validation'
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate'
})
As you can see from the example in the resolve of the abstract state you can define different factory functions, ui router will wait until all dependencies are resolved before resolving the children states.
Resolve property explanation :
The resolve property is a map object. The map object contains key/value pairs of:
key – {string}: a name of a dependency to be injected into the controller.
factory - {string|function}:
If string, then it is an alias for a service.
Otherwise if function, then it is injected and the return value is treated as the dependency. If the result is a promise, it is resolved before the controller is instantiated and its value is injected into the controller.
for more details I refer you to ui router doc.
As said in my comment i suggest you to try the following :
.state('app.form.validation', {
url: '/validation',
templateUrl: "assets/views/form_validation.html",
title: 'Form Validate',
controller: "validationCtrl",
resolve:{
"myCtrl": loadSequence('validationCtrl'),
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
}]
}
})
The other part of my comment was about the fact that child states inherits parent's resolve and children can override it.
So you can just do the following :
.state('app', {
// all states require logging by default
resolve:{
"currentAuth": ["Auth", function(Auth) {
return Auth.$requireSignIn();
// i'm guessing we're redirecting toward app.login if not logged
}]
}
})
.state('app.login', {
resolve:{
"currentAuth": ["Auth", function(Auth) {
return true;// just be sure to not do infinite redirections
}]
}
})
Note if you have some trouble because Auth isn't yet loaded with lazy loading, you should be able to load it in a angular.run.
Firstly coming to the error
Error: [ng:areq]:
validationCtrl&p1=not%20aNaNunction%2C%20got%20undefined
controller: "validationCtrl", change it according to the main controller
that is
This means there is no validationCtrl function.
I may be wrong but i think there is a small typo' in this line in your controller controller: "validationCtrl", change it according to the **main controller** controller: "ValidationCtrl"
that is
This error happens due to either defining two angular.modules with the same name in different files containing different arguments as you may be trying to implement the dependancy injection.
It causes the problem as the script loaded inyour main html file won't know which angular.module to be configured.
To resolve this define the angular.modules with different names.
How to block view permission for child views unless the parent currentAuth is resolved
You can install this package angular middlewareThis middleware package contains some pre-defined route functions or you can also create your own functions.Along with this $http documentation using the success and callback functions,you can create your own middleware and the auth service while using a singleton factory
OR
Assuming that you are using node.js as your backend you can use [middleware][3] routing in your server using express and map it to the frontend routes.
Here is a perfect tutorial for middleware authentication in nodejs

Angular route resolve calling a service

I'm struggling to get Angular route resolve working. I find the documentation less than useless for more complex parts of the javascript framework like this.
I have the following service:
app.service("AuthService", ["$http", "$q", function($http, $q){
this.test = function(){
return $q(function(resolve, reject){
var auth = false;
resolve(auth);
});
}
}]);
Now my routes looks like this:
app.config(["$routeProvider", "$locationProvider", function($routeProvider, $locationProvider){
$locationProvider.html5Mode(true).hashPrefix("!");
$routeProvider
.when("/account", {
templateUrl: "/views/auth/account.html",
controller: "AccountController",
resolve: {
auth: ["AuthService", function(AuthService) {
return AuthService.test().then(function(auth){
if (auth) return true;
else return false;
});
}]
}
});
}]);
What I want to happen here is the following:
User goes to /account
AuthService is fired and returns a variable (true or false)
In the resolve, if the returned value is false, the route cannot be loaded
If the returned value is true, the user is authenticated and can view the route
I don't think I've fully understood how to use resolve and my method so far does not work. Could someone please explain the correct way to do this?
The resolve block when configuring routes is designed to make navigation conditional based upon the resolution or rejection of a promise.
You are attempting to handle this by resolving with a resolution value of true or false
Please try the following:
resolve: {
auth: ["AuthService", function(AuthService) {
return AuthService.test().then(function(auth){
if (!auth){
throw 'not authorized';
}
});
}]
}
This will cause the promise to be rejected and therefore not allow the routing to continue/complete.
Also of note is that the value coming out of the promise resolution will be injected into the handling controller
This solution works for me, I also tried with .then function but it's incorrect because resolve performs it.
resolve: {
auth:
["AuthService", "AnotherService", "$rootScope", function(AuthService, AnotherService, $rootScope) {
if ($rootScope.yourCondition){
return AuthService.getFunction();
}
else{
return AnotherService.getAnotherFunction();
}
}]
},
views: {
'partialView#': {
/* == Component version == */
component: "yourComponent",
bindings: {
auth: 'auth', // Inject auth loaded by resolve into component
params: '$stateParams'
}
}
}

Angular ui-router, possible to resolve an authentication check before other resolves?

When using the resolves in ui-router, is it possible to have something resolve first (an authentication check for example) before the other resolves on a state are even started?
This is an example of how I'm doing my authentication check right now:
angular.module('Example', [
'ui.router',
'services.auth',
'services.api'
])
.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('order', {
url: '/order',
views: {
'main': {
templateUrl: 'order/order.tpl.html',
controller: 'OrderCtrl'
}
},
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', function (auth) {
return auth.loggedIn();
}],
cards: ['api', function (api) {
return api.getCards();
}],
shippingAddresses: ['api', function (api) {
return api.getShippingAddresses();
}]
}
});
}]);
I'd like loggedIn to resolve first before cards and shippingAddresses start.
I could just put the other API calls in the controller (that's where they were before), but I'd like the data retrieved from them to be available before the templates start rendering.
This might work but then I couldn't inject the resolved values of each individual API call.
...
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', 'api', function (auth, api) {
return auth.loggedIn()
.then(api.getCards())
.then(api.getShippingAddresses())
.then(function () {
// whatever....
});
}]
}
...
Edit:
Looks like the answer here solves my issue, maybe it's not the best way to solve this problem but it works.
Just by injecting the resolved value of the authentication check it will wait until it is resolved before continuing on with the other resolves.
...
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', function (auth) {
return auth.loggedIn();
}],
cards: ['api', 'loggedIn', function (api, loggedIn) {
return api.getCards();
}],
shippingAddresses: ['api', 'loggedIn', function (api, loggedIn) {
return api.getShippingAddresses();
}]
}
...
I would answer this myself but I'm going to leave it open for other people to provide their solutions.
I've provided some proof of concept how promises can be combined together: http://jsfiddle.net/pP7Uh/1/
Any rejects will prevents from calling next then method
resolve: {
data: function (api) {
var state;
return api.loggedIn().then(function(loggedIn) {
state = loggedIn
return api.getCards();
}).then(function(getCards) {
return {
loggedIn: state,
getCards: getCards
}
}).catch(function(error) {
console.error('error', error)
});
}
}
After this in data resolved object you will have:
{
loggedIn:"calling loggedIn",
getCards:"calling getCards"
}

Categories

Resources