I'm using AngularJS 1.0 and need to maintain state (user-entered data) on a page even when the user navigates (angular routing) to another page and then comes back to the original page. Here is what it looks like:
From what I've read online, the solution is to use a service/factory to store the data. Here is a snippet of my implementation (notice the comments; they address my concern):
function cassetteController($scope, $rootScope, $http, $timeout, cassetteRepository) {
// Need to get state from repository in case the user is coming back to this page.
// This means every time we add some variable to our controller, we have to remember to include it here.
$scope.userEnteredSubId = cassetteRepository.userEnteredSubId;
$scope.cassettes = cassetteRepository.cassettes;
$scope.numberOfCassettesToShow = cassetteRepository.numberOfCassettesToShow;
$scope.subId1 = cassetteRepository.subId1;
$scope.subId2 = cassetteRepository.subId2;
$scope.subId3 = cassetteRepository.subId3;
// Every time we alter a $scope variable, we have to remember to also alter the state in the factory.
// This seems error-prone and tedious. Each line below is followed by a line that stores its
// value in the factory.
$scope.onClickCassette = function (cassette) {
$scope.subId1 = cassette._content[0].SolarPanel.SubId;
cassetteRepository.subId1 = cassette._content[0].SolarPanel.SubId;
$scope.subId2 = cassette._content[1].SolarPanel.SubId;
cassetteRepository.subId2 = cassette._content[1].SolarPanel.SubId;
$scope.subId3 = cassette._content[2].SolarPanel.SubId;
cassetteRepository.subId3 = cassette._content[2].SolarPanel.SubId;
}
}
And here is the factory that stores the state:
app.factory('cassetteRepository', ['$http', '$rootScope', function ($http, $rootScope) {
var state = {
userEnteredSubId: '',
cassettes: [],
numberOfCassettesToShow: 10,
subId1: '',
subId2: '',
subId3: ''
}
return state;
}]);
My question is this: Really? It seems like there has to be a more elegant approach. I've only shown a portion of my controller. Every time I set a variable, I have to remember to set its counterpart in the factory where the state is maintained. Am I missing something? Is there a better approach?
If you are using this only to save values then I would use a value instead of creating a service. Here is how you set up a value in Angular:
angular.module('yourAppName')
.value('cassetteRepository', { data: {} });
Now you can inject that value and use its data property directly so you don't have to update things twice. Here's how to use it in a controller:
angular.module('yourAppName')
.controller('yourControllerName', ['$scope', 'cassetteRepository', function($scope, cassetteRepository) {
// create a local scope reference if you need to use this in your view
$scope.cassetteRepository = cassetteRepository;
// now when you set properties on $scope.cassetteRepository.data they are set on
// the value and are persisted and shared across your app.
// you can also use ng-model to two-way bind in your view.
$scope.onClickCassette = function (cassette) {
$scope.cassetteRepository.data.subId1 = cassette._content[0].SolarPanel.SubId;
$scope.cassetteRepository.data.subId2 = cassette._content[1].SolarPanel.SubId;
$scope.cassetteRepository.data.subId3 = cassette._content[2].SolarPanel.SubId;
};
});
You don't have to use the data object in your value. You could also just define the properties directly. E.g.:
.value('cassetteRepository', { userEnteredSubId = "", cassettes = [], numberOfCassettesToShow = 10, etc. })
That way you can provide default values and eliminate having the .data part.
Related
I have a situation where within my angular service, I have a number of properties. These properties are linked to the controller.
Service:
angular.module('...')
.factory('PollServ', function PollServ($http, ...) {
var service = {
question: '',
votes: [[]]
}
...
// make http request to API
var request = $http({ ...
// once the value is retrieved, update properties
request.then(function (res) {
service.question = res.data.question;
...
}
Controller:
angular.module('...')
.controller('PollCtrl', function PollCtrl(..., PollServ) {
$scope.question = PollServ.question;
$scope.votes = PollServ.votes;
...
Now, although the votes are being updated properly, the question is not. I am not doing anything different, except the fact that votes is an array and question is just a regular string. I think the array may have something to do with being able to dynamically update, but not the simple string.
How can I get it to work, without unnecessary turning the string into an array as well?
You said it yourself - the question is a string and thus will not be updated in your controller/view.
What you could do is turning the question into an object. For example:
In Factory
var service = {
question: {
name: ''
},
votes: [[]]
}
...
service.question.name = res.data.question;
You then need to change the reference in your view to question.name.
In an effort to keep my $rootScope clean, I am in need of an object that contains data that is shared among the controllers in my application. As defined, Services are the way to do this inside an Angular app.
With that being said, does a service require a method to be defined in order to work, or can it simply return a object with a list of properties that can be manipulated by the controllers?
Additionally, if the object returned by the service is just data, what is the best way to incorporate it into the scope. Below is my service definition:
angular.module('dataService', [])
.service('gameData', function() {
this.loggedIn = "false";
this.gameJoined = "false";
this.tableFull = "false";
this.username = "";
this.tableInfo = {};
this.gameNum = null;
this.atTable = "";
this.numJoined = 0;
this.userNames = [];
this.numPlayers = _.range(2,7);
this.numOfRuns = _.range(1,11);
});
And when I use this method to inject the service into the controller:
client.controller('loginController', ['$scope','gameData', function
($rootScope, $scope, gameData) {
$scope.gameData = gameData;
console.log('gameData: ');
console.log(gameData.loggedIn);
}]);
gameData.loggedIn in is undefined when this runs.
Any clarification would be greatly appreciated!
Z
A service does not have to define a method, but if you simply want an object then consider using either value(name, value) or constant(name, value) to define a service that simply returns a value.
The difference between these two is that a constant may be injected into a module configuration function (a value cannot) and a value may be overridden by a decorator (which a constant cannot) but for a simple object to share data either will do the job.
angular.module('dataService', [])
.value('gameData', {
loggedIn: false,
gameJoined: false,
tableFull: false,
username: "",
tableInfo: {},
gameNum: null,
atTable: "",
numJoined: 0,
userNames: [],
numPlayers: _.range(2,7),
numOfRuns: _.range(1,11)
});
client.controller('loginController', function($rootScope, $scope, gameData) {
$scope.gameData = gameData;
console.log('gameData: ');
console.log(gameData.loggedIn);
});
No, methods are not compulsory in services. The reason you're code isn't working is because you have messed up the dependancies in the controller.
Change:
['$scope','gameData', function
($rootScope, $scope, gameData)
to
['$rootScope', '$scope','gameData', function
($rootScope, $scope, gameData)
I can find bits and pieces of how to solve this, but no concrete way to make it work.
I have an asynchronous call to a server to fetch data in AngularJS and wish to store it in a variable. This variable then needs to be accessible to all the directives in the app, but they obviously all need to wait for the variable to be assigned before they can use it. I'm also using TypeScript and its export functionality to spin directives from their own functions.
Controller
export class MainController{
fundData: Object;
constructor(scope, FundService) {
FundService.fetchData('some_param').then(d => {
let data = d[0],
fundProps = data.properties_pub;
this.fundData = {
'isin': data.clientCode,
'nav': fundProps.nav.value,
'nav_change': fundProps.nav_change.value.toFixed(2),
'nav_change_direction': change,
'total_aum': fundProps.net_asset.value.toFixed(2)
};
scope.ctrl = this;
});
}
}
Directive
class OverviewController {
scope: ng.IScope;
constructor(scope){
scope.$watch('data', newVal => {
console.log(newVal);
});
}
}
OverviewController.$inject = ['$scope'];
export function overview(): ng.IDirective {
return {
restrict : "C",
controller : OverviewController,
controllerAs : "overview",
template : require("../templates/overview"),
bindToController :{
data: '='
}
}
}
HTML
<div ng-controller="MainController">
<div class="overview" data="ctrl.fundData"></div>
</div>
Bootstrap Process
let module = angular.module(MODULE_NAME,[])
.controller('MainController', ['$scope','FundService', MainController])
.service('FundService', FundService)
.directive('overview', overview);
Things I've Tried:
$rootScope
I can set something static and share it, so this works:
$rootScope.data = 2;
This doesn't:
someFunction().then(data => { $rootScope.data = data });
Maybe there's something about promises in $rootScope I don't understand.
Setting in controller
I can set the result of the call to a variable in the controller, set that to an attribute in the directive, and then bind the attribute to its controller, but this doesn't work either, even if I use $watch on the variable.
What I would do is fetch the data, store it in a service (which I think you are already doing) and then broadcast an event when the data in the service is updated. Here's an example (in raw javascript)
module.service('DataService', function(rootScope) {
...
var data;
services.setData = function(newData) {
data = newData;
rootScope.$broadcast('DataUpdated');
};
...
});
And then in your directives all you would need to do is listen for the 'DataUpdated' event:
scope.$on('DataUpdated', ...);
Hope that helps!
I cannot get a binded service value to update when it is changed. I have tried numerous methods of doing so but none of them have worked, what am I doing wrong? From everything I have seen, this seems like it should work...
HTML:
<div class="drawer" ng-controller="DrawerController">
{{activeCountry}}
</div>
Controller:
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService', function($scope, mapService) {
$scope.$watch(function() { return mapService.activeCountry }, function(newValue, oldValue) {
$scope.activeCountry = mapService.activeCountry;
});
}]);
Service:
angular.module('worldboxApp').
service('mapService', function(dbService, mapboxService, userService) {
this.init = function() {
this.activeCountry = {};
}
this.countryClick = function(e) {
this.activeCountry = e.layer.feature;
};
this.init();
});
I put a break point to make sure the mapService.activeCountry variable is being changed, but all that ever shows in the html is {}.
If you work with objects and their properties on your scope, rather than directly with strings/numbers/booleans, you're more likely to maintain references to the correct scope.
I believe the guideline is that you generally want to have a '.' (dot) in your bindings (esp for ngModel) - that is, {{data.something}} is generally better than just {{something}}. If you update a property on an object, the reference to the parent object is maintained and the updated property can be seen by Angular.
This generally doesn't matter for props you're setting and modifying only in the controller, but for values returned from a service (and that may be shared by multiple consumers of the service), I find it helps to work with an object.
See (these focus on relevance to ngModel binding):
https://github.com/angular/angular.js/wiki/Understanding-Scopes
If you are not using a .(dot) in your AngularJS models you are doing it wrong?
angular.module('worldboxApp', []);
/* Controller */
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService',
function($scope, mapService) {
//map to an object (by ref) rather than just a string (by val), otherwise it's easy to lose reference
$scope.data = mapService.data;
$scope.setCountry = setCountry; //see below
function setCountry(country) {
// could have just set $scope.setCountry = mapService.setCountry;
// however we can wrap it here if we want to do something less generic
// like getting data out of an event object, before passing it on to
// the service.
mapService.setCountry(country);
}
}
]);
/* Service */
angular.module('worldboxApp')
.service('mapService', ['$log',
function($log) {
var self = this; //so that the functions can reference .data; 'this' within the functions would not reach the correct scope
self.data = {
activeCountry: null
}; //we use an object since it can be returned by reference, and changing activeCountry's value will not break the link between it here and the controller using it
_init();
function _init() {
self.data.activeCountry = '';
$log.log('Init was called!');
}
this.setCountry = function _setCountry(country) {
$log.log('setCountry was called: ' + country);
self.data.activeCountry = country;
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<div ng-app="worldboxApp">
<div ng-controller="DrawerController">
<button ng-click="setCountry('USA')">USA</button>
<br />
<button ng-click="setCountry('AUS')">AUS</button>
<br />Active Country: {{data.activeCountry}}
</div>
</div>
In some case $watch is not working with factory object. Than you may use events for updates.
app.factory('userService',['$rootScope',function($rootScope){
var user = {};
return {
getFirstname : function () {
return user.firstname;
},
setFirstname : function (firstname) {
user.firstname = firstname;
$rootScope.$broadcast("updates");
}
}
}]);
app.controller('MainCtrl',['userService','$scope','$rootScope', function(userService,$scope,$rootScope) {
userService.setFirstname("bharat");
$scope.name = userService.getFirstname();
$rootScope.$on("updates",function(){
$scope.name = userService.getFirstname();
});
}]);
app.controller('one',['userService','$scope', function(userService,$scope) {
$scope.updateName=function(){
userService.setFirstname($scope.firstname);
}
}]);
Here is the plunker
Note:- In Some case if broadcast event is not fired instantly you may use $timeout. I have added this in plunker and time depends on your needs. this will work for both factories and services.
I am learning AngularJS. Let's say I have /view1 using My1Ctrl, and /view2 using My2Ctrl; that can be navigated to using tabs where each view has its own simple, but different form.
How would I make sure that the values entered in the form of view1 are not reset, when a user leaves and then returns to view1 ?
What I mean is, how can the second visit to view1 keep the exact same state of the model as I left it ?
I took a bit of time to work out what is the best way of doing this. I also wanted to keep the state, when the user leaves the page and then presses the back button, to get back to the old page; and not just put all my data into the rootscope.
The final result is to have a service for each controller. In the controller, you just have functions and variables that you dont care about, if they are cleared.
The service for the controller is injected by dependency injection. As services are singletons, their data is not destroyed like the data in the controller.
In the service, I have a model. the model ONLY has data - no functions -. That way it can be converted back and forth from JSON to persist it. I used the html5 localstorage for persistence.
Lastly i used window.onbeforeunload and $rootScope.$broadcast('saveState'); to let all the services know that they should save their state, and $rootScope.$broadcast('restoreState') to let them know to restore their state ( used for when the user leaves the page and presses the back button to return to the page respectively).
Example service called userService for my userController :
app.factory('userService', ['$rootScope', function ($rootScope) {
var service = {
model: {
name: '',
email: ''
},
SaveState: function () {
sessionStorage.userService = angular.toJson(service.model);
},
RestoreState: function () {
service.model = angular.fromJson(sessionStorage.userService);
}
}
$rootScope.$on("savestate", service.SaveState);
$rootScope.$on("restorestate", service.RestoreState);
return service;
}]);
userController example
function userCtrl($scope, userService) {
$scope.user = userService;
}
The view then uses binding like this
<h1>{{user.model.name}}</h1>
And in the app module, within the run function i handle the broadcasting of the saveState and restoreState
$rootScope.$on("$routeChangeStart", function (event, next, current) {
if (sessionStorage.restorestate == "true") {
$rootScope.$broadcast('restorestate'); //let everything know we need to restore state
sessionStorage.restorestate = false;
}
});
//let everthing know that we need to save state now.
window.onbeforeunload = function (event) {
$rootScope.$broadcast('savestate');
};
As i mentioned this took a while to come to this point. It is a very clean way of doing it, but it is a fair bit of engineering to do something that i would suspect is a very common issue when developing in Angular.
I would love to see easier, but as clean ways to handle keeping state across controllers, including when the user leaves and returns to the page.
A bit late for an answer but just updated fiddle with some best practice
jsfiddle
var myApp = angular.module('myApp',[]);
myApp.factory('UserService', function() {
var userService = {};
userService.name = "HI Atul";
userService.ChangeName = function (value) {
userService.name = value;
};
return userService;
});
function MyCtrl($scope, UserService) {
$scope.name = UserService.name;
$scope.updatedname="";
$scope.changeName=function(data){
$scope.updateServiceName(data);
}
$scope.updateServiceName = function(name){
UserService.ChangeName(name);
$scope.name = UserService.name;
}
}
$rootScope is a big global variable, which is fine for one-off things, or small apps.
Use a service if you want to encapsulate your model and/or behavior (and possibly reuse it elsewhere). In addition to the google group post the OP mentioned, see also https://groups.google.com/d/topic/angular/eegk_lB6kVs/discussion.
Angular doesn't really provide what you are looking for out of the box. What i would do to accomplish what you're after is use the following add ons
UI Router & UI Router Extras
These two will provide you with state based routing and sticky states, you can tab between states and all information will be saved as the scope "stays alive" so to speak.
Check the documentation on both as it's pretty straight forward, ui router extras also has a good demonstration of how sticky states works.
I had the same problem, This is what I did:
I have a SPA with multiple views in the same page (without ajax), so this is the code of the module:
var app = angular.module('otisApp', ['chieffancypants.loadingBar', 'ngRoute']);
app.config(['$routeProvider', function($routeProvider){
$routeProvider.when('/:page', {
templateUrl: function(page){return page.page + '.html';},
controller:'otisCtrl'
})
.otherwise({redirectTo:'/otis'});
}]);
I have only one controller for all views, but, the problem is the same as the question, the controller always refresh data, in order to avoid this behavior I did what people suggest above and I created a service for that purpose, then pass it to the controller as follows:
app.factory('otisService', function($http){
var service = {
answers:[],
...
}
return service;
});
app.controller('otisCtrl', ['$scope', '$window', 'otisService', '$routeParams',
function($scope, $window, otisService, $routeParams){
$scope.message = "Hello from page: " + $routeParams.page;
$scope.update = function(answer){
otisService.answers.push(answers);
};
...
}]);
Now I can call the update function from any of my views, pass values and update my model, I haven't no needed to use html5 apis for persistence data (this is in my case, maybe in other cases would be necessary to use html5 apis like localstorage and other stuff).
An alternative to services is to use the value store.
In the base of my app I added this
var agentApp = angular.module('rbAgent', ['ui.router', 'rbApp.tryGoal', 'rbApp.tryGoal.service', 'ui.bootstrap']);
agentApp.value('agentMemory',
{
contextId: '',
sessionId: ''
}
);
...
And then in my controller I just reference the value store. I don't think it holds thing if the user closes the browser.
angular.module('rbAgent')
.controller('AgentGoalListController', ['agentMemory', '$scope', '$rootScope', 'config', '$state', function(agentMemory, $scope, $rootScope, config, $state){
$scope.config = config;
$scope.contextId = agentMemory.contextId;
...
Solution that will work for multiple scopes and multiple variables within those scopes
This service was based off of Anton's answer, but is more extensible and will work across multiple scopes and allows the selection of multiple scope variables in the same scope. It uses the route path to index each scope, and then the scope variable names to index one level deeper.
Create service with this code:
angular.module('restoreScope', []).factory('restoreScope', ['$rootScope', '$route', function ($rootScope, $route) {
var getOrRegisterScopeVariable = function (scope, name, defaultValue, storedScope) {
if (storedScope[name] == null) {
storedScope[name] = defaultValue;
}
scope[name] = storedScope[name];
}
var service = {
GetOrRegisterScopeVariables: function (names, defaultValues) {
var scope = $route.current.locals.$scope;
var storedBaseScope = angular.fromJson(sessionStorage.restoreScope);
if (storedBaseScope == null) {
storedBaseScope = {};
}
// stored scope is indexed by route name
var storedScope = storedBaseScope[$route.current.$$route.originalPath];
if (storedScope == null) {
storedScope = {};
}
if (typeof names === "string") {
getOrRegisterScopeVariable(scope, names, defaultValues, storedScope);
} else if (Array.isArray(names)) {
angular.forEach(names, function (name, i) {
getOrRegisterScopeVariable(scope, name, defaultValues[i], storedScope);
});
} else {
console.error("First argument to GetOrRegisterScopeVariables is not a string or array");
}
// save stored scope back off
storedBaseScope[$route.current.$$route.originalPath] = storedScope;
sessionStorage.restoreScope = angular.toJson(storedBaseScope);
},
SaveState: function () {
// get current scope
var scope = $route.current.locals.$scope;
var storedBaseScope = angular.fromJson(sessionStorage.restoreScope);
// save off scope based on registered indexes
angular.forEach(storedBaseScope[$route.current.$$route.originalPath], function (item, i) {
storedBaseScope[$route.current.$$route.originalPath][i] = scope[i];
});
sessionStorage.restoreScope = angular.toJson(storedBaseScope);
}
}
$rootScope.$on("savestate", service.SaveState);
return service;
}]);
Add this code to your run function in your app module:
$rootScope.$on('$locationChangeStart', function (event, next, current) {
$rootScope.$broadcast('savestate');
});
window.onbeforeunload = function (event) {
$rootScope.$broadcast('savestate');
};
Inject the restoreScope service into your controller (example below):
function My1Ctrl($scope, restoreScope) {
restoreScope.GetOrRegisterScopeVariables([
// scope variable name(s)
'user',
'anotherUser'
],[
// default value(s)
{ name: 'user name', email: 'user#website.com' },
{ name: 'another user name', email: 'anotherUser#website.com' }
]);
}
The above example will initialize $scope.user to the stored value, otherwise will default to the provided value and save that off. If the page is closed, refreshed, or the route is changed, the current values of all registered scope variables will be saved off, and will be restored the next time the route/page is visited.
You can use $locationChangeStart event to store the previous value in $rootScope or in a service. When you come back, just initialize all previously stored values. Here is a quick demo using $rootScope.
var app = angular.module("myApp", ["ngRoute"]);
app.controller("tab1Ctrl", function($scope, $rootScope) {
if ($rootScope.savedScopes) {
for (key in $rootScope.savedScopes) {
$scope[key] = $rootScope.savedScopes[key];
}
}
$scope.$on('$locationChangeStart', function(event, next, current) {
$rootScope.savedScopes = {
name: $scope.name,
age: $scope.age
};
});
});
app.controller("tab2Ctrl", function($scope) {
$scope.language = "English";
});
app.config(function($routeProvider) {
$routeProvider
.when("/", {
template: "<h2>Tab1 content</h2>Name: <input ng-model='name'/><br/><br/>Age: <input type='number' ng-model='age' /><h4 style='color: red'>Fill the details and click on Tab2</h4>",
controller: "tab1Ctrl"
})
.when("/tab2", {
template: "<h2>Tab2 content</h2> My language: {{language}}<h4 style='color: red'>Now go back to Tab1</h4>",
controller: "tab2Ctrl"
});
});
<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular-route.js"></script>
<body ng-app="myApp">
Tab1
Tab2
<div ng-view></div>
</body>
</html>