Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 747055d

Browse files
committedApr 3, 2014
feat(ngModelOptions): Model update behavior can now be customized
By default, any change to the content will trigger an immediate model update and form validation. This PR implements a new directive `ng-model-options` that allow you to override this default behavior in several ways. You should specify an object with the different parameters. For example, it allows to trigger an update only when a particular event or list of events is received by the input using the `updateOn` key. Should you need multiple events, just assign an array to it. I.e. `ng-model-options="{ updateOn: '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-model-options="{ updateOn: ['default','submit'] }"` Also, with the `debounce` option, `ng-model-options` will allow deferring the actual model update until a timer expires. The timer will be reset each time an event is triggered. I.e. `ng-model-options="{ debounce: 500 }" for 500ms after the latest event. Custom timeouts for each event can be set for each event if you use 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} }"` You can use the directive in any tag so its contents became the default settings for any child control, although they can be overridden. Closes angular#1285, angular#2129
1 parent c704f76 commit 747055d

File tree

3 files changed

+545
-83
lines changed

3 files changed

+545
-83
lines changed
 

‎docs/content/guide/forms.ngdoc

+77
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,83 @@ This allows us to extend the above example with these features:
181181

182182

183183

184+
# Custom triggers
185+
186+
By default, any change to the content will trigger a model update and form validation. You can
187+
override this behavior using the {@link ng.directive:ngModelOptions ngModelOptions} directive to
188+
bind only to specified list of events. I.e. `ng-model-options="{ updateOn: "blur" }"` will update
189+
and validate only after the control loses focus. You can set a single event using an array instead
190+
of a string. I.e. `ng-model-options="{ updateOn: ["mousedown", "blur"] }"`
191+
192+
If you want to keep the default behavior and just add new events that may trigger the model update
193+
and validation, add "default" as one of the specified events.
194+
195+
I.e. `ng-model-options="{ updateOn: ["default", "blur"] }"`
196+
197+
The following example shows how to override immediate updates. Changes on the inputs within the form will update the model
198+
only when the control loses focus (blur event).
199+
200+
<example>
201+
<file name="index.html">
202+
<div ng-controller="ControllerUpdateOn">
203+
<form>
204+
Name:
205+
<input type="text" ng-model="user.name" ng-model-options="{ updateOn: "blur" }" /><br />
206+
Other data:
207+
<input type="text" ng-model="user.data" /><br />
208+
</form>
209+
<pre>username = "{{user.name}}"</pre>
210+
</div>
211+
</file>
212+
<file name="script.js">
213+
function ControllerUpdateOn($scope) {
214+
$scope.user = {};
215+
}
216+
</file>
217+
</example>
218+
219+
220+
221+
# Non-immediate (debounced) model updates
222+
223+
You can delay the model update/validation by using the `debounce` key with the
224+
{@link ng.directive:ngModelOptions ngModelOptions} directive. This delay will also apply to
225+
parsers, validators and model flags like `$dirty` or `$pristine`.
226+
227+
228+
I.e. `ng-model-options="{ debounce: 500 }"` will wait for half a second since
229+
the last content change before triggering the model update and form validation.
230+
231+
If custom triggers are used, custom debouncing timeouts can be set for each event using an object
232+
in `debounce`. This can be useful to force immediate updates on some specific circumstances
233+
(like blur events).
234+
235+
I.e. `ng-model-options="{ updateOn: ["default", "blur"], debounce: { default: 500, blur: 0 } }"`
236+
237+
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
238+
overridden.
239+
240+
This example shows how to debounce model changes. Model will be updated only 250 milliseconds after last change.
241+
242+
<example>
243+
<file name="index.html">
244+
<div ng-controller="ControllerUpdateOn">
245+
<form>
246+
Name:
247+
<input type="text" ng-model="user.name" ng-model-options="{ debounce: 250 }" /><br />
248+
</form>
249+
<pre>username = "{{user.name}}"</pre>
250+
</div>
251+
</file>
252+
<file name="script.js">
253+
function ControllerUpdateOn($scope) {
254+
$scope.user = {};
255+
}
256+
</file>
257+
</example>
258+
259+
260+
184261
# Custom Validation
185262

