Scenario
I have a service UserService that maintains the (boolean) sessionStatus of the user.
The view conditionally shows [LOGOUT] on ng-show=sessionStatus (i.e. if not logged in (false), no show).
sessionStatus of ViewController should therefore always match that of UserService ... right?
If you click [LOGOUT] when it's visible, it does some loggy outty things, sessionStatus value changes, and view should update with new outcome of ng-show....
Problem
Currently, clicking logout does not seem to update var UserService.sessionStatus?
How do I keep $scope.sessionStatus updated when UserService.logout() occurs and changes UserService.sessionStatus?
How do I map the changing $scope to ng-show?
Files
View
<a ng-show="sessionStatus" ng-click="logout()">Logout</a>
ViewController
app.controller('AppController', function($scope, $interval, $http, UserService) {
$scope.logout = function() { UserService.logout(); }
// This ain't working
$scope.$watch(UserService.sessionStatus, function() {
$scope.sessionStatus = UserService.sessionStatus;
});
});
UserService
NB: appUser is an injected global var in the HTML head (a hacky fix until I get session/cookie stuff working properly)
app.factory('UserService', function($http) {
var pre;
var sessionStatus;
function init() { // Logged in : Logged out
pre = appUser.user != undefined ? appUser.user : { name: 'Logged out', uid: '0' };
sessionStatus = pre.uid != "0" ? true : false;
}
function resetSession() { appUser = null; init(); }
init();
return {
sessionStatus: function() { return sessionStatus; }, // update on change!
logout: function() {
$http.get("/logout").then(function (data, status, headers, config) {
resetSession();
})
}
};
});
Instead of a watch, simply use a scoped function that returns the session status from the service.
$scope.sessionStatus = function() {
return userService.sessionStatus();
};
Your Logout link would look as below:
<a ng-show="sessionStatus()" ng-click="logout()">Logout</a>
A stripped down Plunker for your functionality: http://plnkr.co/edit/u9mjQvdsvuSYTKMEfUwR?p=preview
Using a scoped function is cleaner and is the "correct" way to do it. Yet, for the sake of completeness you could also have fixed your watch:
$scope.$watch(function () {
return UserService.sessionStatus;
}, function() {
$scope.sessionStatus = UserService.sessionStatus;
});
The first argument of the $watch method takes a WatchExpression which can be a string or a method.
But again, $watch should not be used in controllers. Using scoped methods as suggested are cleaner and easier to test.
Related
I am wrapping a web service client into an angular service. This particular client works emmiting events for particular updates like so:
app.service('AngularClient', function () {
this.info = null
this.login = function () {
client.login(loginInfo).then(function (loggedClient) {
loggedClient.on('newInfo', function (info) {
this.info = info
})
})
}
})
A controller uses this service and binds it to its $scope:
app.controller('Ctrl', function (AngularClient, $scope) {
$scope.client = AngularClient
})
However, anytime the 'newInfo' event gets fired angular doesn't automatically trigger a digest cycle, so I can't control when the info gets updated in the UI. What's the angular way of making sure this happens everytime?
If you want to keep receiving updates to the login event, you could do something like this:
app.service('AngularClient', function () {
this.info = null
this.loginCallback = null
this.login = function () {
client.login(loginInfo).then(function (loggedClient) {
loggedClient.on('newInfo', function (info) {
this.info = info
if (this.loginCallback) this.loginCallback(info);
})
})
}
})
app.controller('Ctrl', function (AngularClient, $scope) {
$scope.client = AngularClient
AngularClient.loginCallback = function(info, err){ // optional error
$scope.user.property = info.property;
$scope.$apply();
}
})
Angular will not be aware that it should digest again if on custom async methods created by your code.
A way to get around that is to use $scope.$apply(). However $scope is not exposed to your service. So on your angular controller, you can wrap the listened function of your event into an $apply();
$scope.$apply(function () {
//your code here.
});
http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
I'm struggling to figure out how to do this. Hope anyone can help :)
I have multiple controllers in my Angular app. Like titleCtrl and SettingsCtrl
I have a service which holds a variable like this:
var myVar = {title: 'test', settings: {color: 'black', font: 'verdana'}};
I'm making a $http.get request to update the "myVar" variable from the server.
The question is, how do I update the $scope.title in titleCtrl and $scope.settings in SettingsCtrl AFTER the http request has finished? I know how to do it in a single controller, but how do I update the $scopes in multiple controllers?
Use a watch on that variable in the service. When its updated, then update your values in controller scope. Here's an example:
Inside your controller, you can watch a var myVar on YourService and when it changes, update a variable called myVarInController with the value it changed to.
$scope.$watch(
// This function returns the value being watched.
function() {
return YourService.myVar;
},
// This is the change listener, called when the value returned above changes
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
$scope.myVarInController = newValue;
}
}
);
Just in you service create a object when you get data from you server copy it to that object, so all your controllers can reference to that object.
Please see here http://plnkr.co/edit/j25GJLTHlzTEVS8HNqcA?p=preview
JS:
var app = angular.module('plunker', []);
app.service('dataSer', function($http) {
var obj = {};
getData = function() {
$http.get("test.json").then(function(response) {
angular.copy(response.data, obj);
});
}
return {
obj: obj,
getData: getData
};
});
app.controller('MainCtrl', function($scope, dataSer) {
$scope.data = dataSer;
$scope.get = function() {
$scope.data.getData()
}
});
app.controller('SecondCtrl', function($scope, dataSer) {
$scope.data = dataSer;
});
HTML:
<div ng-controller="MainCtrl">
<button ng-click="get()">get data</button>
<p>Fist Controller:
<br/>{{ data.obj.title}}</p>
</div>
<div ng-controller="SecondCtrl">
<p>Second Controller:
<br/>{{data.obj.settings}}</p>
</div>
Use both factory and service to pass value to two controllers. This is the only way to pass value
angular.module('mulipleCtrlApp', [])
.service('shareService', function () {
return {
title: 'test',
settings: {
color: 'black',
font: 'verdana'
}
};
})
.controller('titleCtrl', function ($scope, shareService) {
$scope.myVar = shareService;
$scope.testchange = function () {
$scope.myVar.title = 'Completed test';
};
})
.controller('settingCtrl', function ($scope, shareService) {
$scope.myVar = shareService;
});
Egghead Link
Jsfiddler Link example
Make your service return promise object.
Then in controller you can define a success call back to fetch title in one and settings in
another controller once the promise is resolved.
Code to use promises
In your service class:
var factory = {};
var factory.fetchData = function () {
return $http({method: 'GET', url: '/someUrl'});
}
return factory;
In controller 1:
$scope.getData = function(){
factory.fetchData().success(response){
$scope.title = response.title;
}
}
Similarly you can update controller 2, to set settings data.
I've found a better and easier maintainable solution in my opinion. Simply do the following to achieve to-way data-binding between one (or more) controller(s) with a service:
Lets assume you fetch (i.e. $http) and store data in your service (serviceName) in the variable serviceData.
In your controller reference the service like this to achieve to-way data-binding:
$scope.data = serviceName
In your view/html bind to the data properties like this:
<input ng-model="data.serviceData.title">
Thats it! :) When your serviceData variable updates the view/scope does as well. This will work with multiple controllers.
My basic premise is I want to call back to the server to get the logged in user in case someone comes to the site and is still logged in. On the page I want to call this method. Since I am passing the user service to all my controllers I don't know which controller will be in use since I won't know what page they're landing on.
I have the following User Service
app.factory('userService', function ($window) {
var root = {};
root.get_current_user = function(http){
var config = {
params: {}
};
http.post("/api/user/show", null, config)
.success(function(data, status, headers, config) {
if(data.success == true) {
user = data.user;
show_authenticated();
}
});
};
return root;
});
Here is an empty controller I'm trying to inject the service into
app.controller('myResourcesController', function($scope, $http, userService) {
});
So on the top of my index file I want to have something along the lines of
controller.get_current_user();
This will be called from all the pages though so I'm not sure the syntax here. All examples I found related to calling a specific controller, and usually from within another controller. Perhaps this needs to go into my angularjs somewhere and not simply within a script tag on my index page.
You could run factory initialization in run method of your angular application.
https://docs.angularjs.org/guide/module#module-loading-dependencies
E.g.
app.run(['userService', function(userService) {
userService.get_current_user();
}]);
And userService factory should store authenticated user object internaly.
...
if (data.success == true) {
root.user = data.user;
}
...
Then you will be able to use your factory in any controller
app.controller('myController', ['userService', function(userService) {
//alert(userService.user);
}]);
You need to inject $http through the factory constructor function, for firsts
app.factory('userService', function ($window, $http) {
var root = {};
root.get_current_user = function(){
var config = {
params: {}
};
$http.post("/api/user/show", null, config)
.success(function(data, status, headers, config) {
if(data.success == true) {
user = data.user;
show_authenticated();
}
});
};
return root;
});
in your controller you can say
$scope.get_current_user = UserService.get_current_user();
ng attributes in your html if needed. besides this, i am not sure what you need.
I have an AngularJS service that loads data from localStorage while "initializing" (i.e. in the factory function), like this:
module.service('myService', function ($localStorage) {
var data = $localStorage.data;
if (!isValid(data)) // isValid omitted on purpose, not relevant.
data = undefined;
return {
getData: function() {
return data;
}
setData: function(value) {
if (isValid(value))
data = value;
}
};
}
In my tests, I'd like to check that data is actually loaded from localStorage if the value is present there and valid; this is not about testing isValid, but the service initialization that uses it and $localStorage.
I'd like to be able to call the myService factory inside my test. I'm getting an initialized instance of it in the beforeEach hook since I need to test methods of myService as well. I think I need to have a different instance created for my specific initialization test, but since services are singletons in AngularJS, I'm not sure whether this can be done.
describe('myService', function() {
myService = $localStorage = null;
beforeEach(module('app'));
beforeEach(inject(function($injector) {
myService = $injector.get('myService');
$localStorage = $injector.get('$localStorage');
});
it('should look for stuff in localStorage on creation', function () {
$localStorage.data = 'my data';
// I'd like to call service factory here!!
myService.getData().should.equal('my data');
});
});
Can this be achieved? Does my code have a difficult-to-test structure, or am I disrespecting the "Angular way" and this should be done differently?
Try this:
describe('myService', function() {
myService = $localStorage = null;
beforeEach(module('app'));
beforeEach(inject(function($injector) {
$localStorage = $injector.get('$localStorage');
$localStorage.data = 'my data';
myService = $injector.get('myService');
});
it('should look for stuff in localStorage on creation', function () {
myService.getData().should.equal('my data');
});
});
I'm using Angular 1.08, hence I need to use responseInterceptors.
First the code.
Interpreter:
app.factory('errorInterceptor', ['$q', 'NotificationService', function ($q, NotificationService) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
// do something on error
alert('whoops.. error');
NotificationService.setError("Error occured!");
return $q.reject(response);
});
}
}]);
app.config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('errorInterceptor');
});
NotificationService:
app.service("NotificationService", function () {
var error = '';
this.setError = function (value) {
error = value;
}
this.getError = function () {
return error;
}
this.hasError = function () {
return error.length > 0;
}
});
Directive error-box:
app.directive("errorBox", function (NotificationService) {
return {
restrict: 'E',
replace: true,
template: '<div data-ng-show="hasError">{{ errorMessage }}</div>',
link: function (scope) {
scope.$watch(NotificationService.getError, function (newVal, oldVal) {
if (newVal != oldVal) {
scope.errorMessage = newVal;
scope.hasError = NotificationService.hasError();
}
});
}
}
});
The problem: When I use <error-box> at multiple places, all these boxes will display the error message. This is not my intent. I'd like to show only the error-box where the exception occurs.
For example, I have a directive which shows a list of transactions. When fetching the transactions fails, I want to show the error-box which is declared in that part.
I also have a directive where I can edit a customer. This directive also contains the error-box tag.
What happens is when saving a customer fails, both error-boxes are displayed, however, I only want the error-box of the customer to be displayed.
Does someone has an idea to implement this?
Angular services are Singleton objects, as described in the angular docs here. This means that Angular only creates one single "global" instance of a service and uses that same instance whenever the given service is requested. That means that Angular only ever creates one single instance of your NotificationService services and then will supply that one instance to every instance of your errorBox directive. So if one directive updates the NotificationService's error value, then all of the <error-box directives will get that value.
So, you're going to have to either create multiple notification services for each type of error (ie TransactionNotification and CustomerNotification, etc) or add different methods to your main NotificationService that would allow you to set only specific alerts (such as NotificationService.setCustomerError() or NotificationService.setTransactionError()).
None of those options are particularly user-friendly nor clean, but I believe (given the way you've set up your service), that's the only way to do it.
UPDATE: After thinking about it, I might suggest just dropping your whole NotificationService class and just using $scope events to notify your <error-box> elements when an error occurs:
In your 'errorInterceptor':
app.factory('errorInterceptor', ['$q', '$rootScope', function ($q, $rootScope) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
// do something on error
alert('whoops.. error');
var errorType = ...; // do something to determine the type of error
switch(errorType){
case 'TransactionError':
$rootScope.$emit('transaction-error', 'An error occurred!');
break;
case 'CustomerError':
$rootScope.$emit('customer-error', 'An error occurred!');
break;
...
}
return $q.reject(response);
});
}
}]);
And then in your errorBox directive:
link: function (scope, element, attrs) {
var typeOfError = attrs.errorType;
scope.$on(typeOfError, function (newVal, oldVal) {
if (newVal != oldVal) {
scope.errorMessage = newVal;
}
});
}
And then in your view:
<error-box error-type="transaction-error"></error-box>
<error-box error-type="customer-error"></error-box>
Does that make sense?