AngularJS proper way of refreshing data in Controller - angularjs

I have a testing "hello" view showing "Hello {{username}}!" or "Hello anonymous!".
This view has its own controller and is accesible via url (configure by ui.router).
Then I have a UserModel with methods setUsername(newUsername) and getUsername().
There is also logging view with a controller that uses setUsername() method on logging in success and then navigates to "hello" view.
The code looks like this:
HelloController:
anguler.module('hello', ...
.config(function($stateProvider){
$stateProvider
.state('hello', {
url: '/hello',
views: {
'main#': {
controller: 'HelloController as helloController',
templateUrl: 'app/hello/hello-tmpl.html'
}
},
});
})
.controller('HelloController', function (UserModel) {
var helloController = this;
helloController.username = UserModel.getUsername();
})
There is also a "log out" button in the top bar. So in order to show the changes in "hello" view I added a list of function that UserModel would call when user state changes:
.service('UserModel', function() {
var model = this;
var username = '';
var onChangesFunctions = [];
function onChange() {
onChangesFunctions.forEach(function(f) {
f();
});
}
model.onChange = function(f) {
onChangesFunctions.push(f);
};
model.setUsername = function(newUsername) {
username = newUsername;
onChange();
};
model.clearUserData = function() {
username = '';
onChange();
};
and added a code in HelloController to add a listener to the UserModel.onChangesFunctions.
The problem with that approach is that HelloController is initialized many times (everytime that user navigates to it) and every time it is registering new function as the listener.
Is there any better way to refresh user data?

The problem of your approach is memory leaks. As you said when your controller is destroyed and the new one is created your function will still persist in the service and the controller which should have been killed is still most likely in the memory because of the function.
I don't clearly understand what your goal is; however what you can do is destroying the functions when the controller is destroyed:
.controller('HelloController', function (UserModel, $scope) {
var helloController = this;
helloController.username = UserModel.getUsername();
$scope.$on('$destroy', function() {
// either destroy all functions you add to the service queue
// or
// simply call the controller specific logic here, this will be called when your controller is destroyed
});
});
Another approach is listening on '$stateChangeStart' / '$stateChangeSuccess' event.
Regardless of the way you choose I would highly recommend to avoid binding services to the controller instance specific logic. This is a way to hell

Related

How to store controller functions in a service and call them in AngularJS

I need to execute functions of some controllers when my application ends (e.g. when closing the navigator tab) so I've thought in a service to manage the list of those functions and call them when needed. These functions changes depending on the controllers I have opened.
Here's some code
Controller 1
angular.module('myApp').component('myComponent', {
controller: function ($scope) {
var mc = this;
mc.saveData = function(objectToSave){
...
};
}
});
Controller 2
angular.module('myApp').component('anotherComponent', {
controller: function ($scope) {
var ac = this;
ac.printData = function(objects, priority){
...
};
}
});
How to store those functions (saveData & printData) considering they have different parameters, so when I need it, I can call them (myComponent.saveData & anotherComponent.printData).
The above code is not general controller but the angular1.5+ component with its own controller scope. So the methods saveData and printData can only be accessed in respective component HTML template.
So to utilise the above method anywhere in application, they should be part of some service\factory and that needs to be injected wherever you may required.
You can create service like :
angular.module('FTWApp').service('someService', function() {
this.saveData = function (objectToSave) {
// saveData method code
};
this.printData = function (objects, priority) {
// printData method code
};
});
and inject it wherever you need, like in your component:
controller: function(someService) {
// define method parameter data
someService.saveData(objectToSave);
someService.printData (objects, priority);
}
I managed to make this, creating a service for managing the methods that will be fired.
angular.module('FTWApp').service('myService',function(){
var ac = this;
ac.addMethodForOnClose = addMethodForOnClose;
ac.arrMethods = [];
function addMethodForOnClose(idModule, method){
ac.arrMethods[idModule] = {
id: idModule,
method: method
}
};
function executeMethodsOnClose(){
for(object in ac.arrayMethods){
ac.arrMethods[object].method();
}
});
Then in the controllers, just add the method needed to that array:
myService.addMethodForOnClose(id, vm.methodToLaunchOnClose);
Afterwards, capture the $window.onunload and run myService.executeMethodsOnClose()

Refreshing data from service in controller

I have two separate controllers: AuthController and NavController.
AuthController is responsible for running registration/login form, and NavController is responsible for displaying navbar where I want to show current username if one is logged in. Finally, I have service "auth" that handles all that register/login stuff
auth service have this function:
auth.currentUser = function() {
if (auth.isLoggedIn()) {
var token = auth.getToken();
var payload = this.decodeUsername(token);
return payload.username;
}
};
and NavController looks like this:
app.controller('NavController', ['$scope', 'auth',
function($scope, auth) {
$scope.isLoggedIn = auth.isLoggedIn;
$scope.logOut = auth.logOut;
$scope.currentUser = auth.currentUser();
}
]);
So i can display current username, but if user just logged in NavController "doesn't know" that anything changed. I've tried to use event, but this two controllers doesn't have parent-child relation. Should I wrap them in one parent controller and do "AuthController-emit->SuperController-broadcast->NavController" or there is better way to communicate there two controllers?
You have two options:
Use $rootScope.broadcast (example here) and this will send an event from the top down to every controller. This works best if multiple things will want to see this message.
Or if you only ever want the navbar to be notified you could use a callback.
In your auth service have a function that gets called on state change such as
authApi.stateChange = function() {}
In your nav bar controller you then set authApi.stateChange = $scope.authUpdated; and then your authUpdated function will be notified from the service when authApi.stateChange() is called
When there is something to be shared between controllers, a Service would the best way to achieve the result. As its singleton, there will be only one instance, and your controllers - 'Auth' - can set/update value, 'Nav' can bind to the changes.
If there is some fetching involved use promise. And if the data is going to be fetched only once then you are better off by just using promise.
auth.currentUser = function() {
var defer = $q.defer();
if (auth.isLoggedIn()) {
var token = //some asynoperation//;
var payload = this.decodeUsername(token);
defer.resolve(payload.username);
}else{
defer.reject("Not logged in");
}
return defer.promise;
};
(//do remember to inject $q)

How to preserve ui-router parameters on browser refresh

am using angular ui-router to manage states of my SPA.
I have this route:
.state('index.administration.security.roleEdit', {
url: '/roleEdit',
templateUrl: 'app/administration/security/role/roleEdit.html',
controller: 'roleEditCtrl',
controllerAs: 'roleEditCtrl',
params: { data: null },
resolve: {
role: function ($stateParams) {
return angular.fromJson($stateParams.data);
},
modules: function (securityService) {
return securityService.getAllModules();
}
}
})
Also, I'm passing 'data' parameter as json object to the state.
Now when i first load this state, everything is fine.
But when i do browser refresh (F5 key) the $stateParams.data is null in the resolve function of the state.
How can I solve this?
I see these possible solutions:
1. persist somehow parameters
2. override browser refresh (don't know how) to stop the browser refreshing the app.
3. on refresh goto other sibling state.
Please help
UPDATE
Ok, I set data like this:
vm.editRole = function(roleId){
var role = dataService.getRoleById(roleId).then(function(result){
$state.go('roleEdit', {data:angular.toJson(result)});
});
}
UPDATE 2
The roleEdit Controller looks like this:
(function(){
angular.module('app.administration').controller('roleEdit',
['role','modules', '$scope', 'securityService', '$state', roleEditCtrl]);
function roleEditCtrl('role', 'modules',$scope, securityService, $state){
var vm = this;
vm.roles = roles;
vm.originalRole = angular.copy(role);
vm.modules=modules;
vm.saveChanges = _saveChanges;
vm.cancel = _cancel;
return vm;
function _saveChanges(){
securityService.UpdateRole(vm.role).then(function(result){
$staste.go('^.roles');
}
}
function _cancel(){
vm.role = angular.copy(vm.originalRole);
$sscope.roleEditForm.$setPristine();
}
}
})();
Had the same problem, leaving this here in case someone else needs it. Solved it by using localStorage.
I've set this as a part of app's run method
$rootScope.$on('$stateChangeSuccess', function (event, toState) {
localStorage.setItem('__stateName', toState.name);
});
Now, depending on your app's structure you might want to consider putting this someplace else, but in my case I had parent AppController surrounding all child controllers, so this part of the code went there.
var previousState = localStorage.getItem('__stateName');
previousState && $state.go(previousState) || $state.go(<SOME_OTHER_ROUTE>);
Basically, when user hits browser refresh, AppController get initialized from the start (before child controllers), firing the state transition immediately if there was one. Otherwise, you'd go to some other state of yours.
I would stay away from option 2. You don't want to mess up a browser's expected behavior.
Maybe you could refactor your data into your resolve object ala:
resolve: {
role: function ($stateParams) {
return angular.fromJson($stateParams.data);
},
modules: function (securityService) {
return securityService.getAllModules();
},
data: function (dataService) {
return dataService.retrieveStoredData();
}
Where your dataService would be a service you use to store that data if it really is that important (through cookies or localstorage).
There is no reasonable way to expect that field to be populated on browser refresh if you previously executed javascript to pass stateful values to it.

How to pass dynamic data from one module controller to another module controller

I'm new to AngularJS, I want to pass dynamic value (username) from one controller in one module to another controller in another module. Routing and other things are working fine.
This is my code
loginModule.js
(function() {
var app = angular.module("waleteros", ["ngRoute","ui.bootstrap","ngCookies"]);
app.config(function($routeProvider) {
$routeProvider
.when("/",{
templateUrl:"views/login.html",
controller:"LoginCtrl"
})
}
})
app.js
(function() {
var app = angular.module("waleterosAdmin", ["ngRoute","ngGrid","ui.bootstrap","ngCookies"]);
app.config(function($routeProvider) {
$routeProvider
.when("/home",{
templateUrl:"homepage.html",
controller:"HomeCtrl"
})
}
})
loginCtrl.js
(function(){
var app = angular.module("waleteros");
var LoginCtrl= function($scope,$location)
{
$scope.signIn=function(email,pin)
{
//Some authentication code...
//Here i want to pass the username to homectrl.js
window.location.href="views/home.html"
}
}
app.controller("LoginCtrl", LoginCtrl);
})
HomeCtrl.js
(function(){
var app = angular.module("waleterosAdmin");
var HomeCtrl=function($scope)
{
//Here i want to get the username
}
app.controller("HomeCtrl", HomeCtrl);
})
you can share service between modules ,and thus pass value between modules,
please have a look here Share a single service between multiple angular.js apps ,and here sharing between modules with AngularJS?
You would use a service to persist the data, and then inject the service into your controllers:
app.service("SharedProperties", function () {
var _userName = null;
return {
getUser: function () {
return _userName
},
setUser: function(user) {
_userName = user;
}
}
});
Now inject SharedProperties and use the getter/setter
var LoginCtrl= function($scope,$location, SharedProperties)
{
$scope.signIn=function(email,pin)
{
//Some authentication code...
SharedProperties.setUser(user);
//Here i want to pass the username to homectrl.js
window.location.href="views/home.html"
}
}
var app = angular.module("waleterosAdmin");
var HomeCtrl=function($scope, SharedProperties)
{
//Here i want to get the username
var user = SharedProperties.getUser();
}
One word of warning about services is that they persist for the lifetime of the application, i.e. they are only instantiated once. I have run into scenarios, especially once routing is implemented, where I want to wipe the data off of the service to save space and replace it with new data (you don't want to keep adding to this service every time you look at a different view). To do this, you could either write a "wipe" method that you call to clean the service on the route changes, or stick the data into a directive (on its controller), put the controllers into their own directives, and have these require the first directive, so that the data is accesible from the controller's with the added benefit of being wiped once the DOM element is declared on is destroy (on a view change, for instance).

Change controller variable from within a service

In the "ok"-function of a modal, I'm trying to update a variable from the scope that opened the modal. This
$scope.modalOptions.assets.length = 0;
perfectly works: the variable "assets" in the "parent" scope immediatly changes and, while the modal is still open, the data represantation of "assets" is updated and emptied in the main page.
What bugs me is that changing above to
$scope.modalOptions.assets = $scope.modalOptions.assetFactory.query();
has no effect at all. I can verify that the API Controller is called and that it returns new data which should, in effect, change the representation of "assets" as well.
The variable itself is defined in the controller like this:
$scope.assets = bondFactory.query();
And I pass it in to the Modal-Service like this:
assets: $scope.assets
I'd be thankful for tips and ideas..
EDIT
How the Modal is called:
$scope.postModal = function () {
//Pass view
var customModalDefaults = {
templateUrl: 'scripts/app/views/postBond.html'
}
//Pass data
var customModalOptions = {
asset: angular.copy($scope.asset),
assets: $scope.assets,
currencies: globalVariables.currencies,
assetFactory: bondFactory,
validationErrors: [],
action: 'POST'
};
//Show & Callback
ModalAssetService.showModal(customModalDefaults, customModalOptions).then(function (result) {
// fill me with usefull content
});
};
The Modal Service itself:
app.service('ModalAssetService', ['$modal',
function ($modal) {
var modalDefaults = {};
var modalOptions = {};
this.showModal = function (customModalDefaults, customModalOptions) {
//Create temp objects to work with since we're in a singleton service
var tempModalDefaults = {};
var tempModalOptions = {};
//Map angular-ui modal custom defaults to modal defaults defined in service
angular.extend(tempModalDefaults, modalDefaults, customModalDefaults);
//Map modal.html $scope custom properties to defaults defined in service
angular.extend(tempModalOptions, modalOptions, customModalOptions);
//Create controller
tempModalDefaults.controller = function ($scope, $modalInstance, $resource, errorService) {
$scope.modalOptions = tempModalOptions;
// exit modal with "ok"
$scope.modalOptions.ok = function (result) {
//POST
if ($scope.modalOptions.action == 'POST') {
// try and post asset
$scope.modalOptions.assetFactory.save($scope.modalOptions.asset, function () {
// success
$scope.modalOptions.assetFactory.query({}, function (data) {
$scope.modalOptions.assets = data;
// $modalInstance.close();
});
}, function (error) {
// error
$scope.modalOptions.validationErrors = errorService.fn(error);
});
};
//PUT
if ($scope.modalOptions.action == 'PUT') {
//TODO
alert("put");
};
};
// exit modal with "cancel"
$scope.modalOptions.close = function (result) {
$modalInstance.dismiss('cancel');
};
}
return $modal.open(tempModalDefaults).result;
};
}
]);
EDIT 2: the bondFactory
app.factory('bondFactory', ['$resource', function ($resource) {
return $resource('../api/bond/:id', null, {
'update': { method: 'PUT' }
})
}]);
SOLUTION
I still did not figure out exactly how to fix the problem of the scope not being notified about a change if the change results from an assignment of a $resource GET to a variable from said scope.
Anyways, for my specific case, I found a better solution: when clicking "OK" in the modal, the new asset is being sent to the API. If errors happen there (validation failed etc.), the Modal will stay open and some notifications will inform the user. If the new asset was posted successfully, then a new GET is sent to the API and only the last new asset is being added to the existing array of asset-elements. This means no "flashing", just one more row is added to the list of assets.
It's not a good form of programming to assume that the last element from the GET-Request is definitly the new asset and there is of course some overhead in retrieving the complete list of all assets when just the very last of these would suffice, but I guess it works and I'll just add some sort to the API to make sure it always is in right order. Code:
$scope.modalOptions.assetFactory.save($scope.modalOptions.asset, function () {
//success
$scope.modalOptions.assetFactory.query({}, function (result) {
$scope.modalOptions.assets.push(result[result.length - 1]);
$modalInstance.close();
});
}, function (error) {
//error
$scope.modalOptions.validationErrors = errorService.fn(error);
});
Your factory is performing an async operation, so you need to use callbacks to access the data and have the view update:
$scope.modalOptions.assetFactory.query({}, function(data) {
$scope.modalOptions.assets = data;
});
Without knowing how you are calling the modal, or if you are using a hand-rolled one or the one from UI-Bootstrap, I can only guess at how things are working. Having said that, if you are not using the modal from UI-Bootstrap you really should be. It uses promises and has the ability to return just about anything from the modal.
$scope.myModalPromise = $modal.open(modalConfig);
$scope.myModalPromise.result.then(function(data){
//things to do upon closing the modal with $close
//data is any value or object that you want to pass back
}, function(data){
//things to do upon closing the modal with $dismiss
//data is any value or object that you want to pass back
});
The setup lets your modal perform asynch operations and only return the value when they have completed. The beauty of this is that they are returned to the calling controller cleanly and in an Angular way.

Resources