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 c31bb4004947..f1869efa1d48 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -37,6 +37,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
@@ -117,6 +123,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
@@ -192,6 +203,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
@@ -268,6 +284,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
@@ -334,6 +355,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
@@ -384,6 +408,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
@@ -454,7 +483,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.
@@ -472,16 +501,27 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}
- var listener = function() {
- if (composing) return;
- var value = element.val();
+ var timeout = null,
+ eventList,
+ updateTimeout,
+ updateDefaultTimeout;
- // By default we will trim the value
- // If the attribute ng-trim exists we will avoid trimming
- // e.g.
- if (toBoolean(attr.ngTrim || 'T')) {
- value = trim(value);
- }
+ 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 = getValue();
if (ctrl.$viewValue !== value ||
// If the value is still empty/falsy, and there is no `required` error, run validators
@@ -498,41 +538,82 @@ 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 value = element.val();
- element.on('keydown', function(event) {
- var key = event.keyCode;
+ // By default we will trim the value
+ // If the attribute ng-trim exists we will avoid trimming
+ // e.g.
+ if (toBoolean(attr.ngTrim || 'T')) {
+ value = trim(value);
+ }
+
+ var callbackTimeout = (!isEmpty(updateTimeout))
+ ? updateTimeout[event.type] || updateTimeout['default'] || updateDefaultTimeout || 0
+ : updateDefaultTimeout || 0;
- // ignore
- // command modifiers arrows
- if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
+ if (callbackTimeout>0) {
+ timeout = $timeout(update, callbackTimeout, false, timeout);
+ }
+ else {
+ update();
+ }
+ };
- deferListener();
+ var deferListener = function(ev) {
+ $browser.defer(function() {
+ listener(ev);
});
+ };
- // 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);
- }
+ var defaultEvents = true;
+
+ // 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 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 (defaultEvents) {
+
+ // 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 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 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);
+ }
+ }
+ }
ctrl.$render = function() {
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
@@ -593,8 +674,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
}
-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);
@@ -638,8 +719,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);
@@ -649,8 +730,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);
@@ -660,18 +741,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() {
@@ -682,17 +776,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() {
@@ -852,14 +990,14 @@ function checkboxInputType(scope, element, attr, ctrl) {
*/
-var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
+var inputDirective = ['$browser', '$sniffer', '$timeout', function($browser, $sniffer, $timeout) {
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);
+ 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);
}
}
};
diff --git a/src/ng/directive/ngUpdateModel.js b/src/ng/directive/ngUpdateModel.js
new file mode 100644
index 000000000000..6fb137bd740f
--- /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 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/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index e3e50e02a69e..9b0c90a266dd 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('');