crmAutosave - Rewrite as service/class instead of directive. Expose internal API.
authorTim Otten <totten@civicrm.org>
Wed, 18 Feb 2015 19:52:30 +0000 (11:52 -0800)
committerTim Otten <totten@civicrm.org>
Thu, 19 Feb 2015 22:03:35 +0000 (14:03 -0800)
js/angular-crmAutosave.js
tests/karma/unit/crmAutosaveSpec.js

index 8b5a24c56a859748f656f0f53911398a05435787..f492cb973925dc98d016a4657976506c2e80c00e 100644 (file)
 
   angular.module('crmAutosave', ['crmUtil']);
 
-  // usage: <form crm-autosave="myCtrl.save()" crm-autosave-model="myModel">...</form>
-  //
-  // Automatically save changes. Don't save while the user is actively updating the model -- save
-  // after a pause in user activity (e.g. after 2sec).
-  //
-  //  - crm-autosave="callback" -- A function to handle saving. Should return a promise.
-  //                               If it's not a promise, then we'll assume that it completes successfully.
-  //  - crm-autosave-interval="object" -- Interval spec. Default: {poll: 250, save: 5000}
-  //  - crm-autosave-if="conditional" -- Only allow autosave when conditional returns true. Default: !form.$invalid
-  //  - crm-autosave-model="object" -- (Re)schedule saves based on observed changes to object.
-  //    We perform deep ispection on the model object. This could be a performance issue you had many concurrent
-  //    autosave forms, but it should be fine with one.
-  //
-  // The call to the autosave function will cause the form to be marked as pristine (unless there's an error).
-  angular.module('crmAutosave').directive('crmAutosave', function($interval, $timeout) {
-    return {
-      restrict: 'AE',
-      require: '^form',
-      link: function(scope, element, attrs, form) {
-        var intervals = angular.extend({poll: 250, save: 5000}, scope.$eval(attrs.crmAutosaveInterval));
-        var jobs = {poll: null, save: null}; // job handles used ot cancel/reschedule timeouts/intervals
-        var lastSeenModel = null;
-        var saving = false;
+  // usage:
+  //   var autosave = new CrmAutosaveCtrl({
+  //     save: function           -- A function to handle saving. Should return a promise.
+  //                                 If it's not a promise, then we'll assume that it completes successfully.
+  //     saveIf: function         -- Only allow autosave when conditional returns true. Default: !form.$invalid
+  //     model: object|function   -- (Re)schedule saves based on observed changes to object. We perform deep
+  //                                 inspection on the model object. This could be a performance issue you
+  //                                 had many concurrent autosave forms or a particularly large model, but
+  //                                 it should be fine with typical usage.
+  //     interval: object         -- Interval spec. Default: {poll: 250, save: 5000}
+  //     form: object|function    -- FormController or its getter
+  //   });
+  //   autosave.start();
+  //   $scope.$on('$destroy', autosave.stop);
+  // Note: if the save operation itself
+  angular.module('crmAutosave').service('CrmAutosaveCtrl', function($interval, $timeout) {
+    function CrmAutosaveCtrl(options) {
+      var intervals = angular.extend({poll: 250, save: 5000}, options.interval);
+      var jobs = {poll: null, save: null}; // job handles used ot cancel/reschedule timeouts/intervals
+      var lastSeenModel = null;
+      var saving = false;
 
-        // Determine if model has changed; (re)schedule the save.
-        // This is a bit expensive and doesn't need to be continuous, so we use polling instead of watches.
-        function checkChanges() {
-          if (saving) {
-            return;
-          }
-          var currentModel = scope.$eval(attrs.crmAutosaveModel);
-          if (lastSeenModel === null) {
-            lastSeenModel = angular.copy(currentModel);
-          }
-          else if (!angular.equals(currentModel, lastSeenModel)) {
-            lastSeenModel = angular.copy(currentModel);
-            if (jobs.save) {
-              $timeout.cancel(jobs.save);
-            }
-            jobs.save = $timeout(doAutosave, intervals.save);
+      // Determine if model has changed; (re)schedule the save.
+      // This is a bit expensive and doesn't need to be continuous, so we use polling instead of watches.
+      function checkChanges() {
+        if (saving) {
+          return;
+        }
+        var currentModel = _.isFunction(options.model) ? options.model() : options.model;
+        if (!angular.equals(currentModel, lastSeenModel)) {
+          lastSeenModel = angular.copy(currentModel);
+          if (jobs.save) {
+            $timeout.cancel(jobs.save);
           }
+          jobs.save = $timeout(doAutosave, intervals.save);
         }
+      }
 
-        function doAutosave() {
-          jobs.save = null;
-          if (saving) {
-            return;
-          }
+      function doAutosave() {
+        jobs.save = null;
+        if (saving) {
+          return;
+        }
 
-          if (attrs.crmAutosaveIf) {
-            if (!scope.$eval(attrs.crmAutosaveIf)) {
-              return;
-            }
-          }
-          else if (form.$invalid) {
+        var form = _.isFunction(options.form) ? options.form() : options.form;
+
+        if (options.saveIf) {
+          if (!options.saveIf()) {
             return;
           }
+        }
+        else if (form && form.$invalid) {
+          return;
+        }
 
-          saving = true;
-          lastSeenModel = angular.copy(scope.$eval(attrs.crmAutosaveModel));
+        saving = true;
+        lastSeenModel = angular.copy(_.isFunction(options.model) ? options.model() : options.model);
 
-          // Set to pristine before saving -- not after saving.
-          // If an eager user continues editing concurrent with the
-          // save process, then the form should become dirty again.
+        // Set to pristine before saving -- not after saving.
+        // If an eager user continues editing concurrent with the
+        // save process, then the form should become dirty again.
+        if (form) {
           form.$setPristine();
-          var res = scope.$eval(attrs.crmAutosave);
-          if (res && res.then) {
-            res.then(
-              function() {
-                saving = false;
-              },
-              function() {
-                saving = false;
+        }
+        var res = options.save();
+        if (res && res.then) {
+          res.then(
+            function() {
+              saving = false;
+            },
+            function() {
+              saving = false;
+              if (form) {
                 form.$setDirty();
               }
-            );
-          }
-          else {
-            saving = false;
-          }
+            }
+          );
         }
+        else {
+          saving = false;
+        }
+      }
 
-        jobs.poll = $interval(checkChanges, intervals.poll);
-        element.on('$destroy', function() {
-          if (jobs.poll) {
-            $interval.cancel(jobs.poll);
-            jobs.poll = null;
-          }
-          if (jobs.save) {
-            $timeout.cancel(jobs.save);
-            jobs.save = null;
-          }
-        });
+      this.start = function() {
+        if (!jobs.poll) {
+          lastSeenModel = angular.copy(_.isFunction(options.model) ? options.model() : options.model);
+          jobs.poll = $interval(checkChanges, intervals.poll);
+        }
+      };
 
-      }
-    };
+      this.stop = function() {
+        if (jobs.poll) {
+          $interval.cancel(jobs.poll);
+          jobs.poll = null;
+        }
+        if (jobs.save) {
+          $timeout.cancel(jobs.save);
+          jobs.save = null;
+        }
+      };
+    }
+
+    return CrmAutosaveCtrl;
   });
 
 })(angular, CRM.$, CRM._);
index fc5926aefefd64925a089c433559eb71cac80200..2d06f1fbb218c1ae12c21ddff6553626ab3d94a3 100644 (file)
@@ -13,15 +13,17 @@ describe('crmAutosave', function() {
       $interval,
       $timeout,
       fakeCtrl,
+      CrmAutosaveCtrl,
       model,
       element;
 
-    beforeEach(inject(function(_$compile_, _$rootScope_, _$interval_, _$timeout_, _$q_) {
+    beforeEach(inject(function(_$compile_, _$rootScope_, _$interval_, _$timeout_, _$q_, _CrmAutosaveCtrl_) {
       // The injector unwraps the underscores (_) from around the parameter names when matching
       $compile = _$compile_;
       $rootScope = _$rootScope_;
       $interval = _$interval_;
       $timeout = _$timeout_;
+      CrmAutosaveCtrl = _CrmAutosaveCtrl_;
 
       $rootScope.fakeCtrl = fakeCtrl = {
         doSave: function() {
@@ -74,7 +76,15 @@ describe('crmAutosave', function() {
     };
     angular.forEach(fakeSaves, function(saveFunc, saveFuncExpr) {
       it('calls ' + saveFuncExpr + ' twice over the course of three changes', function() {
-        element = $compile('<form name="myForm" crm-autosave="' + saveFuncExpr + '" crm-autosave-model="model" crm-autosave-interval="{poll: 25, save: 50}"><input class="fieldA" ng-model="model.fieldA"><input class="fieldB" ng-model="model.fieldB"></form>')($rootScope);
+        var myAutosave = $rootScope.myAutosave = new CrmAutosaveCtrl({
+          save: fakeCtrl[saveFunc],
+          model: function(){ return model; },
+          interval: {poll: 25, save: 50},
+          form: function(){ return $rootScope.myForm; }
+        });
+        myAutosave.start();
+        $rootScope.$on('$destroy', myAutosave.stop);
+        element = $compile('<form name="myForm"><input class="fieldA" ng-model="model.fieldA"><input class="fieldB" ng-model="model.fieldB"></form>')($rootScope);
         $rootScope.$digest();
 
         // check that we load pristine data and don't do any saving
@@ -122,7 +132,15 @@ describe('crmAutosave', function() {
     });
 
     it('does not save an invalid form', function() {
-      element = $compile('<form name="myForm" crm-autosave="fakeCtrl.doSave()" crm-autosave-model="model" crm-autosave-interval="{poll: 25, save: 50}"><input class="fieldA" ng-model="model.fieldA"><input class="fieldB" required ng-model="model.fieldB"></form>')($rootScope);
+      var myAutosave = $rootScope.myAutosave = new CrmAutosaveCtrl({
+        save: fakeCtrl.doSave,
+        model: function(){ return model; },
+        interval: {poll: 25, save: 50},
+        form: function(){ return $rootScope.myForm; }
+      });
+      myAutosave.start();
+      $rootScope.$on('$destroy', myAutosave.stop);
+      element = $compile('<form name="myForm"><input class="fieldA" ng-model="model.fieldA"><input class="fieldB" required ng-model="model.fieldB"></form>')($rootScope);
       $rootScope.$digest();
 
       // check that we load pristine data and don't do any saving
@@ -165,7 +183,15 @@ describe('crmAutosave', function() {
     });
 
     it('defers saving new changes when a save is already pending', function() {
-      element = $compile('<form name="myForm" crm-autosave="fakeCtrl.doSaveSlowly()" crm-autosave-model="model" crm-autosave-interval="{poll: 25, save: 50}"><input class="fieldA" ng-model="model.fieldA"><input class="fieldB" ng-model="model.fieldB"></form>')($rootScope);
+      var myAutosave = $rootScope.myAutosave = new CrmAutosaveCtrl({
+        save: fakeCtrl.doSaveSlowly,
+        model: function(){ return model; },
+        interval: {poll: 25, save: 50},
+        form: function(){ return $rootScope.myForm; }
+      });
+      myAutosave.start();
+      $rootScope.$on('$destroy', myAutosave.stop);
+      element = $compile('<form name="myForm"><input class="fieldA" ng-model="model.fieldA"><input class="fieldB" ng-model="model.fieldB"></form>')($rootScope);
       $rootScope.$digest();
 
       // check that we load pristine data and don't do any saving