angularjs autosave form is it the right way?

2019-02-01 06:41发布

问题:

My goal is to autosave a form after is valid and update it with timeout. I set up like:

(function(window, angular, undefined) {
    'use strict';
    angular.module('nodblog.api.article', ['restangular'])
        .config(function (RestangularProvider) {
            RestangularProvider.setBaseUrl('/api');
            RestangularProvider.setRestangularFields({
                id: "_id"
            });
            RestangularProvider.setRequestInterceptor(function(elem, operation, what) {
                if (operation === 'put') {
                    elem._id = undefined;
                    return elem;
                }
                return elem;
            }); 
        })
        .provider('Article', function() {
            this.$get = function(Restangular) {
                function ngArticle() {};
                ngArticle.prototype.articles = Restangular.all('articles');
                ngArticle.prototype.one = function(id) {
                    return Restangular.one('articles', id).get();
                };
                ngArticle.prototype.all = function() {
                    return this.articles.getList();
                };
                ngArticle.prototype.store = function(data) {
                    return this.articles.post(data);
                };
                ngArticle.prototype.copy = function(original) {
                    return  Restangular.copy(original);
                };
                return new ngArticle;
            }
    })
})(window, angular);

angular.module('nodblog',['nodblog.route'])
.directive("autosaveForm", function($timeout,Article) {
    return {
        restrict: "A",
        link: function (scope, element, attrs) {
            var id = null;
            scope.$watch('form.$valid', function(validity) {
                if(validity){
                    Article.store(scope.article).then(
                        function(data) {
                            scope.article = Article.copy(data);
                            _autosave();
                        }, 
                        function error(reason) {
                            throw new Error(reason);
                        }
                    );
                }  
            })
            function _autosave(){
                    scope.article.put().then(
                    function() {
                        $timeout(_autosave, 5000); 
                    },
                    function error(reason) {
                        throw new Error(reason);
                    }
                );
            }
        }
    }
})

.controller('CreateCtrl', function ($scope,$location,Article) {
        $scope.article = {};
        $scope.save = function(){
            if(typeof $scope.article.put === 'function'){
                $scope.article.put().then(function() {
                    return $location.path('/blog');
                });
            }
            else{
                Article.store($scope.article).then(
                    function(data) {
                        return $location.path('/blog');
                    }, 
                    function error(reason) {
                        throw new Error(reason);
                    }
                );
            }
        };
     })

I'm wondering if there is a best way.

回答1:

Looking at the code I can see is that the $watch will not be re-fired if current input is valid and the user changes anything that is valid too. This is because watch functions are only executed if the value has changed. You should also check the dirty state of the form and reset it when the form data has been persisted otherwise you'll get an endless persist loop.

And your not clearing any previous timeouts.

And the current code will save invalid data if a current timeout is in progress.

I've plunked a directive which does this all and has better SOC so it can be reused. Just provide it a callback expression and you're good to go.

See it in action in this plunker.

Demo Controller

myApp.controller('MyController', function($scope) {

  $scope.form = {
    state: {},
    data: {}
  };

  $scope.saveForm = function() {
    console.log('Saving form data ...', $scope.form.data);  
  };

});

Demo Html

  <div ng-controller="MyController">

    <form name="form.state" auto-save-form="saveForm()">

      <div>
        <label>Numbers only</label>
        <input name="text" 
               ng-model="form.data.text" 
               ng-pattern="/^\d+$/"/>
      </div>

      <span ng-if="form.state.$dirty && form.state.$valid">Updating ...</span>      

    </form>
  </div>

Directive

myApp.directive('autoSaveForm', function($timeout) {

  return {
    require: ['^form'],
    link: function($scope, $element, $attrs, $ctrls) {

      var $formCtrl = $ctrls[0];
      var savePromise = null;
      var expression = $attrs.autoSaveForm || 'true';

      $scope.$watch(function() {

        if($formCtrl.$valid && $formCtrl.$dirty) {

          if(savePromise) {
            $timeout.cancel(savePromise);
          }

          savePromise = $timeout(function() {

            savePromise = null;

            // Still valid?

            if($formCtrl.$valid) {

              if($scope.$eval(expression) !== false) {
                console.log('Form data persisted -- setting prestine flag');
                $formCtrl.$setPristine();  
              }

            }

          }, 500);
        }

      });
    }
  };

});


