I have an AuthService, which logs in a user, it returns back a user json object. What I want to do is set that object and have all the changes reflected across the application (logged in/logged out state) without having to refresh the page.
How would I accomplish this with AngularJS?
The easiest way to accomplish this is by using a service. For example:
app.factory( 'AuthService', function() {
var currentUser;
return {
login: function() { ... },
logout: function() { ... },
isLoggedIn: function() { ... },
currentUser: function() { return currentUser; }
...
};
});
You can then reference this in any of your controllers. The following code watches for changes in a value from the service (by calling the function specified) and then syncs the changed values to the scope.
app.controller( 'MainCtrl', function( $scope, AuthService ) {
$scope.$watch( AuthService.isLoggedIn, function ( isLoggedIn ) {
$scope.isLoggedIn = isLoggedIn;
$scope.currentUser = AuthService.currentUser();
});
});
And then, of course, you can use that information however you see fit; e.g. in directives, in templates, etc. You can repeat this (customized to what you need to do) in your menu controllers, etc. It will all be updated automatically when you change the state on the service.
Anything more specific depends on your implementation.
I would amend the good response of Josh by adding that, as an AuthService is typically of interest of anyone (say, anyone but the login view should disappear if nobody is logged), maybe a simpler alternative would be to notify interested parties using $rootScope.$broadcast('loginStatusChanged', isLoggedIn); (1) (2), while interested parties (such as controllers) would listen using $scope.$on('loginStatusChanged', function (event, isLoggedIn) { $scope.isLoggedIn = isLoggedIn; }.
(1) $rootScope being injected as an argument of the service
(2) Note that, in the likely case of a asynchronous login operation, you'll want to notify Angular that the broadcast will change things, by including it in a $rootScope.$apply() function.
Now, speaking of keeping the user context in every/many controllers, you might not be happy listening for login changes in everyone of them, and might prefer to listen only in a topmost login controller, then adding other login-aware controllers as children/embedded controllers of this one. This way, the children controller will be able to see the inherited parent $scope properties such as your user context.
Related
I have a main controller for my profile edit page, from where users can add their education, in a modal window. The modal has it's own controller. The user can add as many education items they want and they can modify the existing ones.
Now for my issue. I want the modal controller to take care of both adding a new item and updating an existing one. The difference is that on update, the controller should receive a fully populated education object, which it should update. I don't know how to pass this object from the main controller to the modal controller. I've seen some ways of passing data between controllers via services, but that seems too much of a hassle, and it doesn't make sense to me to use a service in this particular case.
My main controller creates modals like so:
vm.openEducation = function(){
$modal.open({
templateUrl: "some.html",
controller: "SomeController",
controllerAs: "vm",
size: 'lg'
}).result.then(function (education) {
vm.educations.push(education);
});
}
And the child controller (which currently only supports insert, hence the empty init of vm.education):
function SomeController($scope){
var vm = this;
vm.education = {}; // or get from parent controller
vm.save = function () {
$scope.$close(vm.education);
};
vm.close = function () {
$scope.$dismiss();
}
return vm;
}
You can use $rootScope.$broadcast to send data from the modal and then $scope.$on to receive that data anywhere else.
For example:
// in your modal
$rootScope.$broadcast('education:updated', yourDataObj)
// in your controller
$scope.$on('education:updated', function(e, data) {
console.log(data)
})
Note that the $broadcast will be picked up by anything that's listening for it, so if you have multiple controller instances you will get multiple console.log messages
So here's my general problem, I have a factory which needs to make an ajax call to get its value. I want to inject this factory, but only after the ajax call has been resolved, meaning, when it's injected, I want the value to already be there. Is this possible? Let me give you some context so you can maybe give me a better option.
I have a UserFactory.
var app = angular.module('app',[]);
app.factory('UserFactory',[
function(){
//ajax call to get the user object
//dont allow injection until user has arrived
}
]);
var home = angular.module('home',[]);
home.controller('UserInfoController',[
'UserFactory',//I don't want this resolved until the ajax call has completed
function(User){
//bind user data to the view
}
]);
Is this possible? Or am I going about this wrong? I want to be able to access the user's information without having to use promises every time, just in case it's not already there.
What you can do is set up your factory so that it remembers the user data.
Something like this
app.factory('userFactory', function ($http){
return {
getUser: function(){
return $http({
method: 'GET',
url: ''
});
},
currentUser: {}
};
});
So then in your controller, check to see if there is anything in currentUser. If not make the getUser call and store it into currentUser. Since the factory is a singleton the userdata will always exists inside this factory and you can inject it anywhere you need it.
Here's an example: http://jsfiddle.net/4m6gmsw2/
If you still need the user data to be there before injecting it you can take a look at using resolve: https://thinkster.io/egghead/resolve/
In the state router, there is resolve property for each state.
.state({
name: 'myState',
resolve: {
user: function(myService) {
return myService.asyncGet();
}
}
Then in your controller:
.controller('myController', function(user) {
$scope.myData = user.data; //no messy promises
});
EDIT: As asked, I'll explain a bit more efficiently !
I've been sitting in front of an annoying problem recently, which is that whenever I update a value inside a directive, the controllers I'm not currently "in" are the only ones to be updated properly.
Scenario example: Profile page is made of two controllers. Navbar_controller which is just currently displaying the user name :
<div ng-if="Auth.isAuthenticated">Hello, {{Auth.getCurrentUser().name}}</div>
The second controller , Profile_controller is here to update user values. This is a simple function in the angular first controller, which updates CurrentUser:
$scope.updateUser = function (type, form) {
if (!$scope.modif)
return ;
$http.put('/api/users/' + Auth.getCurrentUser()._id + '/update', {type:type, modif:$scope.modif})
.success(function (data, status) {
$scope.user = Auth.setNewUser(data);
})
.error(function () {
console.log("error");
});
};
When I update, for example, the name. I can see that the database has been modified properly. And indeed, navbar_controller got the update because a new name is printed in the div. However, Profile_controller doesn't get the update: the name printed in the profile page didn't change.
Here are the two basic functions in Auth.service.js :
getCurrentUser: function() {
return currentUser;
},
// 'user' is the data retrieved in http put request dot success
setNewUser: function(user) {
currentUser = user;
$rootScope.$broadcast(); // Navbar_controller is updated with or without this line
return currentUser;
}
Anyway, if I look at the navbar and its controller, which is calling Auth.getCurrentUser() method, the user values are instantly modified. I'e been using an ugly method consisting in modifying the controller values manually or by refreshing the page... But this isn't the way to go, right ?
There must be something with "$rootScope.$broadcast();", but I'm really new to Angular and other questions on stackoverflow are too specific to help me understand properly.
Thank you !
Your question was a little difficult to understand, but I think the problem is that you are reference a changing object in your various controllers. Here is an example to explain:
Service:
var myObject = { ... };
return {
getObject() { return myObject; }
setObject(obj) { myObject = obj; }
};
Controller 1:
$scope.myObjA = Service.getObject();
Controller 2:
$scope.myObjB = Service.getObject();
Now on initialisation both controllers will be referencing the same object, so if you changed a property inside either controller (eg. $scope.myObjB.name = 'bob';), then the other controller would also see the name.
However if you changed the object itself in a controller (eg. Service.setObject(newObj);), then the controller will be referencing the new object, while the other controller will still be referencing the old one.
You can fix this by wrapping your service object in a container:
var cont = {
user: ...
};
function getContainer() { return cont; }
function setNewUser(user) { cont.user = user; }
Then inside your controllers, get the container (not the user):
$scope.cont = Service.getContainer();
And inside your html:
<div>{{cont.user.name}}</div>
Now when you update the user, all attached controllers will be updated.
Well I'd try to change and store the user information in $rootScope, for your scenario could be a good fit.
getCurrentUser: function() {
$rootScope.currentUser===undefined ? 'no User': $rootScope.currentUser;
},
setNewUser: function(user) {
$rootScope.currentUser = user;
//$rootScope.$broadcast(); no need to broadcast
return getCurrentUser();
}
in that way currentUser will be updated in different scopes as needed!
I'll quote AnuglarJs FAQ regarding to $rootscope:
$rootScope exists, but it can be used for evil
Occasionally there are pieces of data that you want to make global to
the whole app. For these, you can inject $rootScope and set values on
it like any other scope. Since the scopes inherit from the root scope,
these values will be available to the expressions attached to
directives like ng-show just like values on your local $scope.
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
I have a service defined as
app.service('Auth', ['$http',function ($http) {
this.isLoggedIn = false;
this.user = null;
}]
and a controller is using it as
app.controller('AuthenticationController', ['$rootScope','$scope', '$http', '$location', 'Auth',function($rootScope,$scope, $http, $location, Auth){
$scope.login = function(){
Auth.isLoggedIn = true;
Auth.user = {
name: "Shahzad Fateh Ali",
id: 1
};
$location.path('/users');
}}]);
My DOM uses it as
<header ng-show="Auth.isLoggedIn">...</header>
and
<mainmenu ng-show="Auth.isLoggedIn"></mainmenu>
Here mainmenu is a Directive.
Now, I want to observe Auth.isLoggedIn and update the DOM based on its value.
Regards,
Shahzad Fateh Ali
You can create a $watch on a function, it doesn't have to be a string name to a property on the scope. So something like this:
$scope.$watch(function() { return Auth.isLoggedIn; }, function(value, oldValue) {
// Do something when it changes
});
But I would encourage you to use events here. So your Auth service would have a method which you call to say that the user has logged in. The Auth service should get $rootScope injected, and then $rootScope.$broadcast() an event that says that the user has authenticated. Other parts of your application can then listen to that event with $scope.$on() and take action when the user logged in.
If you're having problems with isLoggedIn not getting updated in your service, try making it an object instead of just a boolean. I have had similar issues with two-way binding a boolean variable in my service and the solution was to make it part of an object. As an example, you could create an Authentication object in your service with attributes of isLoggedIn and authenticatedUser, then in your service return this object. When you set isLoggedIn and the user you should see it updated in your service as well.
I wonder if I can call controller method from service.
I know that Service is singleton and I can't inject $scope to the service.
In my case I manage Google Maps in service and want to open modal Dialog when user right clicks on Polygon.
As I know, to open/create new instance of dialog, somehow Service must notify controller to do that.
This is a template with controller + method and service: Template
var myApp = angular.module('myApp', []);
function MyCtrl($scope, gridService, $timeout) {
// how to call "foo" method from service?
$scope.foo = function(){
alert('called from service');
};
}
myApp.service('gridService', ['$timeout', function ( $timeout) {
var grid = {
fetching: false,
pristine: true,
pageType: 'Edit'
}
return {
gridSetup: function () {
return grid;
},
setGridSetup: function (newGrid) {
}
}
}]);
Thanks,
The answer is simple: you don't.
The service exists to manipulate data, nothing else. And it really shouldn't care "why" it's doing what it's doing. You ask the service to do something and wait for a response.
Personally I prefer using promises to resolve async operations (i.e. to notify the controller about a state change) since it's heavily supported by many angular services like $http.
But feel free to use callbacks of you wish.
Usually you do not need to call controller from the service - in general the single service could be used by any controller so service shall know nothing about them. In most cases controller calls to the service in react to some user's action and then you need to update view from controller somehow when service did its work (get response from server, etc.). I see following general ways how to do it.
1. Use callbacks.
//controller
$scope.onButtonClick = function() {
$scope.label = "wait, loading...";
function onSuccess(result) {
$scope.label = "done! result is " + result;
}
myService.doSomeWork(param1, param2, onSuccess);
}
//service
doSomeWork: function(param1, param2, onSuccess) {
$.get({...}, onSuccess);
}
So you provide a callback for each action.
2. Subscribe on events
You may use jQuery for events subscribing/triggering
//controller
$(myService).on('update', function() {
$scope.lastUpdateTime = new Date();
});
$scope.onButtonClick = function() {
myService.doUpdate();
}
//service
doUpdate: function() {
$.get({...}, function onOk() {
$(this).trigger('update');
});
}
3. Use promises
A lot of built-in angular services return promise objects, you may use them too:
//controller
myService.doSomething(param1).then(function(result) {
$scope.result = result;
});
//service
doSomething: function(param1) {
return $http.get({...});
}
4. Share some data
An example is $resource service - for example when you call query method it returns empty array-like object that could be safely put to scope and then fills it with values when http request is done.
//controller
$scope.options = myService.options;
$scope.onClick = function() { myService.update() }
//service
options: [],
update: function() {
var self = this;
$http.get({...}).success(function(res) {
self.options.splice(0, self.options.length); //to keep same array
self.options.push.apply(self.options, res.data.options);
});
}
In all these cases services and controllers are separated, services could be used with any controller and you may easily write unit-tests on services that will not break if you change your controller/view part somehow.
A possible solution would be to have a dialog service which you can inject into the grid service. So when the user right clicks on the polygon the handler would call open on the dialog service.
Take a look at the modal service on angular ui as an example.