mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-06-02 19:42:35 +08:00
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:
committed by
Peter Bacon Darwin
parent
e55c8bcbca
commit
dbe381f29f
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}]
|
||||
};
|
||||
};
|
||||
@@ -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"/>');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user