diff --git a/angularFiles.js b/angularFiles.js
index 1647ba48481a..ffe6dc7ef936 100755
--- a/angularFiles.js
+++ b/angularFiles.js
@@ -63,6 +63,7 @@ angularFiles = {
'src/ng/directive/ngStyle.js',
'src/ng/directive/ngSwitch.js',
'src/ng/directive/ngTransclude.js',
+ 'src/ng/directive/ngUpdateModel.js',
'src/ng/directive/script.js',
'src/ng/directive/select.js',
'src/ng/directive/style.js'
diff --git a/docs/content/guide/forms.ngdoc b/docs/content/guide/forms.ngdoc
index 0b91fc61f8ee..62ce6744f9d0 100644
--- a/docs/content/guide/forms.ngdoc
+++ b/docs/content/guide/forms.ngdoc
@@ -180,6 +180,46 @@ This allows us to extend the above example with these features:
+# Non-immediate (debounced) or custom triggered model updates
+
+By default, any change on the content will trigger a model update and form validation. You can override this behavior using the `ng-update-model-on`
+attribute to bind only to a comma-delimited list of events. I.e. `ng-update-model-on="blur"` will update and validate only after the control loses
+focus.
+
+If you want to keep the default behavior and just add new events that may trigger the model update
+and validation, add "default" as one of the specified events. I.e. `ng-update-model-on="default,mousedown"`
+
+You can delay the model update/validation using `ng-update-model-debounce`. I.e. `ng-update-model-debounce="500"` will wait for half a second since
+the last content change before triggering the model update and form validation. This debouncing feature is not available on radio buttons.
+
+Custom debouncing timeouts can be set for each event for each event if you use an object in `ng-update-model-on`.
+I.e. `ng-update-model-on="{default: 500, blur: 0}"`
+
+Using the object notation allows any valid Angular expression to be used inside, including data and function calls from the scope.
+
+If those attributes are added to an element, they will be applied to all the child elements and controls that inherit from it unless they are
+overriden.
+
+The following example shows how to override immediate updates. Changes on the inputs within the form will update the model
+only when the control loses focus (blur event).
+
+
+
+
+
+
model = {{user | json}}
+
+
+
+
+
# Custom Validation
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index 0c02adeca685..67da68e7b5b5 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -48,6 +48,8 @@
ngValueDirective,
ngAttributeAliasDirectives,
ngEventDirectives,
+ ngUpdateModelOnDirective,
+ ngUpdateModelDebounceDirective,
$AnchorScrollProvider,
$AnimateProvider,
@@ -183,6 +185,8 @@ function publishExternalAPI(angular){
ngChange: ngChangeDirective,
required: requiredDirective,
ngRequired: requiredDirective,
+ ngUpdateModelOn: ngUpdateModelOnDirective,
+ ngUpdateModelDebounce: ngUpdateModelDebounceDirective,
ngValue: ngValueDirective
}).
directive({
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index cb432c52a5c7..75103f52e29e 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -42,6 +42,12 @@ var inputType = {
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered
+ * content change before triggering a model update (debouncing).
+ * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events
+ * that will trigger a model update. If it is not set, it defaults to any inmediate change. If
+ * the list contains "default", the original behavior is also kept. You can also specify an
+ * object in which the key is the event and the value the particular timeout to be applied to it.
*
* @example
@@ -541,6 +547,11 @@ var inputType = {
* patterns defined as scope expressions.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
+ * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered
+ * content change before triggering a model update.
+ * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events
+ * that will trigger a model update. If it is not set, it defaults to any inmediate change. If
+ * the list contains "default", the original behavior is also kept.
*
* @example
@@ -616,6 +627,11 @@ var inputType = {
* patterns defined as scope expressions.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
+ * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered
+ * content change before triggering a model update.
+ * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events
+ * that will trigger a model update. If it is not set, it defaults to any inmediate change. If
+ * the list contains "default", the original behavior is also kept.
*
* @example
@@ -692,6 +708,11 @@ var inputType = {
* patterns defined as scope expressions.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
+ * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered
+ * content change before triggering a model update.
+ * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events
+ * that will trigger a model update. If it is not set, it defaults to any inmediate change. If
+ * the list contains "default", the original behavior is also kept.
*
* @example
@@ -758,6 +779,9 @@ var inputType = {
* interaction with the input element.
* @param {string} ngValue Angular expression which sets the value to which the expression should
* be set when selected.
+ * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events
+ * that will trigger a model update. If it is not set, it defaults to any inmediate change. If
+ * the list contains "default", the original behavior is also kept.
*
* @example
@@ -808,6 +832,11 @@ var inputType = {
* @param {string=} ngFalseValue The value to which the expression should be set when not selected.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
+ * @param {integer=} ngUpdateModelDebounce Time in milliseconds to wait since the last registered
+ * content change before triggering a model update.
+ * @param {string=} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events
+ * that will trigger a model update. If it is not set, it defaults to any inmediate change. If
+ * the list contains "default", the original behavior is also kept.
*
* @example
@@ -878,7 +907,7 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
}
}
-function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+function textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) {
var validity = element.prop('validity');
// In composition mode, users are still inputing intermediate text buffer,
// hold the listener until composition is done.
@@ -896,8 +925,26 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}
- var listener = function() {
- if (composing) return;
+ var timeout = null,
+ eventList,
+ updateTimeout,
+ updateDefaultTimeout;
+
+ var isEmpty = function(value) {
+ return isUndefined(value) || value === '' || value === null || value !== value;
+ };
+
+ // Get update model details from controllers
+ if (isDefined(updModOnCtrl)) {
+ eventList = updModOnCtrl.$getEventList();
+ updateTimeout = updModOnCtrl.$getDebounceTimeout();
+ }
+
+ if (isDefined(updModlTimCtrl)) {
+ updateDefaultTimeout = updModlTimCtrl.$getDefaultTimeout();
+ }
+
+ var update = function() {
var value = element.val();
// By default we will trim the value
@@ -922,41 +969,73 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
};
- // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
- // input event on backspace, delete or cut
- if ($sniffer.hasEvent('input')) {
- element.on('input', listener);
- } else {
- var timeout;
+ var listener = function(event) {
+ if (composing) return;
- var deferListener = function() {
- if (!timeout) {
- timeout = $browser.defer(function() {
- listener();
- timeout = null;
- });
- }
- };
+ var callbackTimeout = (!isEmpty(updateTimeout))
+ ? updateTimeout[event.type] || updateTimeout['default'] || updateDefaultTimeout || 0
+ : updateDefaultTimeout || 0;
- element.on('keydown', function(event) {
- var key = event.keyCode;
+ if (callbackTimeout>0) {
+ timeout = $timeout(update, callbackTimeout, false, timeout);
+ }
+ else {
+ update();
+ }
+ };
- // ignore
- // command modifiers arrows
- if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
+ var deferListener = function(ev) {
+ $browser.defer(function() {
+ listener(ev);
+ });
+ };
+
+ var defaultEvents = true;
- deferListener();
+ // Allow adding/overriding bound events
+ if (!isEmpty(eventList)) {
+ defaultEvents = false;
+ // bind to user-defined events
+ forEach(eventList.split(','), function(ev) {
+ ev = trim(ev).toLowerCase();
+ if (ev === 'default') {
+ defaultEvents = true;
+ }
+ else {
+ element.on(ev, listener);
+ }
});
+ }
+
+ if (defaultEvents) {
- // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
- if ($sniffer.hasEvent('paste')) {
- element.on('paste cut', deferListener);
+ // default behavior: bind to input events or keydown+change
+
+ // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
+ // input event on backspace, delete or cut
+ if ($sniffer.hasEvent('input')) {
+ element.bind('input', listener);
+ } else {
+ element.on('keydown', function(event) {
+ var key = event.keyCode;
+
+ // ignore
+ // command modifiers arrows
+ if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
+
+ deferListener('keydown');
+ });
+
+ // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
+ if ($sniffer.hasEvent('paste')) {
+ element.on('paste cut', deferListener);
+ }
}
- }
- // if user paste into input using mouse on older browser
- // or form autocomplete on newer browser, we need "change" event to catch it
- element.on('change', listener);
+ // if user paste into input using mouse on older browser
+ // or form autocomplete on newer browser, we need "change" event to catch it
+ element.on('change', listener);
+ }
ctrl.$render = function() {
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
@@ -1068,8 +1147,8 @@ function createDateParser(regexp, mapping) {
}
function createDateInputType(type, regexp, parseDate, format) {
- return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
- textInputType(scope, element, attr, ctrl, $sniffer, $browser);
+ return function dynamicDateInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser, $filter) {
+ textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser);
ctrl.$parsers.push(function(value) {
if(ctrl.$isEmpty(value)) {
@@ -1119,8 +1198,8 @@ function createDateInputType(type, regexp, parseDate, format) {
};
}
-function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- textInputType(scope, element, attr, ctrl, $sniffer, $browser);
+function numberInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) {
+ textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser);
ctrl.$parsers.push(function(value) {
var empty = ctrl.$isEmpty(value);
@@ -1164,8 +1243,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}
-function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- textInputType(scope, element, attr, ctrl, $sniffer, $browser);
+function urlInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) {
+ textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser);
var urlValidator = function(value) {
return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value);
@@ -1175,8 +1254,8 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
ctrl.$parsers.push(urlValidator);
}
-function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- textInputType(scope, element, attr, ctrl, $sniffer, $browser);
+function emailInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser) {
+ textInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout, $sniffer, $browser);
var emailValidator = function(value) {
return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value);
@@ -1186,18 +1265,31 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
ctrl.$parsers.push(emailValidator);
}
-function radioInputType(scope, element, attr, ctrl) {
- // make the name unique, if not defined
- if (isUndefined(attr.name)) {
- element.attr('name', nextUid());
- }
+function radioInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout) {
- element.on('click', function() {
+ // Get update model details from controllers
+ var eventList = (isDefined(updModOnCtrl)) ? updModOnCtrl.$getEventList() : 'click';
+
+ var listener = function() {
if (element[0].checked) {
scope.$apply(function() {
ctrl.$setViewValue(attr.value);
});
}
+ };
+
+ // make the name unique, if not defined
+ if (isUndefined(attr.name)) {
+ element.attr('name', nextUid());
+ }
+
+ // bind to user-defined/default events
+ forEach(eventList.split(','), function(ev) {
+ ev = trim(ev).toLowerCase();
+ if (ev === 'default') {
+ ev = 'click';
+ }
+ element.bind(ev, listener);
});
ctrl.$render = function() {
@@ -1208,17 +1300,61 @@ function radioInputType(scope, element, attr, ctrl) {
attr.$observe('value', ctrl.$render);
}
-function checkboxInputType(scope, element, attr, ctrl) {
- var trueValue = attr.ngTrueValue,
- falseValue = attr.ngFalseValue;
+function checkboxInputType(scope, element, attr, ctrl, updModOnCtrl, updModlTimCtrl, $timeout) {
+ var timeout = null,
+ trueValue = attr.ngTrueValue,
+ falseValue = attr.ngFalseValue,
+ eventList,
+ updateDefaultTimeout,
+ updateTimeout;
+
+ // Get update model details from controllers
+ eventList = 'click';
+
+ // Get update model details from controllers
+ if (isDefined(updModOnCtrl)) {
+ eventList = updModOnCtrl.$getEventList();
+ updateTimeout = updModOnCtrl.$getDebounceTimeout();
+ }
- if (!isString(trueValue)) trueValue = true;
- if (!isString(falseValue)) falseValue = false;
+ if (isDefined(updModlTimCtrl)) {
+ updateDefaultTimeout = updModlTimCtrl.$getDefaultTimeout();
+ }
- element.on('click', function() {
+ var update = function() {
scope.$apply(function() {
ctrl.$setViewValue(element[0].checked);
});
+ };
+
+ var listener = function(event) {
+
+ var isEmpty = function(value) {
+ return isUndefined(value) || value === '' || value === null || value !== value;
+ };
+
+ var callbackTimeout = (!isEmpty(updateTimeout))
+ ? updateTimeout[event.type] || updateTimeout['default'] || updateDefaultTimeout || 0
+ : updateDefaultTimeout || 0;
+
+ if (callbackTimeout>0) {
+ timeout = $timeout(update, callbackTimeout, false, timeout);
+ }
+ else {
+ update();
+ }
+ };
+
+ if (!isString(trueValue)) trueValue = true;
+ if (!isString(falseValue)) falseValue = false;
+
+ // bind to user-defined/default events
+ forEach(eventList.split(','), function(ev) {
+ ev = trim(ev).toLowerCase();
+ if (ev === 'default') {
+ ev = 'click';
+ }
+ element.bind(ev, listener);
});
ctrl.$render = function() {
@@ -1378,14 +1514,15 @@ function checkboxInputType(scope, element, attr, ctrl) {
*/
-var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sniffer, $filter) {
+
+var inputDirective = ['$browser', '$sniffer', '$timeout', '$filter', function($browser, $sniffer, $timeout, $filter) {
return {
restrict: 'E',
- require: '?ngModel',
- link: function(scope, element, attr, ctrl) {
- if (ctrl) {
- (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
- $browser, $filter);
+ require: ['?ngModel', '^?ngUpdateModelOn', '^?ngUpdateModelDebounce'],
+ link: function(scope, element, attr, ctrls) {
+ if (ctrls[0]) {
+ (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], ctrls[1], ctrls[2], $timeout,
+ $sniffer, $browser, $filter);
}
}
};
diff --git a/src/ng/directive/ngUpdateModel.js b/src/ng/directive/ngUpdateModel.js
new file mode 100644
index 000000000000..f9e1b97fd373
--- /dev/null
+++ b/src/ng/directive/ngUpdateModel.js
@@ -0,0 +1,97 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name ng.directive:ngUpdateModelOn
+ * @restrict A
+ *
+ * @description
+ * The `ngUpdateModelOn` directive changes default behavior of model updates. You can customize
+ * which events will be bound to the `input` elements so that the model update will
+ * only be triggered when they occur.
+ *
+ * This option will be applicable to those `input` elements that descend from the
+ * element containing the directive. So, if you use `ngUpdateModelOn` on a `form`
+ * element, the default behavior will be used on the `input` elements within.
+ *
+ * See {@link guide/forms this link} for more information about debouncing and custom
+ * events.
+ *
+ * @element ANY
+ * @param {string} ngUpdateModelOn Allows specifying an event or a comma-delimited list of events
+ * that will trigger a model update. If it is not set, it defaults to any inmediate change. If
+ * the list contains "default", the original behavior is also kept. You can also specify an
+ * object in which the key is the event and the value the particular debouncing timeout to be
+ * applied to it.
+ */
+
+var SIMPLEOBJECT_TEST = /^\s*?\{(.*)\}\s*?$/;
+
+var NgUpdateModelOnController = ['$attrs', '$scope',
+ function UpdateModelOnController($attrs, $scope) {
+
+ var attr = $attrs['ngUpdateModelOn'];
+ var updateModelOnValue;
+ var updateModelDebounceValue;
+
+ if (SIMPLEOBJECT_TEST.test(attr)) {
+ updateModelDebounceValue = $scope.$eval(attr);
+ var keys = [];
+ for(var k in updateModelDebounceValue) {
+ keys.push(k);
+ }
+ updateModelOnValue = keys.join(',');
+ }
+ else {
+ updateModelOnValue = attr;
+ }
+
+ this.$getEventList = function() {
+ return updateModelOnValue;
+ };
+
+ this.$getDebounceTimeout = function() {
+ return updateModelDebounceValue;
+ };
+}];
+
+var ngUpdateModelOnDirective = [function() {
+ return {
+ restrict: 'A',
+ controller: NgUpdateModelOnController
+ };
+}];
+
+
+/**
+ * @ngdoc directive
+ * @name ng.directive:ngUpdateModelDebounce
+ * @restrict A
+ *
+ * @description
+ * The `ngUpdateModelDebounce` directive allows specifying a debounced timeout to model updates so they
+ * are not triggerer instantly but after the timer has expired.
+ *
+ * If you need to specify different timeouts for each event, you can use
+ * {@link module:ng.directive:ng.directive:ngUpdateModelOn ngUpdateModelOn} directive which the object notation.
+ *
+ * @element ANY
+ * @param {integer} ngUpdateModelDebounce Time in milliseconds to wait since the last registered
+ * content change before triggering a model update.
+ */
+var NgUpdateModelDebounceController = ['$attrs',
+ function UpdateModelDebounceController($attrs) {
+
+ var updateModelDefaultTimeoutValue = $attrs['ngUpdateModelDebounce'];
+
+ this.$getDefaultTimeout = function() {
+ return updateModelDefaultTimeoutValue;
+ };
+}];
+
+var ngUpdateModelDebounceDirective = [function() {
+ return {
+ restrict: 'A',
+ controller: NgUpdateModelDebounceController
+ };
+}];
diff --git a/src/ng/timeout.js b/src/ng/timeout.js
index 33a4dcde81ed..5c2152cdac05 100644
--- a/src/ng/timeout.js
+++ b/src/ng/timeout.js
@@ -24,20 +24,48 @@ function $TimeoutProvider() {
* In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
* synchronously flush the queue of deferred functions.
*
+ * You can also use `$timeout` to debounce the call of a function using the returned promise
+ * as the fourth parameter in the next call. See the following example:
+ *
+ * ```js
+ * var debounce;
+ * var doRealSave = function() {
+ * // Save model to DB
+ * }
+ * $scope.save = function() {
+ * // debounce call for 2 seconds
+ * debounce = $timeout(doRealSave, 2000, false, debounce);
+ * }
+ * ```
+ *
+ * And in the form:
+ *
+ * ```html
+ *
+ * ```
+ *
* @param {function()} fn A function, whose execution should be delayed.
* @param {number=} [delay=0] Delay in milliseconds.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @param {Promise=} debounce If set to an outgoing promise, it will reject it before creating
+ * the new one. This allows debouncing the execution of the function.
* @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
* promise will be resolved with is the return value of the `fn` function.
*
*/
- function timeout(fn, delay, invokeApply) {
+ function timeout(fn, delay, invokeApply, debounce) {
var deferred = $q.defer(),
promise = deferred.promise,
skipApply = (isDefined(invokeApply) && !invokeApply),
timeoutId;
+ // debouncing support
+ if (debounce && debounce.$$timeoutId in deferreds) {
+ deferreds[debounce.$$timeoutId].reject('debounced');
+ $browser.defer.cancel(debounce.$$timeoutId);
+ }
+
timeoutId = $browser.defer(function() {
try {
deferred.resolve(fn());
diff --git a/src/ngScenario/browserTrigger.js b/src/ngScenario/browserTrigger.js
index f74a04c15110..c44713fd33dd 100644
--- a/src/ngScenario/browserTrigger.js
+++ b/src/ngScenario/browserTrigger.js
@@ -65,7 +65,7 @@
}
if (msie < 9) {
- if (inputType == 'radio' || inputType == 'checkbox') {
+ if ((inputType == 'radio' || inputType == 'checkbox') && (eventType == 'click')) {
element.checked = !element.checked;
}
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index eba3028e7bce..b58dd1b6b80f 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -608,6 +608,175 @@ describe('input', function() {
});
+ describe('ng-update-model attributes', function() {
+
+ it('should allow overriding the model update trigger event on text inputs', function() {
+ compileInput('');
+
+ changeInputValueTo('a');
+ expect(scope.name).toBeUndefined();
+ browserTrigger(inputElm, 'blur');
+ expect(scope.name).toEqual('a');
+ });
+
+
+ it('should bind the element to a list of events on text inputs', function() {
+ compileInput('');
+
+ changeInputValueTo('a');
+ expect(scope.name).toBeUndefined();
+ browserTrigger(inputElm, 'blur');
+ expect(scope.name).toEqual('a');
+
+ changeInputValueTo('b');
+ expect(scope.name).toEqual('a');
+ browserTrigger(inputElm, 'mousemove');
+ expect(scope.name).toEqual('b');
+ });
+
+
+ it('should allow keeping the default update behavior on text inputs', function() {
+ compileInput('');
+
+ changeInputValueTo('a');
+ expect(scope.name).toEqual('a');
+ });
+
+
+ it('should allow overriding the model update trigger event on checkboxes', function() {
+ compileInput('');
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(undefined);
+
+ browserTrigger(inputElm, 'blur');
+ expect(scope.checkbox).toBe(true);
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(true);
+ });
+
+
+ it('should allow keeping the default update behavior on checkboxes', function() {
+ compileInput('');
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(true);
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(false);
+ });
+
+
+ it('should allow overriding the model update trigger event on radio buttons', function() {
+ compileInput(
+ '' +
+ '' +
+ '');
+
+ scope.$apply(function() {
+ scope.color = 'white';
+ });
+ browserTrigger(inputElm[2], 'click');
+ expect(scope.color).toBe('white');
+
+ browserTrigger(inputElm[2], 'blur');
+ expect(scope.color).toBe('blue');
+
+ });
+
+
+ it('should allow keeping the default update behavior on radio buttons', function() {
+ compileInput(
+ '' +
+ '' +
+ '');
+
+ scope.$apply(function() {
+ scope.color = 'white';
+ });
+ browserTrigger(inputElm[2], 'click');
+ expect(scope.color).toBe('blue');
+ });
+
+
+ it('should trigger only after timeout in text inputs', inject(function($timeout) {
+ compileInput('');
+
+ changeInputValueTo('a');
+ changeInputValueTo('b');
+ changeInputValueTo('c');
+ expect(scope.name).toEqual(undefined);
+ $timeout.flush();
+ expect(scope.name).toEqual('c');
+ }));
+
+
+ it('should trigger only after timeout in checkboxes', inject(function($timeout) {
+ compileInput('');
+
+ browserTrigger(inputElm, 'click');
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(undefined);
+ $timeout.flush();
+ expect(scope.checkbox).toBe(false);
+ }));
+
+
+ it('should allow selecting different debounce timeouts for each event', inject(function($timeout) {
+ compileInput('');
+
+ changeInputValueTo('a');
+ expect(scope.checkbox).toBe(undefined);
+ $timeout.flush(4000);
+ expect(scope.checkbox).toBe(undefined);
+ $timeout.flush(7000);
+ expect(scope.name).toEqual('a');
+ changeInputValueTo('b');
+ browserTrigger(inputElm, 'blur');
+ $timeout.flush(4000);
+ expect(scope.name).toEqual('a');
+ $timeout.flush(2000);
+ expect(scope.name).toEqual('b');
+ }));
+
+
+ it('should allow selecting different debounce timeouts for each event on checkboxes', inject(function($timeout) {
+ compileInput('');
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(undefined);
+ $timeout.flush(8000);
+ expect(scope.checkbox).toBe(undefined);
+ $timeout.flush(3000);
+ expect(scope.checkbox).toBe(true);
+ browserTrigger(inputElm, 'click');
+ browserTrigger(inputElm, 'blur');
+ $timeout.flush(3000);
+ expect(scope.checkbox).toBe(true);
+ $timeout.flush(3000);
+ expect(scope.checkbox).toBe(false);
+
+ }));
+
+
+ it('should inherit model update settings from ancestor elements', inject(function($timeout) {
+ var doc = $compile('')(scope);
+
+ var input = doc.find('input').eq(0);
+ input.val('a');
+ expect(scope.name).toEqual(undefined);
+ browserTrigger(input, 'blur');
+ expect(scope.name).toBe(undefined);
+ $timeout.flush();
+ expect(scope.name).toEqual('a');
+ dealoc(doc);
+ }));
+
+ });
+
+
it('should allow complex reference binding', function() {
compileInput('');
diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js
index 97c8448eedce..02f31de9fbd9 100644
--- a/test/ng/timeoutSpec.js
+++ b/test/ng/timeoutSpec.js
@@ -213,4 +213,34 @@ describe('$timeout', function() {
expect(cancelSpy).toHaveBeenCalledOnce();
}));
});
+
+
+ describe('debouncing', function() {
+ it('should allow debouncing tasks', inject(function($timeout) {
+ var task = jasmine.createSpy('task'),
+ successtask = jasmine.createSpy('successtask'),
+ errortask = jasmine.createSpy('errortask'),
+ promise = null;
+
+ promise = $timeout(task, 10000, true, promise);
+ promise.then(successtask, errortask);
+
+ expect(task).not.toHaveBeenCalled();
+ expect(successtask).not.toHaveBeenCalled();
+ expect(errortask).not.toHaveBeenCalled();
+
+ promise = $timeout(task, 10000, true, promise);
+ expect(task).not.toHaveBeenCalled();
+ expect(successtask).not.toHaveBeenCalled();
+ expect(errortask).not.toHaveBeenCalled();
+
+ $timeout.flush();
+
+ expect(task).toHaveBeenCalled();
+ // it's a different promise, so 'successtask' should not be called but 'errortask' should
+ expect(successtask).not.toHaveBeenCalled();
+ expect(errortask).toHaveBeenCalled();
+ }));
+
+ });
});