I am building routes/states and the menu based on what the user is authorized to see. I've looked around and tried a few different things, but i'm hitting a brick wall. The SessionService object in the RoleService Factory is empty whenever RoleService.validateRole() is called. No route is added and the app is effectively dead. Why is the injected factory empty and the methods undefined.
Here is a simplified layout of the app starting in order of dependencies.
In app.run(), I am adding the states to the app instead of doing it in the config.
$stateProviderRef.state(value.stateName, state);
The states come from (a factory) AppConfig.getStates(), which returns an array.
var states = AppConfig.getStates();
In getStates() we validate each route's role.
if(RoleService.validateRole(routes[i].role))
The RoleService depends on the SessionService and the validateRole function does this check:
if(SessionService.currentUser.role === role)
The SessionService depends on the AuthenticationService which is just a factory that returns a promise using $http (the user object). The SessionService.currentUser is a function that .then()s the returned promise from the AuthenticationService.
return {
currentUser: function(){
AuthenticationService.then(function(result){
return result;
});
}
};
I'm not sure of a better way to explain the code without including the entire files.
Based on the plunker (mentioned in comment), I updated/cloned it to another, which is working
I. simple - when static data are returned (no $http)
Because the service SessonService was defined like this:
return {
currentUser: function() {
...
we cannot call it as a property:
...
return {
validateRoleAdmin: function () {
if (SessionService.currentUser.role === 'admin') {
...
},
validateRole: function (role) {
if(SessionService.currentUser.role === role){
...
it is a function it must be called as a function currentUser():
return {
validateRoleAdmin: function () {
if (SessionService.currentUser().role === 'admin') {
...
},
validateRole: function (role) {
if(SessionService.currentUser().role === role){
...
II. waiting for async calls
The adjusted example
Next, if we in example create a static result of the service AuthenticationService:
angular.module('daedalus').factory('AuthenticationService',
function() {
return {"idsid": "ad_jdschuma","role": "user","id": "33333"}
}
)
we cannot expect there will be some then method:
currentUser: function() {
//AuthenticationService.then(function(result) {
// return result;
//});
return AuthenticationService;
}
And to make it really async we can replace it with this:
angular.module('daedalus').factory('AuthenticationService',
['$timeout', function($timeout) {
return {
getData: function() {
return $timeout(function() {
return {
"idsid": "ad_jdschuma",
"role": "user",
"id": "33333"
}
})
}
};
}])
And then use even the .then() - Session service:
angular.module('daedalus').factory('SessionService', ['AuthenticationService',
function(AuthenticationService) {
return {
currentUser: function(){
return AuthenticationService
.getData()
.then(function(result){
return result;
});
}
};
}]
)
And the RoleService:
return {
...
validateRole: function(route) {
console.log('SessionService currentUser: ' + JSON.stringify(SessionService))
return SessionService
.currentUser()
.then(function(userRole) {
if (userRole.role === route.role) {
return route;
} else {
return null;
}
})
}
And with this in place in appConfig
getStates: function(){
var items = [];
var deffered = $q.defer();
var validatedCount = routes.length;
for(var i=0,len=routes.length; i<len; i++){
var route = routes[i];
RoleService
.validateRole(route)
.then(function(route){
if(route) {
items.push(route.stateConfig)
}
if(--validatedCount === 0 ){ // all processed
deffered.resolve(items)
}
})
}
return deffered.promise;
}
We can do that in run:
AppConfig
.getStates()
.then(function(states) {console.log(states)
angular.forEach(states, function(value, key) {
var state = {
"url": value.url,
"templateUrl": value.templateUrl,
"controller": value.controller
};
$stateProviderRef.state(value.stateName, state);
});
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.sync();
});
$urlRouter.listen();
Check it here
The concept of the second solution (async) is too .thenified(). I just intended to show that all is working. Better approach how to get security data is completely covered here:
Confusing $locationChangeSuccess and $stateChangeStart
Related
I have two services:
ProductService
CartService
And a Controller:
ShoppingController
ShoppingController needs to download the cart from the server. In order for CartService to do this, ProductService must first download the products.
ProductService.js
ProductService.DownloadProducts = function(){
if(alreadyHaveDownloadedProducts){
return {
success: function (fn) {
fn(products);
}
};
}else{
if(getProductsPromise== null){
getProductsPromise= $http.post("Api/GetProducts")
}
return getProductsPromise;
}
CartService.js
CartService.DownloadCart = function(){
ProductService.DownloadProducts().success(function(){
if(alreadyHaveDownloadedCart){
//Dont need to go to server
return {
success: function (fn) {
fn(cart);
}
};
}else{
if(getCartPromise == null){
getCartPromise = $http.post("Api/GetCart")
}
return getCartPromise; //<= The promise I actually want to return
}
})
}
ProductService.DownloadProducts
ShoppingController.js
CartService.DownloadCart().success(function(){DisplayCart()});
This approach works nicely so far, because if the ProductService has already been called on a different page, I don't need to go back to the server. Same for cart service.The issue is I currently can't return the getCartPromise as it hasn't been created until the async GetProducts has returned
Is it possible to structure this so I can return the inner promise to ShoppingController while keeping the nice .success() syntax?
My way is similar to yours, but instead of saving some alreadyHaveDownloadedCart (boolean flag), i'm caching the promise it self, and then returns it.
I'm caching the promise for the data on the service class, and then returning it if it is exists otherwise initialize a server call.
Something like that:
class ProductService {
constructor() {
this.productsPromise = null;
}
DownloadProducts() {
if(!this.productsPromise) {
this.productsPromise = $http.post('Api/GetProducts');
this.productsPromise.then(products => this.products = products);
}
return this.productsPromise.then(() => this.products);
}
}
class CartService {
constructor(ProductService) {
this.ProductService = ProductService;
this.cartPromise = null;
}
DownloadCart() {
return this.ProductService.DownloadProducts().success(() => {
if (!this.cartPromise) {
this.cartPromise = $http.post('Api/GetCart');
this.cartPromise.then((cart) => {
this.cart = cart;
});
}
return this.cartPromise.then(() => this.cart);
});
}
}
I have an AngularJS 1.6.3 app and am having an issue with $q.all() not correctly resolving. It seems that the resolve function in my config (sessionService.initializeApp()) is not even getting called at all.
The console.log() statements are not being run, and the network requests are not being made. However, if I change sessionService.initializeApp() to simply be a variable instead of a function call with a return (i.e. var initializeApp = return $q.all(...)), and just call sessionService.initializeApp in the resolve, the network requests are made, but the console.log() statements do not run, and the controller does not instantiate.
I feel like I am returning the promises correctly, but obviously something is wrong since the resolve is not actually resolving. Any ideas here?
app.config.js
//more code above...
.when('/dashboard', {
slug: 'dashboard',
templateUrl: 'app/dashboard/dashboard.html',
controller: 'DashboardController',
options: {
title: 'Dashboard'
},
resolve: {
appReady: function(sessionService) {
console.log('resolving...');
return sessionService.initializeApp();
}
}
})
//more code below...
session.service.js
//more code above...
var state = {
appReady: false,
loading: false,
data: {}
};
var service = {
initializeApp: initializeApp,
getData: getData,
setData: setData
};
function initializeApp() {
return $q.all({
user: ajaxService.get('SupplierService.svc/me', {}),
states: ajaxService.get('SupplierService.svc/states', {}),
utilityTypes: ajaxService.get('SupplierService.svc/utilities/types', {}),
roles: ajaxService.get('SupplierService.svc/contacts/types', {})
}).then(function(response) {
setData('user', response.user['data']);
setData('states', response.states['data']);
setData('utilityTypes', response.utilityTypes['data']);
setData('roles', response.roles['data']);
}).catch(function(e) {
console.log(e);
}).finally(function() {
state.appReady = true;
});
}
function getData(key) {
if (key) {
return state.data[key];
} else {
return state.data;
}
}
function setData(key, val) {
state.data[key] = val;
}
return service;
//more code below...
dashboard.controller.js
//more code above...
function DashboardController($q, ajaxService, sessionService) {
console.log('dashboard...');
});
//more code below...
catch the promise from controller instead of the service. in the service just return the $q.all
//more code above...
var service = {
initializeApp: initializeApp
};
function initializeApp() {
return $q.all({
user: ajaxService.get('SupplierService.svc/me', {}),
states: ajaxService.get('SupplierService.svc/states', {}),
utilityTypes: ajaxService.get('SupplierService.svc/utilities/types', {}),
roles: ajaxService.get('SupplierService.svc/contacts/types', {})
})
}
return service;
catch the promise in controller
function DashboardController($q, ajaxService, sessionService,setData) {
sessionService..then(function(response) {
setData('user', response.user['data']);
setData('states', response.states['data']);
setData('utilityTypes', response.utilityTypes['data']);
setData('roles', response.roles['data']);
}).catch(function(e) {
console.log(e);
}).finally(function() {
state.appReady = true;
});
});
For now, I've given up on using resolve and just modified sessionService.initializeApp() to check for whether the app is ready, and if so, just return a resolved promise immediately so I don't have to refetch resources from the server.
function initializeApp() {
if (isAppReady()) {
return $q.when('appReady');
}
return $q.all({
user: ajaxService.get('SupplierService.svc/me', {}),
states: ajaxService.get('SupplierService.svc/states', {}),
utilityTypes: ajaxService.get('SupplierService.svc/utilities/types', {}),
roles: ajaxService.get('SupplierService.svc/contacts/types', {})
}).then(function(response) {
setData('user', response.user['data']);
setData('states', response.states['data']);
setData('utilityTypes', response.utilityTypes['data']);
setData('roles', response.roles['data']);
}).catch(function(e) {
console.log(e);
}).finally(function() {
setAppReady(true);
});
}
And then in my controllers, I just do:
sessionService.initializeApp().then(function() {
//do stuff
});
i want to share data between components, so im implemented a Service which has an EventEmitter.
My Service looks like this:
#Injectable()
export class LanguageService {
constructor() {
this.languageEventEmitter = new EventEmitter();
this.languages = [];
this.setLanguages();
}
setLanguages() {
var self = this;
axios.get('/api/' + api.version + '/' + api.language)
.then(function (response) {
_.each(response.data, function (language) {
language.selected = false;
self.languages.push(language);
});
self.languageEventEmitter.emit(self.languages);
})
.catch(function (response) {
});
}
getLanguages() {
return this.languages;
}
toggleSelection(language) {
var self = this;
language.selected = !language.selected;
self.languages.push(language);
self.languageEventEmitter.emit(self.languages);
}
}
I have to components, which are subscribing to the service like this:
self.languageService.languageEventEmitter.subscribe((newLanguages) => {
_.each(newLanguages, function (language) {
self.updateLanguages(language);
});
});
When both components are loaded, the language arrays get filled as i wish.
This is the first component:
export class LanguageComponent {
static get parameters() {
return [[LanguageService]];
}
constructor(languageService) {
var self = this;
this.languageService = languageService;
this.languages = [];
this.setLanguages();
}
setLanguages() {
var self = this;
self.languageService.languageEventEmitter.subscribe((newLanguages) => {
_.each(newLanguages, function (language) {
self.updateLanguages(language);
})
});
}
updateLanguages(newLanguage) {
var self = this;
if (!newLanguage) {
return;
}
var match = _.find(self.languages, function (language) {
return newLanguage._id === language._id;
});
if (!match) {
self.languages.push(newLanguage);
}
else {
_.forOwn(newLanguage, function (value, key) {
match[key] = value;
})
}
toggleLanguageSelection(language) {
var self = this;
self.languageService.toggleSelection(language)
}
}
When LanguageComponent executes the function toggleLanguageSelection() which triggered by a click event, the other component, which subscribes like this:
self.languageService.languageEventEmitter.subscribe((newLanguages) => {
_.each(newLanguages, function (language) {
self.updateLanguages(language);
})
});
doesn't get notfiefied of the change. I think this happens because both component get a different instance of my LanguageService, but i'm not sure about that. I also tried to create a singleton, but angular'2 di doesn't work then anymore. What is the reason for this issue and how can i solve this ?
You need to define your shared service when bootstrapping your application:
bootstrap(AppComponent, [ SharedService ]);
and not defining it again within the providers attribute of your components. This way you will have a single instance of the service for the whole application. Components can leverage it to communicate together.
This is because of the "hierarchical injectors" feature of Angular2. For more details, see this question:
What's the best way to inject one service into another in angular 2 (Beta)?
Im currently having an issue where when I call a function in a service that changes its own internal variable it doesn't appear to stay unless I change it from a controller. When I output the variable to the console right after I change it, it appears with the correct value. However when I call a function in the controller that prints the object to console it shows the original value.
The variable Im having issues with is "isSignedIn" which always shows false (confirmed with the $scope.verify method in login controller) even after the line "console.log('Signin:' + isSignedIn);" shows true.
Thanks ahead of time for any guidance!
Service Skype
angular.module('Skype', ['ng'])
.service('skypeClient', function () {
//Service Properties
var client = new Skype.Web.Model.Application;
this.errors = [];
this.errorCount = -1;
var isSignedIn = false;
var state = 'SignedOut';
var init = function () {
client.signInManager.state.when('SignedIn', function () {
isSignedIn = true;
console.log('Signin:' + isSignedIn); // This outputs the correct value
});
property = client.signInManager.state;
property.changed(function (status) {
state = status;
console.log("New State"+status) });
}
//Signin Function
var signIn = function (username,password) {
client.signInManager.signIn({
username: username,
password: password
}).then(function () {
isSignedIn = true;
this.errorCount++;
console.log(this.errorCount);
});
}
//SignOut Function
var signOut = function () {
client.signInManager.signOut()
.then(function () {
this.isSignedIn = false;
}, function (error) {
this.erros.push(error);
this.errorCount++;
});
}
return {
signIn: signIn,
signOut: signOut,
init: init,
state: state,
isSignedIn: isSignedIn
};
});
Controller Login
'use strict';
angular.module('login', ['ngRoute', 'classy', 'Metio.Skype'])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/login', {
templateUrl: 'app/views/login.html',
controller: 'loginCntrl'
});
}]).controller('loginCntrl', function ($scope,skypeClient) {
skypeClient.init();
$scope.skypeClient = skypeClient;
console.log('LoginCntrl Loaded');
$scope.signIn = function () {
skypeClient.signIn($scope.user, $scope.password);
}
$scope.signOut = function () {
skypeClient.signOut();
}
$scope.verify = function () {
console.log(skypeClient);
}
});
[EDIT]
Modified Code according to pdenes recommendations(comments), same issue but cleaner
Factory Skype
.factory('skypeClient', function () {
//Service Properties
var client = new Skype.Web.Model.Application;
var state = 'SignedOut';
//Initialize Listeners
var init = function () {
client.signInManager.state.when('SignedIn', function () {
console.log('Signin:' + state); // This outputs the correct value
});
property = client.signInManager.state;
property.changed(function (status) {
state = status;
console.log("New State" + state);
});
console.log('init');
}
//Signin Function
var signIn = function (username, password) {
client.signInManager.signIn({
username: username,
password: password
}).then(function () {console.log('LoggedIn');});
}
init();
return {
signIn: signIn,
state: function(){return state}
}
});
Controller Login
.controller('loginCntrl', function ($scope,skypeClient) {
$scope.skypeClient = skypeClient;
$scope.signIn = function () {
skypeClient.signIn($scope.user, $scope.password);
}
$scope.verify = function () {
console.log(skypeClient);
console.log($scope.skypeClient);
}
});
DOM Markup
<input type="checkbox">Keep Me Signed In (Currently Signedin: {{skypeClient.state()}} )
Console Output and DOM Changes
When I hit the signin function the console logs "New State SigningIn" and the dom changes to "Currently Signedin: SigningIn" but when I get the next event fired the console logs "New State SignedIn" but the DOM still reflects the old value "Currently Signedin: SigningIn" so the binding only appears to update the first time but not subsequent times.
The value in the object that you return as the service is not the same as the isSignedIn variable you defined earlier. So:
...
var isSignedIn = false;
...
return {
isSignedIn: isSignedIn
// ^this is false when the object is created
// and will remain false, no matter how you
// change the value of the above variable later
};
To expose the value of isSignedIn in your closure from the service, you'd need a function, something like:
...
return {
...
isSignedIn: function () {
return isSignedIn; // <- this refers to the above variable itself
}
};
EDIT: Follow up for the updated code in the question...
So apparently the value is updated properly internally, but the {{skypeClient.state()}} binding is still not updated (only when some other action somehow "forces" it). This is probably happening because the value is updated by something outside of Angular's digest cycle, so there is nothing telling Angular to update things at the right moment.
Looking at the code, state is assigned a new value inside a callback for property.changed (btw, property should properly be declared with var!). Try wrapping that line in an $apply() to make Angular update the bindings!
In place where you call service (factory) value that is returned by function add a $watch. That will update value if it changes in the service.
$scope.$watch(function() { return ModalService.getInfo(); }, function(){
$scope.info = ModalService.getInfo();
});
See solution
I have moved some common code to factory. but the controller is executing before factory get loaded. In this case i am getting the blank response(zero results)
can anyone suggest the best solution.
here is my angular factory,
app.factory('TabsFactory', function($resource){
var activetabs = {};
activetabs.getDepositAccountDetails = function() {
return $resource('xxxx/:number', {}, {
getDepositAccountDetailsService: {
method: 'GET',
isArray: false
}
});
}
activetabs.getAccountInfo = function(){
return accountinit.accountInfo;
}
activetabs.setAccountInfo = function(accountnumber, result) {
var accountinit = {
accountInfo: []
}
if (result.code == "v") {
activetabs.getDepositAccountDetails().getDepositAccountDetailsService({
number: accountnumber
}).$promise.then(function(response) {
accountinit.accountInfo = response;
//here i am getting the JSON response
}, function(error) {
});
}
return accountinit;
}
return activetabs;
});
controller,
TabsFactory.setAccountInfo(accountnumber, $scope.accountInfo);
$scope.accountInfo = TabsFactory.getAccountInfo();
alert(JSON.stringify($scope.accountInfo));
You should use chain promise to update scope variable, because your accountInfo variable is updated inside $resource promise.
Code
TabsFactory.setAccountInfo(accountnumber, $scope.accountInfo).then(function(data){
$scope.accountInfo = TabsFactory.getAccountInfo();
alert(JSON.stringify($scope.accountInfo));
});
Update
Service method should return promise inorder to continue promise chain
activetabs.setAccountInfo = function(accountnumber, result) {
var accountinit = {
accountInfo: []
}
if (result.code == "v") {
//added return below
return activetabs.getDepositAccountDetails().getDepositAccountDetailsService({
number: accountnumber
}).$promise.then(function(response) {
accountinit.accountInfo = response;
return accountinit.accountInfo;
//here i am getting the JSON response
}, function(error) {
});
}
return accountinit;
}
Yes, this will happen because of JavaScript executing asynchronous operations but your controller in such a way that it expects things to be synchronous operations.
When you call TabsFactory.getAccountInfo() its possible that your $resource('xxxx/:number') is still not completed and response ready for you to process!!
So, what to do? You have make use of promise. I usually have a repository (A factory with method that return promise) to handle server communications. Here is an example:
app.factory('accountRepository', ["$http","$q",function($http,$q){
return {
getDepositAccountDetails : function(id) {
var deferred = $q.defer();
$http.ger('xxx').success(deferred.resolve).error(deferred.reject);
return deferred.promise;
}
};
}] );
My repository will have more operations like add account, update account info etc..
my controller/service then calls these methods as follows:
accountRepository.getDepositAccountDetails(123).then(function(response) {
// Process the response..
}, function(error) {
// Some error occured! handle it
});
doing so, my code gets executed only after I get response from server and data is ready for consumption or display. Hope this helps..
Update: You might want to have a look at this to get the idea ;)