AngularJS : Service variable changes not applying - javascript

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

Related

Jasmine and angular mocks : mocking a service that handles local storage

I have one service called wd$cache, that is basically a wrapper for localStorage.setItem and get.item.
Now I'm trying to test a controller that uses that service to achieve a certain result. The main problem is that I have an IF statement that gets triggered only if you have localstorage set already which is driving me nuts! (we are doing TDD here)
SERVICE
(function () {
angular
.module('hub')
.controller('promotionNotificationCtrl', promotionNotificationCtrl);
promotionNotificationCtrl.$inject = [
'hub$promotions',
'hub$client',
'wd$cache'
];
function promotionNotificationCtrl(
hub$promotions,
hub$client,
wd$cache) {
var vm = this;
activate();
//////////
function activate () {
hub$promotions.get(hub$client.brand, hub$client.subbrand).success(function (data) {
if (!wd$cache.get('hub$notification')) {
wd$cache.add('before', 123);
} else {
wd$cache.add('after', 321);
}
});
}
}
})();
TEST
describe('The promotion notification controller', function () {
var controller,
hub$client,
$httpBackend,
wd$cache,
mockData = [{
"foo": "bar"
},
{
"faa": "boo"
}];
beforeEach(module('hub'));
beforeEach(module('wired.core'));
beforeEach(module(function ($provide) {
hub$client = {
brand: 'bw',
subbrand: 'plus'
};
wd$cache = {
add: function () {
},
get: function () {
}
};
$provide.value('hub$client', hub$client);
$provide.value('wd$cache', wd$cache);
spyOn(wd$cache, 'add');
}));
beforeEach(inject(function ($controller, _$httpBackend_, _hub$promotions_) {
controller = $controller('promotionNotificationCtrl');
$httpBackend = _$httpBackend_;
hub$promotions = _hub$promotions_;
// request
$httpBackend.expectGET("/umbraco/api/promotions/get/?brand=bw&lang=en&subbrand=plus").respond(200, mockData);
$httpBackend.flush();
}));
it('should attempt to add a cache with a "before" key if no previous "hub$notification" cache was found', function () {
expect(wd$cache.add).toHaveBeenCalledWith('before', 123); //WORKING
})
it('should attempt to add a cache with a "after" key if a previous "hub$notification" cache was found', function () {
localStorage.setItem('hub$notification');
wd$cache.add('hub$notification');
expect(wd$cache.add).toHaveBeenCalledWith('after', 123); // NOT WORKING
// CANT GET THROUGH THE IF STATEMENT
})
});
Basically I can never get to 'Test Cases' after BeforeEach block, whatever I do. I've tried everything, since mocking it to use actual storage.
Any ideas?
You can provide a mock implementation that is already filled with some data:
var cache = {};
beforeEach(module(function ($provide) {
// ...
wd$cache = {
add: function (key, value) {
cache[key] = value;
},
get: function (key) {
return cache[key];
}
};
// add initial data here or in the individual tests, e.g.
// ...
}));
To set up the cache properly for a specific testcase you can use the cache field like this:
cache['hub$notification'] = 'whatever value makes sense here';
Of course you can also do this in beforeEach.
Currently you are trying to do it like this:
wd$cache.add('hub$notification');
expect(wd$cache.add).toHaveBeenCalledWith('after', 123);
This is problematic for two reasons:
You are not updating the cache because you are spying on the add method without .andCallThrough(). You should fix this (add .andCallThrough() after spy creation) otherwise updates from the controller will not affect the cache.
The spy records your call instead. You don't want this for setup code because it makes subsequent assertions more complicated.

Generate Routes in .run() with config from service Angularjs

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

Update value to the database only when changed by user or suspend certain subscription if value changed by code

