форма автосохранения angularjs это правильный путь?

Моя цель - автоматически сохранить форму после того, как она действительна, и обновить ее с тайм-аутом. Я настроил так:

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

Мне интересно, есть ли лучший способ.


person Whisher    schedule 15.01.2014    source источник
comment
Этот вопрос не относится к теме, поскольку вопросы проверки кода следует публиковать на codereview.stackexchange.com.   -  person Stewie    schedule 15.01.2014


Ответы (3)


Глядя на код, я вижу, что $watch не будет повторно запущен, если текущий ввод действителен, и пользователь также изменит что-либо, что является действительным. Это связано с тем, что функции наблюдения выполняются только в том случае, если значение изменилось. Вы также должны проверить грязное состояние формы и сбросить его, когда данные формы были сохранены, иначе вы получите бесконечный цикл сохранения.

И вы не очищаете предыдущие тайм-ауты.

И текущий код сохранит неверные данные, если текущий тайм-аут продолжается.

Я добавил директиву, которая делает все это и имеет лучший SOC, чтобы ее можно было использовать повторно. Просто предоставьте ему выражение обратного вызова, и все готово.

Посмотрите на этот плункер в действии.

Контроллер демонстрации

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

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

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

});

Демонстрационный 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>

Директива

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

      });
    }
  };

});
person null    schedule 15.01.2014
comment
Спасибо, это приятно. Добавили ли вы какие-либо другие функции в эту директиву, которыми хотели бы поделиться? Я, вероятно, установлю свой таймер немного выше, поэтому, возможно, мне следует закоротить часы на часть таймера. период, чтобы предотвратить $rootscope.$digest при каждом нажатии клавиши? Также, возможно, функция типа validateAndSaveNow подходит для обработки событий beforeNavigate. Координация с angular-loading-bar могла бы быть хорошей. Я могу заменить обычную функцию revertForm, подключенную к кнопке отмены, операцией setBack, и save() произойдет PATCH, а не полная POST... - person shannon; 21.03.2015
comment
Хм, а также, как справиться с одновременным редактированием, не возвращая ручные операции с пользовательским интерфейсом (которые призвано исключить автосохранение) прямо в пользовательский интерфейс? Я полагаю, что ввод поля отправляет запрос на блокировку. Не обращайте на меня внимания, я просто буду добавлять сюда свои мысли. ;) - person shannon; 21.03.2015
comment
Вот пример с очень похожим на ваш подходом, который включает в себя несколько других идей и решает несколько проблем: adamalbrecht.com/2013/10/30/ Часть этого поста написана до вашего ответа, а часть после, поэтому я не знал кому отдать должное :D - person shannon; 21.03.2015
comment
@shannon Часть этого поста написана до вашего ответа, а часть после, поэтому я не знал, кому отдать должное. Я не совсем уверен, но вы предполагаете, что я украл титры этого поста? Кроме того, код не выглядит так же отдаленно для меня. Возможно, я установлю свой таймер немного выше, ... чтобы предотвратить $rootscope.$digest при каждом нажатии клавиши?. Вы будете получать $digest при каждом нажатии клавиши, когда вы устанавливаете ng-model для элемента ввода, таймер ничего не меняет. - person null; 22.03.2015
comment
Нет, вовсе не предлагает. Я предполагаю, что у вас обоих были фундаментально схожие подходы, и я пытался избежать, ради автора этого другого кода, который я нашел, подразумевая, что работа другого автора не имеет никакой ценности. (Мир, чувак.) - person shannon; 22.03.2015
comment
И да, таймер действительно меняет это, если вы замкнете часы (часть цитаты, которую вы пропустили) при создании таймера. Страдает разрешение задержки, но регулировка разрешения задержки (которая затем непосредственно определяет частоту дайджестов) может осуществляться отдельно от регулировки задержки. Есть смысл? - person shannon; 22.03.2015
comment
Я использую это в течение нескольких лет, но недавно заметил, что AngularJS генерирует бесконечную ошибку дайджеста — я думаю, из-за отмены и создания нового тайм-аута $. Я предполагаю, что что-то изменилось, поэтому один или оба из них создают новый дайджест. Я добавил новый ответ с немного измененным вариантом этой директивы, который, надеюсь, позволит избежать этой проблемы. - person Jeremy; 14.06.2017

ОБНОВЛЕНИЕ: для остановки тайм-аута всей логики в директиве

.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);
                    }
                );
            }
        }
    }
})
person Whisher    schedule 17.01.2014
comment
если вы хотите обновить свой вопрос, просто отредактируйте исходный вопрос. Не публикуйте ответ ... то есть, если вы не отвечаете на свой собственный вопрос, но затем напишите свой ответ как ответ, а не обновление - person Steve Lorimer; 07.02.2014
comment
Извините, в следующий раз последую вашему совету. - person Whisher; 07.02.2014

Вот вариант директивы Null, созданный, потому что я начал видеть ошибки «Бесконечный цикл $digest». (Я подозреваю, что что-то изменилось в Angular, где отмена/создание $timeout() теперь запускает дайджест.)

В этом варианте используется правильное выражение $watch — наблюдение за тем, чтобы форма была грязной и действительной, — а затем вызывается $setPristine() ранее, поэтому часы будут повторно запускаться, если форма снова переходит в грязную. Затем мы используем $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);

      });
    }
  };

});
person Jeremy    schedule 14.06.2017