回答2:

UPDATE: to stopping timeout all the logic in the directive

.directive("autosaveForm", function($timeout,$location,Post) {
    var promise;
    return {
        restrict: "A",
        controller:function($scope){
            $scope.post = {};
            $scope.save = function(){
                console.log(promise);
                $timeout.cancel(promise);
                if(typeof $scope.post.put === 'function'){
                    $scope.post.put().then(function() {
                        return $location.path('/post');
                    });
                }
                else{
                    Post.store($scope.post).then(
                        function(data) {
                            return $location.path('/post');
                        }, 
                        function error(reason) {
                            throw new Error(reason);
                        }
                    );
                }
            };

        },
        link: function (scope, element, attrs) {
            scope.$watch('form.$valid', function(validity) {
                element.find('#status').removeClass('btn-success');
                element.find('#status').addClass('btn-danger');
                if(validity){
                    Post.store(scope.post).then(
                        function(data) {
                            element.find('#status').removeClass('btn-danger');
                            element.find('#status').addClass('btn-success');
                            scope.post = Post.copy(data);
                            _autosave();
                        }, 
                        function error(reason) {
                            throw new Error(reason);
                        }
                    );
                }  
            })
            function _autosave(){
                    scope.post.put().then(
                    function() {
                        promise = $timeout(_autosave, 2000);
                    },
                    function error(reason) {
                        throw new Error(reason);
                    }
                );
            }
        }
    }
})


回答3:

Here's a variation of Null's directive, created because I started seeing "Infinite $digest Loop" errors. (I suspect something changed in Angular where cancelling/creating a $timeout() now triggers a digest.)

This variation uses a proper $watch expression - watching for the form to be dirty and valid - and then calls $setPristine() earlier so the watch will re-fire if the form transitions to dirty again. We then use an $interval to wait for a pause in those dirty notifications before saving the form.

app.directive('autoSaveForm', function ($log, $interval) {

  return {
    require: ['^form'],
    link: function (scope, element, attrs, controllers) {

      var $formCtrl = controllers[0];
      var autoSaveExpression = attrs.autoSaveForm;
      if (!autoSaveExpression) {
        $log.error('autoSaveForm missing parameter');
      }

      var savePromise = null;
      var formModified;

      scope.$on('$destroy', function () {
        $interval.cancel(savePromise);
      });

      scope.$watch(function () {
        // note: formCtrl.$valid is undefined when this first runs, so we use !$formCtrl.$invalid instead
        return !$formCtrl.$invalid && $formCtrl.$dirty;
      }, function (newValue, oldVaue, scope) {

        if (!newValue) {
          // ignore, it's not "valid and dirty"
          return;
        }

        // Mark pristine here - so we get notified again if the form is further changed, which would make it dirty again
        $formCtrl.$setPristine();

        if (savePromise) {
          // yikes, note we've had more activity - which we interpret as ongoing changes to the form.
          formModified = true;
          return;
        }

        // initialize - for the new interval timer we're about to create, we haven't yet re-dirtied the form
        formModified = false;

        savePromise = $interval(function () {

          if (formModified) {
            // darn - we've got to wait another period for things to quiet down before we can save
            formModified = false;
            return;
          }

          $interval.cancel(savePromise);
          savePromise = null;

          // Still valid?

          if ($formCtrl.$valid) {

            $formCtrl.$saving = true;
            $log.info('Form data persisting');

            var autoSavePromise = scope.$eval(autoSaveExpression);
            if (!autoSavePromise || !autoSavePromise.finally) {
              $log.error('autoSaveForm not returning a promise');
            }

            autoSavePromise
            .finally(function () {
              $log.info('Form data persisted');
              $formCtrl.$saving = undefined;
            });
          }
        }, 500);

      });
    }
  };

});