angularjs自动保存格式是正确的吗?

时间:2022-03-03 19:39:20

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.

我想知道有没有最好的办法。

3 个解决方案

#1


27  

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.

查看我看到的代码,如果当前输入是有效的,并且用户更改了任何有效的内容,那么$watch将不会被重新触发。这是因为只有在值发生变化时才执行watch函数。您还应该检查窗体的脏状态,并在窗体数据被持久化时重置它,否则您将得到一个没完没了的持久化循环。

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.

我用了一个指令来实现这一切,并且有更好的SOC,所以它可以被重用。只要提供一个回调表达式,就可以了。

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

演示的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


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


0  

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.)

下面是Null指令的一个变体,创建它是因为我开始看到“无限的$digest循环”错误。(我怀疑在取消/创建一个$timeout()现在触发了一个摘要的角度变化。)

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.

这个变体使用一个合适的$watch表达式—监视表单的脏性和有效性—然后在前面调用$ setpure(),以便当表单再次变为脏时,手表将重新启动。然后我们使用$interval在保存表单之前等待脏通知中的暂停。

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);

      });
    }
  };

});

#1


27  

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.

查看我看到的代码,如果当前输入是有效的,并且用户更改了任何有效的内容,那么$watch将不会被重新触发。这是因为只有在值发生变化时才执行watch函数。您还应该检查窗体的脏状态,并在窗体数据被持久化时重置它,否则您将得到一个没完没了的持久化循环。

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.

我用了一个指令来实现这一切,并且有更好的SOC,所以它可以被重用。只要提供一个回调表达式,就可以了。

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

演示的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


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


0  

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.)

下面是Null指令的一个变体,创建它是因为我开始看到“无限的$digest循环”错误。(我怀疑在取消/创建一个$timeout()现在触发了一个摘要的角度变化。)

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.

这个变体使用一个合适的$watch表达式—监视表单的脏性和有效性—然后在前面调用$ setpure(),以便当表单再次变为脏时,手表将重新启动。然后我们使用$interval在保存表单之前等待脏通知中的暂停。

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);

      });
    }
  };

});