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._);
$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() {
};
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
});
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
});
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