186263
Angular provides basic implementation for most common html5 {@link ng.directive:input input}

‎src/ng/directive/input.js

+211-83
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;
1616
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
1717
var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
1818
var TIME_REGEXP = /^(\d\d):(\d\d)$/;
19+
var DEFAULT_REGEXP = /\wdefault\w/;
1920

2021
var inputType = {
2122

@@ -877,8 +878,9 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
877878
}
878879
}
879880

880-
function textInputType(scope, element, attr, ctrl, options, $sniffer, $browser) {
881+
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
881882
var validity = element.prop('validity');
883+
882884
// In composition mode, users are still inputing intermediate text buffer,
883885
// hold the listener until composition is done.
884886
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
@@ -895,9 +897,10 @@ function textInputType(scope, element, attr, ctrl, options, $sniffer, $browser)
895897
});
896898
}
897899

898-
var listener = function() {
900+
var listener = function(ev) {
899901
if (composing) return;
900-
var value = element.val();
902+
var value = element.val(),
903+
event = ev && ev.type;
901904

902905
// By default we will trim the value
903906
// If the attribute ng-trim exists we will avoid trimming
@@ -912,50 +915,59 @@ function textInputType(scope, element, attr, ctrl, options, $sniffer, $browser)
912915
// even when the first character entered causes an error.
913916
(validity && value === '' && !validity.valueMissing)) {
914917
if (scope.$$phase) {
915-
ctrl.$setViewValue(value);
918+
ctrl.$setViewValue(value, event);
916919
} else {
917920
scope.$apply(function() {
918-
ctrl.$setViewValue(value);
921+
ctrl.$setViewValue(value, event);
919922
});
920923
}
921924
}
922925
};
923926

