Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Storing Form/Controller State in AngularJS

         In data-intensive applications, saving form state and progress often comes up as a requirement. Ex :- 3 tabs in one view linked to a single controller. User wants to fill the first 2 tabs, save it, load it back later and fill the 3rd tab contents. Now, the contents of the first 2 tabs might not correspond to a complete resource on the server-side or mandatory fields required for business validations has not been filled in the form yet. If you are looking for a server-side save, you will have to bypass those validations, maintain a flag somewhere to indicate that it is partial saved so that reports don't pick up those data, etc and the server-side implementation might not even be a single place generic change.
Reasons for doing the above might be because the user is doing the data-entry from a form and might need to clarify some of the things from the person who filled it before submitting that data.

An AngularJS directive leveraging local storage can fix the issue quite easily.

Usage :-
<partial-save></partial-save>

Notes :-
  • On clicking save, it is iterating over all scope properties (ignoring certain things like functions since we don't want a load executed after releasing a new build to pick up old function logic) and saving it against a map where the key comprises of the logged-in userId along with the evaluated route of the view.
  • We are then stringifying the entire map and base64 encoding it before saving it to local storage.
  • checkData function is to show the "Load" button only if data is available for that userId and route.  


PartialSaveDirective: function (resourceFactory, location, $rootScope, localStorageService) {

return {
restrict: 'EA',
replace: true,
link: function (scope, elm, attr, ctrl) {

scope.save = function() {
var base = {};
for (var property in scope) {
if(scope.hasOwnProperty(property)
&& scope[property] && property != 'this'
&& typeof scope[property] != "function"
&& property.slice(0, 1) != '$'
&& property.slice(0, 1) != '_') {
base[property] = scope[property];
}
}

$rootScope.bases[$rootScope.sessionData.userId+location.$$path] = base;
var base64encoded = btoa(JSON.stringify($rootScope.bases));
localStorageService.add('bases', base64encoded);
//Show success noty.
};

scope.load = function() {
var key = $rootScope.sessionData.userId + location.$$path;
for(property in $rootScope.bases) {
if(property == key) {
for(prop in $rootScope.bases[property]) {
scope[prop] = $rootScope.bases[property][prop];
}
//Show success noty
}
}
};

scope.checkData = function() {
var key = $rootScope.sessionData.userId + location.$$path;
for(property in $rootScope.bases) {
if(property == key) {
return true;
}
}
return false;
};
},

template: '<div class="pull-right"><div class="btn-group">'+
'<button ng-click="save()" class="btn btn-success">Save Page</button>' +
'<button ng-click="load()" ng-show="checkData()" class="btn btn-primary">Load Page</button>' +
'</div>'
};
}

Now, on login of the application,  the below code will fetch it back from local storage and parse it back to JSON. The issue with that is when we clicked "Save", the above directive stringified it which means dates became strings and when we parse it back, strings remain strings. So, we basically need to traverse the object graph and validate the date strings properly and convert them back to date objects. The challenge with that is Date.parse() or new Date(string) will successfully convert strings such as "4.3" or "hello1" to dates. So, I ended up bower installing angular-moment. Check out this stackoverflow thread :- 
https://stackoverflow.com/questions/7445328/check-if-a-string-is-a-date-value?lq=1

Notes :-
  • This code is to be added on successful login, somewhere like the success callback in an AuthenticationService.
  • scope here is rootScope
  • Traverse is a recursive function for navigating the object graph. The only caveat is that typeof array is an object in javascript so in order to treat objects separately, you will have to do a check using instanceof Array.
  • ISO_8601 is a bunch of formats you can check out at http://www.w3.org/TR/NOTE-datetime. I hope your application is treating all dates in that format.
        var bases = localStorageService.get('bases');
if(bases)
scope.bases = scope.traverse(JSON.parse(atob(bases)));

scope.parseDate = function(key,value) {
if(typeof(value) == 'string' && isNaN(value) &&
moment(value, moment.ISO_8601, true).isValid()) {
return new Date(value);
} else
return false;
};

scope.traverse = function(o) {
for (var i in o) {
if(o.hasOwnProperty(i)
&& o[i] && i != 'this'
&& typeof o[i] != "function"
&& i.slice(0, 1) != '$'
&& i.slice(0, 1) != '_') {
if (typeof(o[i])=="object" && !(o[i] instanceof Array)) {
scope.traverse(o[i]);
} else if(o[i] instanceof Array) {
for(var j in o[i]) {
if(typeof(o[i][j]) == "object")
scope.traverse(o[i][j])
else {
var value1 = scope.parseDate(j, o[i][j]);
if(value1)
o[i][j] = value1;
}
}
} else {
var value2 = scope.parseDate(i, o[i]);
if(value2)
o[i] = value2;
}
}
}
return o;

};

If security is important for your application and you are not allowed to store unencrypted data in your local storage, you can follow my next blog post in order to secure the data properly.




This post first appeared on Night Without End, please read the originial post: here

Share the post

Storing Form/Controller State in AngularJS

×

Subscribe to Night Without End

Get updates delivered right to your inbox!

Thank you for your subscription

×