From: Tim Otten Date: Wed, 18 Feb 2015 19:52:30 +0000 (-0800) Subject: crmAutosave - Rewrite as service/class instead of directive. Expose internal API. X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=06e1fe0ae89202d6b5a7837d2f30cc4297a2ad3b;p=civicrm-core.git crmAutosave - Rewrite as service/class instead of directive. Expose internal API. --- diff --git a/js/angular-crmAutosave.js b/js/angular-crmAutosave.js index 8b5a24c56a..f492cb9739 100644 --- a/js/angular-crmAutosave.js +++ b/js/angular-crmAutosave.js @@ -3,102 +3,109 @@ angular.module('crmAutosave', ['crmUtil']); - // usage:
...
- // - // 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._); diff --git a/tests/karma/unit/crmAutosaveSpec.js b/tests/karma/unit/crmAutosaveSpec.js index fc5926aefe..2d06f1fbb2 100644 --- a/tests/karma/unit/crmAutosaveSpec.js +++ b/tests/karma/unit/crmAutosaveSpec.js @@ -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('
')($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('
')($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('
')($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('
')($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('
')($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('
')($rootScope); $rootScope.$digest(); // check that we load pristine data and don't do any saving