3

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 $scopes 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;
});
Community
  • 1
  • 1
Lu4
  • 14,873
  • 15
  • 79
  • 132
  • Why do you need to store per-scope values? What is the significance of that? It seems to me that all you need to do is to store the ViewModel state of each controller, and make the controller function idempotent. – New Dev Nov 17 '14 at 04:58
  • My ViewModels are heavily cross-referenced, it's not that easy to track their changes or serialize them. In turn `ng-model` nearly always operates with atomic values (or arrays of atomic values), it's not common that you do `ng-model="complexViewModelObject"`. Therefore `ng-model` values are always easily checked for changes and serialized. The only thing that is left is to get the ID that uniquely describes object's storage location, after current post I've made some changes and now I delegate ID creation to end-developer, it's +1 task and +1 ability to forget something, it's not that good – Lu4 Nov 17 '14 at 05:22
  • You know your code best, but I would have reexamined the design, since ViewModels should be fairly simple representations of your app state and the resulting UI, and thus easily serializable. Leave the complexity to Models and Services. Also, it doesn't seem like a good idea to have a dependency on the View - which is what you get by relying on a particular $scope hierarchy. – New Dev Nov 17 '14 at 05:33
  • possible duplicate of [Get DOM element by scope $id](http://stackoverflow.com/questions/23253506/get-dom-element-by-scope-id) – Paul Sweatte Aug 29 '15 at 05:41

0 Answers0