From the viewmodel I want to update value to the server straight after it was changed in the view.
class OrderLine
{
itemCode: KnockoutObservable<string>;
itemName: KnockoutObservable<string>;
constructor(code: string, name: string)
{
this.itemCode = ko.observable(code);
this.itemName = ko.observable(code);
//subscribers
this.itemCode.subscribe(this.updateCode, this, "change");
this.itemName.subscribe(this.updateName, this, "change");
}
updateCode = (newvalue: string) =>
{
//Update value to the server
}
updateName = (newvalue: string) =>
{
//Update value to the server
}
}
Both values can be changed by user and with explicit subscriptions updating to the server/database works fine.
In the server side when updating itemCode will update value of itemName too. So response to the client will return json object with new value for itemName
Here i have a problem, because changing value of itemName in the viewmodel will fire a subscription callback method which will update same value to the server again
updateCode = (newvalue: string) =>
{
//Update value to the server
//in the handler of the successful request
this.itemName(updatedvaluefromServer); //-- this trigger another request
}
Question: is it possible to change value of KnockoutObservable which will notify only view subscribers?
Or is it possible to detect that value was changed from the view?
I tried used "sneaky update" by #RPNiemeyer from here https://stackoverflow.com/a/17984353/1565525
but this approach will suspend all subscribers from being notified, including view's subscribers
Here's a pattern that utilizes a computed observable backed by a regular observable:
class OrderLine
{
private _itemCode: KnockoutObservable<string>;
private _itemName: KnockoutObservable<string>;
itemCode: KnockoutComputed<string>;
itemName: KnockoutComputed<string>;
constructor(code: string, name: string)
{
this._itemCode = ko.observable(code);
this._itemName = ko.observable(code);
this.itemCode = ko.computed({
read: () => this._itemCode(),
write: (newValue) => {
this._itemCode(newValue);
// Update server
// In the handler of the successful request:
this._itemName("...");
}
});
this.itemName = ko.computed({
read: () => this._itemName(),
write: (newValue) => {
this._itemName(newValue);
// Update server
}
});
}
}
In the callback on succesful Ajax calls you update the backing observable, not the write in the computed, as to prevent the problem you're having.
Here's what I was talking about. code is a normal observable, and name is a writable computed. When code is updated, the read value of name will be updated. When name is written to, code is updated, which updates the read value of name. Only the update of the read value is a change in the observable value of name, so there are not two updates to name.
If you watch the console, you will see that updating either of the fields generates one update to each of them.
function orderLine(code, name) {
return {
code: code,
name: name
};
}
var serverValues = [
orderLine(1, 'one'),
orderLine(2, 'two'),
orderLine(3, 'three')
];
function getNameFromCode(code) {
var found = ko.utils.arrayFilter(serverValues, function(line) {
return line.code == code;
});
if (found.length == 0) return '';
return found[0].name;
}
function getCodeFromName(name) {
var found = ko.utils.arrayFilter(serverValues, function(line) {
return line.name == name;
});
if (found.length == 0) return '';
return found[0].code;
}
function vm() {
var self = {};
self.code = ko.observable();
self.name = ko.computed({
read: function() {
return getNameFromCode(self.code());
},
write: function(newValue) {
console.debug("Writing code");
self.code(getCodeFromName(newValue));
}
});
self.code.subscribe(function(newValue) {
console.debug("Updating code to:", newValue);
});
self.name.subscribe(function(newValue) {
console.debug("Updating name to:", newValue);
});
return self;
}
ko.applyBindings(vm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<label>Code</label>
<input data-bind="value:code" />
<br />
<label>Name</label>
<input data-bind="value:name" />

Angular factory: how to use method in object being declared?

I am creating a factory for my angular app.
I am asking how to use a method (here setCurrentUser()) in the same factory I am declaring:
app.factory('User', function ($rootScope) {
var User = {
create: function (authUser, username) {
// ...
setCurrentUser(username); // <== ERROR
});
},
// ...
setCurrentUser: function (username) {
$rootScope.currentUser = User.findByUsername(username);
},
// ...
};
return User;
});
Using this.setCurrentUser(username) gives an undefined error, of course...
Please note I need setCurrentUser to be a method of User factory, since I have to use it from controllers (I can't define it to be a local function of User factory...).
What you could do instead is use functions as local vars and return the User object (your public API) like this:
app.factory('User', function ($rootScope) {
var setCurrentUser = function (username) {
$rootScope.currentUser = ...
};
var create = function(authUser, username) {
// ...
setCurrentUser(username); // <== NO ERROR
};
return {
create: create,
setCurrentUser: setCurrentUser
};
});
I'd try to use a closure, since by the time at which you call User.create() User is defined, so you can call setCurrentUser.
User.setCurrentUser(username);
Try to declare setCurrentUser function before create function declaration. If this won't help, you can use this trick:
var that = this;
and in User object call setCurrentUser as:
that.setCurrentUser

How is it posible to set a var property from within a function defined in that var?

I have a question regarding definitions of factories in AngularJS. I am unsure if this is a Javascript doubt or AngularJS, but I believe it's Angular.
Say I have a factory definition like this:
angular.module('myApp.services')
.factory('User', function($http) { // injectables go here
var backendUrl = "http://localhost:3000";
var service = {
// our factory definition
user: {},
setName: function(newName) {
service.user['name'] = newName;
},
setEmail: function(newEmail) {
service.user['email'] = newEmail;
},
save: function() {
return $http.post(backendUrl + '/users', {
user: service.user
});
}
};
return service;
});
How is it possible that the function setName is able to set service.user['email'], if service is actually defining setName itself?
This is basic javascript. Forget about the factory for a minute, only consider definition of service.
var service = {
//service will have an empty user js object
user: {},
//a key setName with the value as a function
//which sets a key value pair in user defined above
//service = { user: {name: 'Tony'} }
setName: function(newName) {
service.user['name'] = newName;
},
//a key setEmail with the value as a function
//which sets a key value pair in user defined above
//(consider setName has been called already)
//service = { user: {name: 'Tony', email: 'tony#stark.com'} }
setEmail: function(newEmail) {
service.user['email'] = newEmail;
},
//a key save with the value as a function
//which saves
save: function() {
//return something from a call mimicing $http.post
//return $http.post(backendUrl + '/users', { user: service.user });
}
}
Then, return or use service which has become an API by itself.
Check out dev tool console for log in this FIDDLE
setName function is going to be called by some other code like this,
User.setName("newname "); when this is called the service already defined and it's about setting new value to a property.
and you could use this keyword to make the code more meaningful,
setName: function(newName) {
this.user['name'] = newName;
}
I see one issue with your code, even though you have defined setName function you have the user variable is accessible to outside of the factory,now you can do like this as well,
User.user['name'] = newName;
I think what you need to do is make user a private variable,
angular.module('myApp.services')
.factory('User', function($http) { // injectables go here
var backendUrl = "http://localhost:3000";
var user={};
var service = {
// our factory definition
setName: function(newName) {
user['name'] = newName;
},
setEmail: function(newEmail) {
user['email'] = newEmail;
},
getUser:function(){
return user;
},
save: function() {
return $http.post(backendUrl + '/users', {
user: service.user
});
}
};
return service;
});
What you're describing is a behavior of JavaScript's closures.
Basically, a function has access to the variables outside of the function's scope. If those variables change, it's changed everywhere.
When the functions are created, service is still undefined
The functions create a closure around the service variable
service is then assigned to be the object
All the functions are now pointing to the service object
Here's a way to visualize this:
var service = null;
var getService = function() { return service; };
service = 5;
getService(); // returns 5
service = "service can change";
getService(); // returns "service can change";

Categories

Resources