924-
// if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
925-
// input event on backspace, delete or cut
926-
if ($sniffer.hasEvent('input')) {
927-
element.on('input', listener);
928-
} else {
929-
var timeout;
927+
// Allow adding/overriding bound events
928+
if ((ctrl.$options.updateOn) && (ctrl.$options.updateOn.length)) {
929+
// bind to user-defined events
930+
element.on(ctrl.$options.updateOn, listener);
931+
}
930932

931-
var deferListener = function() {
932-
if (!timeout) {
933-
timeout = $browser.defer(function() {
934-
listener();
935-
timeout = null;
936-
});
937-
}
938-
};
933+
// setup default events if requested
934+
if (!ctrl.$options.updateOn || (ctrl.$options.updateOnDefault)) {
935+
// if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
936+
// input event on backspace, delete or cut
937+
if ($sniffer.hasEvent('input')) {
938+
element.on('input', listener);
939+
} else {
940+
var timeout;
939941

940-
element.on('keydown', function(event) {
941-
var key = event.keyCode;
942+
var deferListener = function(ev) {
943+
if (!timeout) {
944+
timeout = $browser.defer(function() {
945+
listener(ev);
946+
timeout = null;
947+
});
948+
}
949+
};
942950

943-
// ignore
944-
// command modifiers arrows
945-
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
951+
element.on('keydown', function(event) {
952+
var key = event.keyCode;
946953

947-
deferListener();
948-
});
954+
// ignore
955+
// command modifiers arrows
956+
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
949957

950-
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
951-
if ($sniffer.hasEvent('paste')) {
952-
element.on('paste cut', deferListener);
958+
deferListener();
959+
});
960+
961+
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
962+
if ($sniffer.hasEvent('paste')) {
963+
element.on('paste cut', deferListener);
964+
}
953965
}
954-
}
955966

956-
// if user paste into input using mouse on older browser
957-
// or form autocomplete on newer browser, we need "change" event to catch it
958-
element.on('change', listener);
967+
// if user paste into input using mouse on older browser
968+
// or form autocomplete on newer browser, we need "change" event to catch it
969+
element.on('change', listener);
970+
}
959971

960972
ctrl.$render = function() {
961973
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
@@ -1067,8 +1079,8 @@ function createDateParser(regexp, mapping) {
10671079
}
10681080

10691081
function createDateInputType(type, regexp, parseDate, format) {
1070-
return function dynamicDateInputType(scope, element, attr, ctrl, options, $sniffer, $browser, $filter) {
1071-
textInputType(scope, element, attr, ctrl, options, $sniffer, $browser);
1082+
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
1083+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
10721084

10731085
ctrl.$parsers.push(function(value) {
10741086
if(ctrl.$isEmpty(value)) {
@@ -1118,8 +1130,8 @@ function createDateInputType(type, regexp, parseDate, format) {
11181130
};
11191131
}
11201132

1121-
function numberInputType(scope, element, attr, ctrl, options, $sniffer, $browser) {
1122-
textInputType(scope, element, attr, ctrl, options, $sniffer, $browser);
1133+
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1134+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11231135

11241136
ctrl.$parsers.push(function(value) {
11251137
var empty = ctrl.$isEmpty(value);
@@ -1163,8 +1175,8 @@ function numberInputType(scope, element, attr, ctrl, options, $sniffer, $browser
11631175
});
11641176
}
11651177

1166-
function urlInputType(scope, element, attr, ctrl, options, $sniffer, $browser) {
1167-
textInputType(scope, element, attr, ctrl, options, $sniffer, $browser);
1178+
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1179+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11681180

11691181
var urlValidator = function(value) {
11701182
return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value);
@@ -1174,8 +1186,8 @@ function urlInputType(scope, element, attr, ctrl, options, $sniffer, $browser) {
11741186
ctrl.$parsers.push(urlValidator);
11751187
}
11761188

1177-
function emailInputType(scope, element, attr, ctrl, options, $sniffer, $browser) {
1178-
textInputType(scope, element, attr, ctrl, options, $sniffer, $browser);
1189+
function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1190+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11791191

11801192
var emailValidator = function(value) {
11811193
return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value);
@@ -1185,19 +1197,29 @@ function emailInputType(scope, element, attr, ctrl, options, $sniffer, $browser)
11851197
ctrl.$parsers.push(emailValidator);
11861198
}
11871199

1188-
function radioInputType(scope, element, attr, ctrl, options) {
1200+
function radioInputType(scope, element, attr, ctrl) {
11891201
// make the name unique, if not defined
11901202
if (isUndefined(attr.name)) {
11911203
element.attr('name', nextUid());
11921204
}
11931205

1194-
element.on('click', function() {
1206+
var listener = function(ev) {
11951207
if (element[0].checked) {
11961208
scope.$apply(function() {
1197-
ctrl.$setViewValue(attr.value);
1209+
ctrl.$setViewValue(attr.value, ev && ev.type);
11981210
});
11991211
}
1200-
});
1212+
};
1213+
1214+
// Allow adding/overriding bound events
1215+
if ((ctrl.$options.updateOn) && (ctrl.$options.updateOn.length)) {
1216+
// bind to user-defined events
1217+
element.on(ctrl.$options.updateOn, listener);
1218+
}
1219+
1220+
if (!ctrl.$options.updateOn || (ctrl.$options.updateOnDefault)) {
1221+
element.on('click', listener);
1222+
}
12011223

12021224
ctrl.$render = function() {
12031225
var value = attr.value;
@@ -1207,18 +1229,28 @@ function radioInputType(scope, element, attr, ctrl, options) {
12071229
attr.$observe('value', ctrl.$render);
12081230
}
12091231

1210-
function checkboxInputType(scope, element, attr, ctrl, options) {
1232+
function checkboxInputType(scope, element, attr, ctrl) {
12111233
var trueValue = attr.ngTrueValue,
12121234
falseValue = attr.ngFalseValue;
12131235

12141236
if (!isString(trueValue)) trueValue = true;
12151237
if (!isString(falseValue)) falseValue = false;
12161238

1217-
element.on('click', function() {
1239+
var listener = function(ev) {
12181240
scope.$apply(function() {
1219-
ctrl.$setViewValue(element[0].checked);
1220-
});
1221-
});
1241+
ctrl.$setViewValue(element[0].checked, ev && ev.type);
1242+
});
1243+
};
1244+
1245+
// Allow adding/overriding bound events
1246+
if ((ctrl.$options.updateOn) && (ctrl.$options.updateOn.length)) {
1247+
// bind to user-defined events
1248+
element.on(ctrl.$options.updateOn, listener);
1249+
}
1250+
1251+
if (!ctrl.$options.updateOn || (ctrl.$options.updateOnDefault)) {
1252+
element.on('click', listener);
1253+
}
12221254

12231255
ctrl.$render = function() {
12241256
element[0].checked = ctrl.$viewValue;
@@ -1383,7 +1415,7 @@ var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sni
13831415
require: ['?ngModel', '^?ngModelOptions'],
13841416
link: function(scope, element, attr, ctrls) {
13851417
if (ctrls[0]) {
1386-
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], ctrls[1], $sniffer,
1418+
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
13871419
$browser, $filter);
13881420
}
13891421
}
@@ -1662,26 +1694,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16621694

16631695
/**
16641696
* @ngdoc method
1665-
* @name ngModel.NgModelController#$setViewValue
1697+
* @name ngModel.NgModelController#$cancelDebounce
16661698
*
16671699
* @description
1668-
* Update the view value.
1669-
*
1670-
* This method should be called when the view value changes, typically from within a DOM event handler.
1671-
* For example {@link ng.directive:input input} and
1672-
* {@link ng.directive:select select} directives call it.
1673-
*
1674-
* It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
1675-
* which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
1676-
* `$modelValue` and the **expression** specified in the `ng-model` attribute.
1677-
*
1678-
* Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
1679-
*
1680-
* Note that calling this function does not trigger a `$digest`.
1700+
* Cancel a pending debounced update.
16811701
*
1682-
* @param {string} value Value from the view.
1702+
* This method should be called before directly update a debounced model from the scope in
1703+
* order to prevent unintended future changes of the model value because of a delayed event.
16831704
*/
1684-
this.$realSetViewValue = function(value) {
1705+
this.$cancelDebounce = function() {
1706+
if ( pendingDebounce ) {
1707+
$timeout.cancel(pendingDebounce);
1708+
pendingDebounce = null;
1709+
}
1710+
};
1711+
1712+
// update the view value
1713+
this.$$realSetViewValue = function(value) {
16851714
this.$viewValue = value;
16861715

16871716
// change to dirty
@@ -1709,22 +1738,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17091738
});
17101739
}
17111740
};
1741+
1742+
/**
1743+
* @ngdoc method
1744+
* @name ngModel.NgModelController#$setViewValue
1745+
*
1746+
* @description
1747+
* Update the view value.
1748+
*
1749+
* This method should be called when the view value changes, typically from within a DOM event handler.
1750+
* For example {@link ng.directive:input input} and
1751+
* {@link ng.directive:select select} directives call it.
1752+
*
1753+
* It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
1754+
* which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
1755+
* `$modelValue` and the **expression** specified in the `ng-model` attribute.
1756+
*
1757+
* Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
1758+
*
1759+
* All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
1760+
* directive is used with a custom debounce for this particular event.
1761+
*
1762+
* Note that calling this function does not trigger a `$digest`.
1763+
*
1764+
* @param {string} value Value from the view.
1765+
* @param {string} trigger Event that triggered the update.
1766+
*/
17121767
this.$setViewValue = function(value, trigger) {
17131768
var that = this;
1714-
trigger = trigger || 'default';
1715-
var debounceDelay = (isObject(this.$options.debounce) ? this.$options.debounce[trigger] : this.$options.debounce) || 0;
1769+
var debounceDelay = (isObject(this.$options.debounce)
1770+
? (this.$options.debounce[trigger] || this.$options.debounce['default'] || 0)
1771+
: this.$options.debounce) || 0;
17161772

1717-
if ( pendingDebounce ) {
1718-
$timeout.cancel(pendingDebounce);
1719-
pendingDebounce = null;
1720-
}
1773+
that.$cancelDebounce();
17211774
if ( debounceDelay ) {
17221775
pendingDebounce = $timeout(function() {
17231776
pendingDebounce = null;
1724-
that.$realSetViewValue(value);
1777+
that.$$realSetViewValue(value);
17251778
}, debounceDelay);
17261779
} else {
1727-
that.$realSetViewValue(value);
1780+
that.$$realSetViewValue(value);
17281781
}
17291782
};
17301783

@@ -1737,12 +1790,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17371790
// if scope model value and ngModel value are out of sync
17381791
if (ctrl.$modelValue !== value) {
17391792

1740-
// Cancel any pending debounced update
1741-
if ( pendingDebounce ) {
1742-
$timeout.cancel(pendingDebounce);
1743-
pendingDebounce = null;
1744-
}
1745-
17461793
var formatters = ctrl.$formatters,
17471794
idx = formatters.length;
17481795

@@ -2155,11 +2202,92 @@ var ngValueDirective = function() {
21552202
};
21562203
};
21572204

2205+
/**
2206+
* @ngdoc directive
2207+
* @name ngModelOptions
2208+
*
2209+
* @description
2210+
* Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of events
2211+
* that will trigger a model update and/or a debouncing delay so that the actual update only takes place
2212+
* when a timer expires; this timer will be reset after another change takes place.
2213+
*
2214+
* @param {Object=} Object that contains options to apply to the current model. Valid keys are:
2215+
* - updateOn: string specifying which event should be the input bound to. If an array is supplied instead,
2216+
* multiple events can be specified. There is a special event called `default` that
2217+
* matches the default events belonging of the control.
2218+
* - debounce: integer value which contains the debounce model update value in milliseconds. A value of 0
2219+
* triggers an immediate update. If an object is supplied instead, you can specify a custom value
2220+
* for each event. I.e.
2221+
* `ngModelOptions="{ updateOn: ["default", "blur"], debounce: {'default': 500, 'blur': 0} }"`
2222+
*
2223+
* @example
2224+
2225+
The following example shows how to override immediate updates. Changes on the inputs within the form will update the model
2226+
only when the control loses focus (blur event).
2227+
2228+
<example name="ngModelOptions-directive-1">
2229+
<file name="index.html">
2230+
<script>
2231+
function Ctrl($scope) {
2232+
$scope.user = { name: 'say', data: '' };
2233+
}
2234+
</script>
2235+
<div ng-controller="Ctrl">
2236+
<form>
2237+
Name:
2238+
<input type="text" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" name="uName" /><br />
2239+
Other data:
2240+
<input type="text" ng-model="user.data" name="uData" /><br />
2241+
</form>
2242+
<pre>user.name = <span ng-bind="user.name"></span></pre>
2243+
</div>
2244+
</file>
2245+
<file name="protractor.js" type="protractor">
2246+
var model = element(by.binding('user.name'));
2247+
var input = element(by.model('user.name'));
2248+
var other = element(by.model('user.data'));
2249+
it('should allow custom events', function() {
2250+
input.sendKeys(' hello');
2251+
expect(model.getText()).toEqual('say');
2252+
other.click();
2253+
expect(model.getText()).toEqual('say hello');
2254+
});
2255+
</file>
2256+
</example>
2257+
2258+
This one shows how to debounce model changes. Model will be updated only 500 milliseconds after last change.
21582259
2260+
<example name="ngModelOptions-directive-2">
2261+
<file name="index.html">
2262+
<script>
2263+
function Ctrl($scope) {
2264+
$scope.user = { name: 'say' };
2265+
}
2266+
</script>
2267+
<div ng-controller="Ctrl">
2268+
<form>
2269+
Name:
2270+
<input type="text" ng-model="user.name" name="uName" ng-model-options="{ debounce: 500 }" /><br />
2271+
</form>
2272+
<pre>user.name = <span ng-bind="user.name"></span></pre>
2273+
</div>
2274+
</file>
2275+
</example>
2276+
*/
21592277
var ngModelOptionsDirective = function() {
21602278
return {
2161-
controller: function($scope, $attrs) {
2279+
controller: ['$scope', '$attrs', function($scope, $attrs) {
2280+
var that = this;
21622281
this.$options = $scope.$eval($attrs.ngModelOptions);
2163-
}
2282+
this.$updateOn = [];
2283+
// Allow adding/overriding bound events
2284+
if (this.$options.updateOn) {
2285+
// look up for default in event list
2286+
this.$options.updateOnDefault = DEFAULT_REGEXP.test(this.$options.updateOn);
2287+
this.$options.updateOn = this.$options.updateOn.replace(DEFAULT_REGEXP, '');
2288+
} else {
2289+
this.$options.updateOnDefault = true;
2290+
}
2291+
}]
21642292
};
21652293
};

‎test/ng/directive/inputSpec.js

+257
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,263 @@ describe('input', function() {
608608
});
609609

610610

611+
describe('ngModelOptions attributes', function() {
612+
613+
it('should allow overriding the model update trigger event on text inputs', function() {
614+
compileInput(
615+
'<input type="text" ng-model="name" name="alias" '+
616+
'ng-model-options="{updateOn: \'blur\'}"'+
617+
'/>');
618+
619+
changeInputValueTo('a');
620+
expect(scope.name).toBeUndefined();
621+
browserTrigger(inputElm, 'blur');
622+
expect(scope.name).toEqual('a');
623+
});
624+
625+
626+
it('should bind the element to a list of events', function() {
627+
compileInput(
628+
'<input type="text" ng-model="name" name="alias" '+
629+
'ng-model-options="{updateOn: [\'blur\', \'mousemove\']}"'+
630+
'/>');
631+
632+
changeInputValueTo('a');
633+
expect(scope.name).toBeUndefined();
634+
browserTrigger(inputElm, 'blur');
635+
expect(scope.name).toEqual('a');
636+
637+
changeInputValueTo('b');
638+
expect(scope.name).toEqual('a');
639+
browserTrigger(inputElm, 'mousemove');
640+
expect(scope.name).toEqual('b');
641+
});
642+
643+
644+
it('should allow keeping the default update behavior on text inputs', function() {
645+
compileInput(
646+
'<input type="text" ng-model="name" name="alias" '+
647+
'ng-model-options="{updateOn: \'default\'}"'+
648+
'/>');
649+
650+
changeInputValueTo('a');
651+
expect(scope.name).toEqual('a');
652+
});
653+
654+
655+
it('should allow overriding the model update trigger event on checkboxes', function() {
656+
compileInput(
657+
'<input type="checkbox" ng-model="checkbox" '+
658+
'ng-model-options="{updateOn: \'blur\'}"'+
659+
'/>');
660+
661+
browserTrigger(inputElm, 'click');
662+
expect(scope.checkbox).toBe(undefined);
663+
664+
browserTrigger(inputElm, 'blur');
665+
expect(scope.checkbox).toBe(true);
666+
667+
browserTrigger(inputElm, 'click');
668+
expect(scope.checkbox).toBe(true);
669+
});
670+
671+
672+
it('should allow keeping the default update behavior on checkboxes', function() {
673+
compileInput(
674+
'<input type="checkbox" ng-model="checkbox" '+
675+
'ng-model-options="{updateOn: [\'blur\', \'default\']}"'+
676+
'/>');
677+
678+
browserTrigger(inputElm, 'click');
679+
expect(scope.checkbox).toBe(true);
680+
681+
browserTrigger(inputElm, 'click');
682+
expect(scope.checkbox).toBe(false);
683+
});
684+
685+
686+
it('should allow overriding the model update trigger event on radio buttons', function() {
687+
compileInput(
688+
'<input type="radio" ng-model="color" value="white" '+
689+
'ng-model-options="{updateOn: \'blur\'}"'+
690+
'/>' +
691+
'<input type="radio" ng-model="color" value="red" '+
692+
'ng-model-options="{updateOn: \'blur\'}"'+
693+
'/>' +
694+
'<input type="radio" ng-model="color" value="blue" '+
695+
'ng-model-options="{updateOn: \'blur\'}"'+
696+
'/>');
697+
698+
scope.$apply(function() {
699+
scope.color = 'white';
700+
});
701+
browserTrigger(inputElm[2], 'click');
702+
expect(scope.color).toBe('white');
703+
704+
browserTrigger(inputElm[2], 'blur');
705+
expect(scope.color).toBe('blue');
706+
707+
});
708+
709+
710+
it('should allow keeping the default update behavior on radio buttons', function() {
711+
compileInput(
712+
'<input type="radio" ng-model="color" value="white" '+
713+
'ng-model-options="{updateOn: [\'blur\', \'default\']}"'+
714+
'/>' +
715+
'<input type="radio" ng-model="color" value="red" '+
716+
'ng-model-options="{updateOn: [\'blur\', \'default\']}"'+
717+
'/>' +
718+
'<input type="radio" ng-model="color" value="blue" '+
719+
'ng-model-options="{updateOn: [\'blur\', \'default\']}"'+
720+
'/>');
721+
722+
scope.$apply(function() {
723+
scope.color = 'white';
724+
});
725+
browserTrigger(inputElm[2], 'click');
726+
expect(scope.color).toBe('blue');
727+
});
728+
729+
730+
it('should trigger only after timeout in text inputs', inject(function($timeout) {
731+
compileInput(
732+
'<input type="text" ng-model="name" name="alias" '+
733+
'ng-model-options="{ debounce: 10000 }"'+
734+
'/>');
735+
736+
changeInputValueTo('a');
737+
changeInputValueTo('b');
738+
changeInputValueTo('c');
739+
expect(scope.name).toEqual(undefined);
740+
$timeout.flush(2000);
741+
expect(scope.name).toEqual(undefined);
742+
$timeout.flush(9000);
743+
expect(scope.name).toEqual('c');
744+
}));
745+
746+
747+
it('should trigger only after timeout in checkboxes', inject(function($timeout) {
748+
compileInput(
749+
'<input type="checkbox" ng-model="checkbox" '+
750+
'ng-model-options="{ debounce: 10000 }"'+
751+
'/>');
752+
753+
browserTrigger(inputElm, 'click');
754+
expect(scope.checkbox).toBe(undefined);
755+
$timeout.flush(2000);
756+
expect(scope.checkbox).toBe(undefined);
757+
$timeout.flush(9000);
758+
expect(scope.checkbox).toBe(true);
759+
}));
760+
761+
762+
it('should trigger only after timeout in radio buttons', inject(function($timeout) {
763+
compileInput(
764+
'<input type="radio" ng-model="color" value="white" />' +
765+
'<input type="radio" ng-model="color" value="red" '+
766+
'ng-model-options="{ debounce: 20000 }"'+
767+
'/>' +
768+
'<input type="radio" ng-model="color" value="blue" '+
769+
'ng-model-options="{ debounce: 30000 }"'+
770+
'/>');
771+
772+
browserTrigger(inputElm[0], 'click');
773+
expect(scope.color).toBe('white');
774+
browserTrigger(inputElm[1], 'click');
775+
expect(scope.color).toBe('white');
776+
$timeout.flush(12000);
777+
expect(scope.color).toBe('white');
778+
$timeout.flush(10000);
779+
expect(scope.color).toBe('red');
780+
781+
}));
782+
783+
it('should allow selecting different debounce timeouts for each event',
784+
inject(function($timeout) {
785+
compileInput(
786+
'<input type="text" ng-model="name" name="alias" '+
787+
'ng-model-options="{'+
788+
'updateOn: [\'default\', \'blur\'], '+
789+
'debounce: {default: 10000, blur: 5000 }'+
790+
'}"'+
791+
'/>');
792+
793+
changeInputValueTo('a');
794+
expect(scope.checkbox).toBe(undefined);
795+
$timeout.flush(6000);
796+
expect(scope.checkbox).toBe(undefined);
797+
$timeout.flush(4000);
798+
expect(scope.name).toEqual('a');
799+
changeInputValueTo('b');
800+
browserTrigger(inputElm, 'blur');
801+
$timeout.flush(4000);
802+
expect(scope.name).toEqual('a');
803+
$timeout.flush(2000);
804+
expect(scope.name).toEqual('b');
805+
}));
806+
807+
808+
it('should allow selecting different debounce timeouts for each event on checkboxes', inject(function($timeout) {
809+
compileInput('<input type="checkbox" ng-model="checkbox" '+
810+
'ng-model-options="{ '+
811+
'updateOn: [\'default\', \'blur\'], debounce: {default: 10000, blur: 5000 } }"'+
812+
'/>');
813+
814+
inputElm[0].checked = false;
815+
browserTrigger(inputElm, 'click');
816+
expect(scope.checkbox).toBe(undefined);
817+
$timeout.flush(8000);
818+
expect(scope.checkbox).toBe(undefined);
819+
$timeout.flush(3000);
820+
expect(scope.checkbox).toBe(true);
821+
inputElm[0].checked = true;
822+
browserTrigger(inputElm, 'click');
823+
browserTrigger(inputElm, 'blur');
824+
$timeout.flush(3000);
825+
expect(scope.checkbox).toBe(true);
826+
$timeout.flush(3000);
827+
expect(scope.checkbox).toBe(false);
828+
}));
829+
830+
831+
it('should inherit model update settings from ancestor elements', inject(function($timeout) {
832+
var doc = $compile(
833+
'<form name="test" '+
834+
'ng-model-options="{ debounce: 10000, updateOn: \'blur\' }" >' +
835+
'<input type="text" ng-model="name" name="alias" />'+
836+
'</form>')(scope);
837+
838+
var input = doc.find('input').eq(0);
839+
input.val('a');
840+
expect(scope.name).toEqual(undefined);
841+
browserTrigger(input, 'blur');
842+
expect(scope.name).toBe(undefined);
843+
$timeout.flush(2000);
844+
expect(scope.name).toBe(undefined);
845+
$timeout.flush(9000);
846+
expect(scope.name).toEqual('a');
847+
dealoc(doc);
848+
}));
849+
850+
851+
it('should allow cancelling pending updates', inject(function($timeout) {
852+
compileInput(
853+
'<form name="test">'+
854+
'<input type="text" ng-model="name" name="alias" '+
855+
'ng-model-options="{ debounce: 10000 }" />'+
856+
'</form>');
857+
changeInputValueTo('a');
858+
expect(scope.name).toEqual(undefined);
859+
$timeout.flush(2000);
860+
scope.test.alias.$cancelDebounce();
861+
expect(scope.name).toEqual(undefined);
862+
$timeout.flush(10000);
863+
expect(scope.name).toEqual(undefined);
864+
}));
865+
866+
});
867+
611868
it('should allow complex reference binding', function() {
612869
compileInput('<input type="text" ng-model="obj[\'abc\'].name"/>');
613870

0 commit comments

Comments
 (0)
Please sign in to comment.