Current question is logical continuation of this question
Abstract
Hi, I'm trying to automatically persist ng-model
values on per-history step basis. So that when a person clicks browser's back/forward button he gets the same values in his inputs/checkboxes/etc. that were there before he left (including user's edits).
I'm using browser's Session Storage(Web Storage) to persist ng-model
values. this kind of storage works on per-tab/per-session basis so it's absolutely ok to use it with this kind of problem.
Via 'ui-router' I'm able to associate each user's "navigation" with unique UUID(GUID) which is later used to obtain a bucket
that contains all persisted $scope
values.
I even get out with an implementation that (partially) works, you can find in the bottom of this question.
Problem
Currently I'm having a problem with storing values on per-scope basis. Currently I use ng-model
's key as a key for a bucket, for example in ng-model="city.name"
I use "city.name"
as a key for my bucket. In this example the resulting bucket that goes into Session Storage will look like
var bucket === {
"city.name": "Some city name"
}
...
$window.sessionStorage.setItem(viewId, bucket);
Obviously if we are iterating over the list of cities we might get dozen of this kind of keys. In order to resolve this problem, I need some sort of id
that uniquely identifies current ng-model
location within whole $scope
structure.
I can't use $scope.$id
for this purpose because its changing each time the scope is recreated. So one of the ideas I've come out with is to count current $scope
's relative position in the $scope tree using $scope
's $$prevSibling
property and concatenating it with the same position of it's parent, and parent's parent, until we reach $rootScope, here's how I see it:
$rootScope.$index = function() {
return this.$$index || (this.$$index = !this.$$prevSibling ? 0 : this.$$prevSibling.$index());
};
$rootScope.$uid = function() {
return this.$$uid || (this.$$uid = (!this.$parent ? '' : this.$parent.$uid()) + ',' + this.$index());
};
A call to $scope.$uid()
would allow to get a string value similar to this '0,4,6,2,1,123'
which would uniquely identify current $scope
's position in a hierarchy of scopes. This value can be concatenated to a ng-model
's key and would result with a bucket like that
{
"0,4,6,2,1,1-city.name": "Some city name 1"
"0,4,6,2,1,2-city.name": "Some city name 2"
"0,4,6,2,1,3-city.name": "Some city name 3"
...
"0,4,6,2,1,123-city.name": "Some city name 123"
}
Question
The provided implementation will obviously fail when $scope
s are creating / destroying dynamically during "navigation" lifetime, in other words $scope
's $$prevSibling
may change. I'm not sure whether $scopes can be prepended (which would f*ck up indexing and break everything apart) but I'm sure that the scopes can be destroyed and deleted causing the inconsistent indexing. So obviously I have a general question what do you think of this problem, are there any better solutions to this problem from your perspective
Thank you in advance!
Existing implementation
I've decorated ng-model
so that we store each change in Session Storage, the changes are being throttled and stored after 500ms delay, each change that happened within these 500ms is restarting previous timer.
$provide.decorator('ngModelDirective', function ($delegate) {
var ngModel = $delegate[0], controller = ngModel.controller;
var persisted, viewId;
ngModel.controller = ['$scope', '$state', '$element', '$attrs', '$injector', '$parse', '$window', '$timeout', function ($scope, $state, $element, $attrs, $injector, $parse, $window, $timeout) {
if (viewId !== $state.params.viewId) {
viewId = $state.params.viewId;
persisted = $window.sessionStorage.getItem(viewId);
}
if ('ng-model-non-persisted' in $attrs) {
} else {
var ngModelGet = $parse($attrs.ngModel),
ngModelSet = ngModelGet.assign;
if (ngModelSet) { // Otherwise base call will throw an exception
if (persisted) {
persisted = angular.fromJson(persisted);
var model = ngModelGet(persisted);
if (model !== void 0) {
ngModelSet($scope, model);
}
} else {
persisted = {};
}
var throttle, cachedValue;
$scope.$watch(function () {
var value = ngModelGet($scope);
if (cachedValue !== value) {
cachedValue = value;
ngModelSet(persisted, value);
if (throttle) {
$timeout.cancel(throttle);
}
throttle = $timeout(function() {
$window.sessionStorage.setItem(viewId, angular.toJson(persisted));
throttle = null;
}, 500);
}
});
}
}
$injector.invoke(controller, this, {
'$scope': $scope,
'$element': $element,
'$attrs': $attrs
});
}];
return $delegate;
});