feat(ngModelOptions): custom triggers and debounce of ngModel updates

By default, any change to an input will trigger an immediate model update,
form validation and run a $digest. This is not always desirable, especially
when you have a large number of bindings to update.

This PR implements a new directive `ngModelOptions`, which allow you to
override this default behavior in several ways. It is implemented as an
attribute, to which you pass an Angular expression, which evaluates to an
**options** object.

All inputs, using ngModel, will search for this directive in their ancestors
and use it if found.  This makes it easy to provide options for a whole
form or even the whole page, as well as specifying exceptions for
individual inputs.

* You can specify what events trigger an update to the model by providing
  an `updateOn` property on the **options** object. This property takes a
  string containing a space separated list of events.

  For example, `ng-model-options="{ updateOn: 'blur' }"` will update the
  model only after the input loses focus.

  There is a special pseudo-event, called "default", which maps to the
  default event used by the input box normally. This is useful if you
  want to keep the default behavior and just add new events.

* You can specify a debounce delay, how long to wait after the last triggering
  event before updating the model, by providing a `debounce` property on
  the **options** object.

  This property can be a simple number, the
  debounce delay for all events. For example,
  `ng-model-options="{ debounce: 500 }" will ensure the model is updated
  only when there has been a period 500ms since the last triggering event.

  The property can also be an object, where the keys map to events and
  the values are a corresponding debounce delay for that event.
  This can be useful to force immediate updates on some specific
  circumstances (like blur events). For example,
  `ng-model-options="{ updateOn: 'default blur', debounce: { default: 500, blur: 0} }"`

This commit also brings to an end one of the longest running Pull Requests
in the history of AngularJS (#2129)!  A testament to the patience of @lrlopez.

Closes #1285, #2129, #6945
This commit is contained in:
Luis Ramón López
2014-04-03 22:01:34 +02:00
committed by Peter Bacon Darwin
parent e55c8bcbca
commit dbe381f29f
4 changed files with 573 additions and 68 deletions

View File

@@ -181,6 +181,82 @@ This allows us to extend the above example with these features:
# Custom triggers
By default, any change to the content will trigger a model update and form validation. You can
override this behavior using the {@link ng.directive:ngModelOptions ngModelOptions} directive to
bind only to specified list of events. I.e. `ng-model-options="{ updateOn: "blur" }"` will update
and validate only after the control loses focus. You can set several events using a space delimited
list. I.e. `ng-model-options="{ updateOn: 'mousedown blur' }"`
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-model-options="{ updateOn: 'default blur' }"`
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).
<example>
<file name="index.html">
<div ng-controller="ControllerUpdateOn">
<form>
Name:
<input type="text" ng-model="user.name" ng-model-options="{ updateOn: "blur" }" /><br />
Other data:
<input type="text" ng-model="user.data" /><br />
</form>
<pre>username = "{{user.name}}"</pre>
</div>
</file>
<file name="script.js">
function ControllerUpdateOn($scope) {
$scope.user = {};
}
</file>
</example>
# Non-immediate (debounced) model updates
You can delay the model update/validation by using the `debounce` key with the
{@link ng.directive:ngModelOptions ngModelOptions} directive. This delay will also apply to
parsers, validators and model flags like `$dirty` or `$pristine`.
I.e. `ng-model-options="{ debounce: 500 }"` will wait for half a second since
the last content change before triggering the model update and form validation.
If custom triggers are used, custom debouncing timeouts can be set for each event using an object
in `debounce`. This can be useful to force immediate updates on some specific circumstances
(like blur events).
I.e. `ng-model-options="{ updateOn: 'default blur', debounce: { default: 500, blur: 0 } }"`
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
overridden.
This example shows how to debounce model changes. Model will be updated only 250 milliseconds after last change.
<example>
<file name="index.html">
<div ng-controller="ControllerUpdateOn">
<form>
Name:
<input type="text" ng-model="user.name" ng-model-options="{ debounce: 250 }" /><br />
</form>
<pre>username = "{{user.name}}"</pre>
</div>
</file>
<file name="script.js">
function ControllerUpdateOn($scope) {
$scope.user = {};
}
</file>
</example>
# Custom Validation
Angular provides basic implementation for most common html5 {@link ng.directive:input input}

View File

@@ -46,6 +46,7 @@
requiredDirective,
requiredDirective,
ngValueDirective,
ngModelOptionsDirective,
ngAttributeAliasDirectives,
ngEventDirectives,
@@ -183,7 +184,8 @@ function publishExternalAPI(angular){
ngChange: ngChangeDirective,
required: requiredDirective,
ngRequired: requiredDirective,
ngValue: ngValueDirective
ngValue: ngValueDirective,
ngModelOptions: ngModelOptionsDirective
}).
directive({
ngInclude: ngIncludeFillContentDirective

View File

@@ -16,6 +16,7 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)$/;
var DEFAULT_REGEXP = /(\b|^)default(\b|$)/;
var inputType = {
@@ -879,6 +880,7 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var validity = element.prop('validity');
// In composition mode, users are still inputing intermediate text buffer,
// hold the listener until composition is done.
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
@@ -895,9 +897,10 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}
var listener = function() {
var listener = function(ev) {
if (composing) return;
var value = element.val();
var value = element.val(),
event = ev && ev.type;
// By default we will trim the value
// If the attribute ng-trim exists we will avoid trimming
@@ -912,50 +915,59 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// even when the first character entered causes an error.
(validity && value === '' && !validity.valueMissing)) {
if (scope.$$phase) {
ctrl.$setViewValue(value);
ctrl.$setViewValue(value, event);
} else {
scope.$apply(function() {
ctrl.$setViewValue(value);
ctrl.$setViewValue(value, event);
});
}
}
};
// 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 deferListener = function() {
if (!timeout) {
timeout = $browser.defer(function() {
listener();
timeout = null;
});
}
};
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();
});
// 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);
}
// Allow adding/overriding bound events
if (ctrl.$options && ctrl.$options.updateOn) {
// bind to user-defined events
element.on(ctrl.$options.updateOn, 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);
// setup default events if requested
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
// 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 deferListener = function(ev) {
if (!timeout) {
timeout = $browser.defer(function() {
listener(ev);
timeout = null;
});
}
};
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(event);
});
// 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);
}
ctrl.$render = function() {
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
@@ -1191,13 +1203,23 @@ function radioInputType(scope, element, attr, ctrl) {
element.attr('name', nextUid());
}
element.on('click', function() {
var listener = function(ev) {
if (element[0].checked) {
scope.$apply(function() {
ctrl.$setViewValue(attr.value);
ctrl.$setViewValue(attr.value, ev && ev.type);
});
}
});
};
// Allow adding/overriding bound events
if (ctrl.$options && ctrl.$options.updateOn) {
// bind to user-defined events
element.on(ctrl.$options.updateOn, listener);
}
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
element.on('click', listener);
}
ctrl.$render = function() {
var value = attr.value;
@@ -1214,11 +1236,21 @@ function checkboxInputType(scope, element, attr, ctrl) {
if (!isString(trueValue)) trueValue = true;
if (!isString(falseValue)) falseValue = false;
element.on('click', function() {
var listener = function(ev) {
scope.$apply(function() {
ctrl.$setViewValue(element[0].checked);
ctrl.$setViewValue(element[0].checked, ev && ev.type);
});
});
};
// Allow adding/overriding bound events
if (ctrl.$options && ctrl.$options.updateOn) {
// bind to user-defined events
element.on(ctrl.$options.updateOn, listener);
}
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
element.on('click', listener);
}
ctrl.$render = function() {
element[0].checked = ctrl.$viewValue;
@@ -1380,10 +1412,10 @@ function checkboxInputType(scope, element, attr, ctrl) {
var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sniffer, $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,
require: ['?ngModel'],
link: function(scope, element, attr, ctrls) {
if (ctrls[0]) {
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
$browser, $filter);
}
}
@@ -1529,8 +1561,8 @@ var VALID_CLASS = 'ng-valid',
*
*
*/
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate',
function($scope, $exceptionHandler, $attr, $element, $parse, $animate) {
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout',
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$parsers = [];
@@ -1542,8 +1574,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$invalid = false;
this.$name = $attr.name;
var ngModelGet = $parse($attr.ngModel),
ngModelSet = ngModelGet.assign;
ngModelSet = ngModelGet.assign,
pendingDebounce = null;
if (!ngModelSet) {
throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
@@ -1659,26 +1693,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
/**
* @ngdoc method
* @name ngModel.NgModelController#$setViewValue
* @name ngModel.NgModelController#$cancelDebounce
*
* @description
* Update the view value.
* Cancel a pending debounced update.
*
* This method should be called when the view value changes, typically from within a DOM event handler.
* For example {@link ng.directive:input input} and
* {@link ng.directive:select select} directives call it.
*
* It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
* which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
* `$modelValue` and the **expression** specified in the `ng-model` attribute.
*
* Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
*
* Note that calling this function does not trigger a `$digest`.
*
* @param {string} value Value from the view.
* This method should be called before directly update a debounced model from the scope in
* order to prevent unintended future changes of the model value because of a delayed event.
*/
this.$setViewValue = function(value) {
this.$cancelDebounce = function() {
if ( pendingDebounce ) {
$timeout.cancel(pendingDebounce);
pendingDebounce = null;
}
};
// update the view value
this.$$realSetViewValue = function(value) {
this.$viewValue = value;
// change to dirty
@@ -1707,6 +1738,48 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
}
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setViewValue
*
* @description
* Update the view value.
*
* This method should be called when the view value changes, typically from within a DOM event handler.
* For example {@link ng.directive:input input} and
* {@link ng.directive:select select} directives call it.
*
* It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
* which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
* `$modelValue` and the **expression** specified in the `ng-model` attribute.
*
* Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
*
* All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
* directive is used with a custom debounce for this particular event.
*
* Note that calling this function does not trigger a `$digest`.
*
* @param {string} value Value from the view.
* @param {string} trigger Event that triggered the update.
*/
this.$setViewValue = function(value, trigger) {
var that = this;
var debounceDelay = this.$options && (isObject(this.$options.debounce)
? (this.$options.debounce[trigger] || this.$options.debounce['default'] || 0)
: this.$options.debounce) || 0;
that.$cancelDebounce();
if ( debounceDelay ) {
pendingDebounce = $timeout(function() {
pendingDebounce = null;
that.$$realSetViewValue(value);
}, debounceDelay);
} else {
that.$$realSetViewValue(value);
}
};
// model -> value
var ctrl = this;
@@ -1844,7 +1917,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
*/
var ngModelDirective = function() {
return {
require: ['ngModel', '^?form'],
require: ['ngModel', '^?form', '^?ngModelOptions'],
controller: NgModelController,
link: function(scope, element, attr, ctrls) {
// notify others, especially parent forms
@@ -1854,6 +1927,11 @@ var ngModelDirective = function() {
formCtrl.$addControl(modelCtrl);
// Pass the ng-model-options to the ng-model controller
if ( ctrls[2] ) {
modelCtrl.$options = ctrls[2].$options;
}
scope.$on('$destroy', function() {
formCtrl.$removeControl(modelCtrl);
});
@@ -2122,3 +2200,95 @@ var ngValueDirective = function() {
}
};
};
/**
* @ngdoc directive
* @name ngModelOptions
*
* @description
* Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of events
* that will trigger a model update and/or a debouncing delay so that the actual update only takes place
* when a timer expires; this timer will be reset after another change takes place.
*
* @param {Object=} Object that contains options to apply to the current model. Valid keys are:
* - updateOn: string specifying which event should be the input bound to. You can set several events
* using an space delimited list. There is a special event called `default` that
* matches the default events belonging of the control.
* - debounce: integer value which contains the debounce model update value in milliseconds. A value of 0
* triggers an immediate update. If an object is supplied instead, you can specify a custom value
* for each event. I.e.
* `ngModelOptions="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
*
* @example
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).
<example name="ngModelOptions-directive-1">
<file name="index.html">
<script>
function Ctrl($scope) {
$scope.user = { name: 'say', data: '' };
}
</script>
<div ng-controller="Ctrl">
<form>
Name:
<input type="text" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" name="uName" /><br />
Other data:
<input type="text" ng-model="user.data" name="uData" /><br />
</form>
<pre>user.name = <span ng-bind="user.name"></span></pre>
</div>
</file>
<file name="protractor.js" type="protractor">
var model = element(by.binding('user.name'));
var input = element(by.model('user.name'));
var other = element(by.model('user.data'));
it('should allow custom events', function() {
input.sendKeys(' hello');
expect(model.getText()).toEqual('say');
other.click();
expect(model.getText()).toEqual('say hello');
});
</file>
</example>
This one shows how to debounce model changes. Model will be updated only 500 milliseconds after last change.
<example name="ngModelOptions-directive-2">
<file name="index.html">
<script>
function Ctrl($scope) {
$scope.user = { name: 'say' };
}
</script>
<div ng-controller="Ctrl">
<form>
Name:
<input type="text" ng-model="user.name" name="uName" ng-model-options="{ debounce: 500 }" /><br />
</form>
<pre>user.name = <span ng-bind="user.name"></span></pre>
</div>
</file>
</example>
*/
var ngModelOptionsDirective = function() {
return {
controller: ['$scope', '$attrs', function($scope, $attrs) {
var that = this;
this.$options = $scope.$eval($attrs.ngModelOptions);
// Allow adding/overriding bound events
if (this.$options.updateOn) {
this.$options.updateOnDefault = false;
// extract "default" pseudo-event from list of events that can trigger a model update
this.$options.updateOn = this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
that.$options.updateOnDefault = true;
return ' ';
});
} else {
this.$options.updateOnDefault = true;
}
}]
};
};

View File

@@ -608,6 +608,263 @@ describe('input', function() {
});
describe('ngModelOptions attributes', function() {
it('should allow overriding the model update trigger event on text inputs', function() {
compileInput(
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ updateOn: \'blur\' }"'+
'/>');
changeInputValueTo('a');
expect(scope.name).toBeUndefined();
browserTrigger(inputElm, 'blur');
expect(scope.name).toEqual('a');
});
it('should bind the element to a list of events', function() {
compileInput(
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ updateOn: \'blur mousemove\' }"'+
'/>');
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(
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ updateOn: \'default\' }"'+
'/>');
changeInputValueTo('a');
expect(scope.name).toEqual('a');
});
it('should allow overriding the model update trigger event on checkboxes', function() {
compileInput(
'<input type="checkbox" ng-model="checkbox" '+
'ng-model-options="{ updateOn: \'blur\' }"'+
'/>');
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(
'<input type="checkbox" ng-model="checkbox" '+
'ng-model-options="{ updateOn: \'blur default\' }"'+
'/>');
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(
'<input type="radio" ng-model="color" value="white" '+
'ng-model-options="{ updateOn: \'blur\'}"'+
'/>' +
'<input type="radio" ng-model="color" value="red" '+
'ng-model-options="{ updateOn: \'blur\'}"'+
'/>' +
'<input type="radio" ng-model="color" value="blue" '+
'ng-model-options="{ updateOn: \'blur\'}"'+
'/>');
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(
'<input type="radio" ng-model="color" value="white" '+
'ng-model-options="{ updateOn: \'blur default\' }"'+
'/>' +
'<input type="radio" ng-model="color" value="red" '+
'ng-model-options="{ updateOn: \'blur default\' }"'+
'/>' +
'<input type="radio" ng-model="color" value="blue" '+
'ng-model-options="{ updateOn: \'blur default\' }"'+
'/>');
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(
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ debounce: 10000 }"'+
'/>');
changeInputValueTo('a');
changeInputValueTo('b');
changeInputValueTo('c');
expect(scope.name).toEqual(undefined);
$timeout.flush(2000);
expect(scope.name).toEqual(undefined);
$timeout.flush(9000);
expect(scope.name).toEqual('c');
}));
it('should trigger only after timeout in checkboxes', inject(function($timeout) {
compileInput(
'<input type="checkbox" ng-model="checkbox" '+
'ng-model-options="{ debounce: 10000 }"'+
'/>');
browserTrigger(inputElm, 'click');
expect(scope.checkbox).toBe(undefined);
$timeout.flush(2000);
expect(scope.checkbox).toBe(undefined);
$timeout.flush(9000);
expect(scope.checkbox).toBe(true);
}));
it('should trigger only after timeout in radio buttons', inject(function($timeout) {
compileInput(
'<input type="radio" ng-model="color" value="white" />' +
'<input type="radio" ng-model="color" value="red" '+
'ng-model-options="{ debounce: 20000 }"'+
'/>' +
'<input type="radio" ng-model="color" value="blue" '+
'ng-model-options="{ debounce: 30000 }"'+
'/>');
browserTrigger(inputElm[0], 'click');
expect(scope.color).toBe('white');
browserTrigger(inputElm[1], 'click');
expect(scope.color).toBe('white');
$timeout.flush(12000);
expect(scope.color).toBe('white');
$timeout.flush(10000);
expect(scope.color).toBe('red');
}));
it('should allow selecting different debounce timeouts for each event',
inject(function($timeout) {
compileInput(
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{'+
'updateOn: \'default blur\', '+
'debounce: { default: 10000, blur: 5000 }'+
'}"'+
'/>');
changeInputValueTo('a');
expect(scope.checkbox).toBe(undefined);
$timeout.flush(6000);
expect(scope.checkbox).toBe(undefined);
$timeout.flush(4000);
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('<input type="checkbox" ng-model="checkbox" '+
'ng-model-options="{ '+
'updateOn: \'default blur\', debounce: { default: 10000, blur: 5000 } }"'+
'/>');
inputElm[0].checked = false;
browserTrigger(inputElm, 'click');
expect(scope.checkbox).toBe(undefined);
$timeout.flush(8000);
expect(scope.checkbox).toBe(undefined);
$timeout.flush(3000);
expect(scope.checkbox).toBe(true);
inputElm[0].checked = 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(
'<form name="test" '+
'ng-model-options="{ debounce: 10000, updateOn: \'blur\' }" >' +
'<input type="text" ng-model="name" name="alias" />'+
'</form>')(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(2000);
expect(scope.name).toBe(undefined);
$timeout.flush(9000);
expect(scope.name).toEqual('a');
dealoc(doc);
}));
it('should allow cancelling pending updates', inject(function($timeout) {
compileInput(
'<form name="test">'+
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ debounce: 10000 }" />'+
'</form>');
changeInputValueTo('a');
expect(scope.name).toEqual(undefined);
$timeout.flush(2000);
scope.test.alias.$cancelDebounce();
expect(scope.name).toEqual(undefined);
$timeout.flush(10000);
expect(scope.name).toEqual(undefined);
}));
});
it('should allow complex reference binding', function() {
compileInput('<input type="text" ng-model="obj[\'abc\'].name"/>');