Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit adfc322

Browse files
shahatapetebacondarwin
authored andcommittedMay 9, 2014
refactor(ngModelOptions): move debounce and updateOn logic into NgModelController
Move responsibility for pending and debouncing model updates into `NgModelController`. Now input directives are only responsible for capturing changes to the input element's value and then calling `$setViewValue` with the new value. Calls to `$setViewValue(value)` change the `$viewValue` property but these changes are not committed to the `$modelValue` until an `updateOn` trigger occurs (and any related `debounce` has resolved). The `$$lastCommittedViewValue` is now stored when `$setViewValue(value)` updates the `$viewValue`, which allows the view to be "reset" by calling `$rollbackViewValue()`. The new `$commitViewValue()` method allows developers to force the `$viewValue` to be committed through to the `$modelValue` immediately, ignoring `updateOn` triggers and `debounce` delays. BREAKING CHANGE: This commit changes the API on `NgModelController`, both semantically and in terms of adding and renaming methods. * `$setViewValue(value)` - This method still changes the `$viewValue` but does not immediately commit this change through to the `$modelValue` as it did previously. Now the value is committed only when a trigger specified in an associated `ngModelOptions` directive occurs. If `ngModelOptions` also has a `debounce` delay specified for the trigger then the change will also be debounced before being committed. In most cases this should not have a significant impact on how `NgModelController` is used: If `updateOn` includes `default` then `$setViewValue` will trigger a (potentially debounced) commit immediately. * `$cancelUpdate()` - is renamed to `$rollbackViewValue()` and has the same meaning, which is to revert the current `$viewValue` back to the `$lastCommittedViewValue`, to cancel any pending debounced updates and to re-render the input. To migrate code that used `$cancelUpdate()` follow the example below: Before: ``` $scope.resetWithCancel = function (e) { if (e.keyCode == 27) { $scope.myForm.myInput1.$cancelUpdate(); $scope.myValue = ''; } }; ``` After: ``` $scope.resetWithCancel = function (e) { if (e.keyCode == 27) { $scope.myForm.myInput1.$rollbackViewValue(); $scope.myValue = ''; } } ```
1 parent 0ef1727 commit adfc322

File tree

2 files changed

+155
-91
lines changed

2 files changed

+155
-91
lines changed
 

‎src/ng/directive/input.js

+92-80
Original file line numberDiff line numberDiff line change
@@ -16,7 +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 = /(\b|^)default(\b|$)/;
19+
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
2020

2121
var inputType = {
2222

@@ -934,51 +934,42 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
934934
}
935935
};
936936

937-
// Allow adding/overriding bound events
938-
if (ctrl.$options && ctrl.$options.updateOn) {
939-
// bind to user-defined events
940-
element.on(ctrl.$options.updateOn, listener);
941-
}
942-
943-
// setup default events if requested
944-
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
945-
// if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
946-
// input event on backspace, delete or cut
947-
if ($sniffer.hasEvent('input')) {
948-
element.on('input', listener);
949-
} else {
950-
var timeout;
951-
952-
var deferListener = function(ev) {
953-
if (!timeout) {
954-
timeout = $browser.defer(function() {
955-
listener(ev);
956-
timeout = null;
957-
});
958-
}
959-
};
937+
// if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
938+
// input event on backspace, delete or cut
939+
if ($sniffer.hasEvent('input')) {
940+
element.on('input', listener);
941+
} else {
942+
var timeout;
943+
944+
var deferListener = function(ev) {
945+
if (!timeout) {
946+
timeout = $browser.defer(function() {
947+
listener(ev);
948+
timeout = null;
949+
});
950+
}
951+
};
960952

961-
element.on('keydown', function(event) {
962-
var key = event.keyCode;
953+
element.on('keydown', function(event) {
954+
var key = event.keyCode;
963955

964-
// ignore
965-
// command modifiers arrows
966-
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
956+
// ignore
957+
// command modifiers arrows
958+
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
967959

968-
deferListener(event);
969-
});
960+
deferListener(event);
961+
});
970962

971-
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
972-
if ($sniffer.hasEvent('paste')) {
973-
element.on('paste cut', deferListener);
974-
}
963+
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
964+
if ($sniffer.hasEvent('paste')) {
965+
element.on('paste cut', deferListener);
975966
}
976-
977-
// if user paste into input using mouse on older browser
978-
// or form autocomplete on newer browser, we need "change" event to catch it
979-
element.on('change', listener);
980967
}
981968

969+
// if user paste into input using mouse on older browser
970+
// or form autocomplete on newer browser, we need "change" event to catch it
971+
element.on('change', listener);
972+
982973
ctrl.$render = function() {
983974
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
984975
};
@@ -1221,15 +1212,7 @@ function radioInputType(scope, element, attr, ctrl) {
12211212
}
12221213
};
12231214

1224-
// Allow adding/overriding bound events
1225-
if (ctrl.$options && ctrl.$options.updateOn) {
1226-
// bind to user-defined events
1227-
element.on(ctrl.$options.updateOn, listener);
1228-
}
1229-
1230-
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
1231-
element.on('click', listener);
1232-
}
1215+
element.on('click', listener);
12331216

12341217
ctrl.$render = function() {
12351218
var value = attr.value;
@@ -1252,15 +1235,7 @@ function checkboxInputType(scope, element, attr, ctrl) {
12521235
});
12531236
};
12541237

1255-
// Allow adding/overriding bound events
1256-
if (ctrl.$options && ctrl.$options.updateOn) {
1257-
// bind to user-defined events
1258-
element.on(ctrl.$options.updateOn, listener);
1259-
}
1260-
1261-
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
1262-
element.on('click', listener);
1263-
}
1238+
element.on('click', listener);
12641239

12651240
ctrl.$render = function() {
12661241
element[0].checked = ctrl.$viewValue;
@@ -1704,22 +1679,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17041679

17051680
/**
17061681
* @ngdoc method
1707-
* @name ngModel.NgModelController#$cancelUpdate
1682+
* @name ngModel.NgModelController#$rollbackViewValue
17081683
*
17091684
* @description
1710-
* Cancel an update and reset the input element's value to prevent an update to the `$viewValue`,
1685+
* Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
17111686
* which may be caused by a pending debounced event or because the input is waiting for a some
17121687
* future event.
17131688
*
17141689
* If you have an input that uses `ng-model-options` to set up debounced events or events such
1715-
* as blur you can have a situation where there is a period when the value of the input element
1716-
* is out of synch with the ngModel's `$viewValue`.
1690+
* as blur you can have a situation where there is a period when the `$viewValue`
1691+
* is out of synch with the ngModel's `$modelValue`.
17171692
*
17181693
* In this case, you can run into difficulties if you try to update the ngModel's `$modelValue`
17191694
* programmatically before these debounced/future events have resolved/occurred, because Angular's
17201695
* dirty checking mechanism is not able to tell whether the model has actually changed or not.
17211696
*
1722-
* The `$cancelUpdate()` method should be called before programmatically changing the model of an
1697+
* The `$rollbackViewValue()` method should be called before programmatically changing the model of an
17231698
* input which may have such events pending. This is important in order to make sure that the
17241699
* input field will be updated with the new model value and any pending operations are cancelled.
17251700
*
@@ -1730,7 +1705,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17301705
* .controller('CancelUpdateCtrl', function($scope) {
17311706
* $scope.resetWithCancel = function (e) {
17321707
* if (e.keyCode == 27) {
1733-
* $scope.myForm.myInput1.$cancelUpdate();
1708+
* $scope.myForm.myInput1.$rollbackViewValue();
17341709
* $scope.myValue = '';
17351710
* }
17361711
* };
@@ -1749,26 +1724,39 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17491724
* <p>Now see what happens if you start typing then press the Escape key</p>
17501725
*
17511726
* <form name="myForm" ng-model-options="{ updateOn: 'blur' }">
1752-
* <p>With $cancelUpdate()</p>
1727+
* <p>With $rollbackViewValue()</p>
17531728
* <input name="myInput1" ng-model="myValue" ng-keydown="resetWithCancel($event)"><br/>
17541729
* myValue: "{{ myValue }}"
17551730
*
1756-
* <p>Without $cancelUpdate()</p>
1731+
* <p>Without $rollbackViewValue()</p>
17571732
* <input name="myInput2" ng-model="myValue" ng-keydown="resetWithoutCancel($event)"><br/>
17581733
* myValue: "{{ myValue }}"
17591734
* </form>
17601735
* </div>
17611736
* </file>
17621737
* </example>
17631738
*/
1764-
this.$cancelUpdate = function() {
1739+
this.$rollbackViewValue = function() {
17651740
$timeout.cancel(pendingDebounce);
1741+
ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
17661742
ctrl.$render();
17671743
};
17681744

1769-
// update the view value
1770-
this.$$realSetViewValue = function(value) {
1771-
ctrl.$viewValue = value;
1745+
/**
1746+
* @ngdoc method
1747+
* @name ngModel.NgModelController#$commitViewValue
1748+
*
1749+
* @description
1750+
* Commit a pending update to the `$modelValue`.
1751+
*
1752+
* Updates may be pending by a debounced event or because the input is waiting for a some future
1753+
* event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
1754+
* usually handles calling this in response to input events.
1755+
*/
1756+
this.$commitViewValue = function() {
1757+
var value = ctrl.$viewValue;
1758+
ctrl.$$lastCommittedViewValue = value;
1759+
$timeout.cancel(pendingDebounce);
17721760

17731761
// change to dirty
17741762
if (ctrl.$pristine) {
@@ -1813,6 +1801,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18131801
*
18141802
* Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
18151803
*
1804+
* In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
1805+
* and the `default` trigger is not listed, all those actions will remain pending until one of the
1806+
* `updateOn` events is triggered on the DOM element.
18161807
* All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
18171808
* directive is used with a custom debounce for this particular event.
18181809
*
@@ -1822,6 +1813,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18221813
* @param {string} trigger Event that triggered the update.
18231814
*/
18241815
this.$setViewValue = function(value, trigger) {
1816+
ctrl.$viewValue = value;
1817+
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
1818+
ctrl.$$debounceViewValueCommit(trigger);
1819+
}
1820+
};
1821+
1822+
this.$$debounceViewValueCommit = function(trigger) {
18251823
var debounceDelay = 0,
18261824
options = ctrl.$options,
18271825
debounce;
@@ -1840,10 +1838,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18401838
$timeout.cancel(pendingDebounce);
18411839
if (debounceDelay) {
18421840
pendingDebounce = $timeout(function() {
1843-
ctrl.$$realSetViewValue(value);
1841+
ctrl.$commitViewValue();
18441842
}, debounceDelay);
18451843
} else {
1846-
ctrl.$$realSetViewValue(value);
1844+
ctrl.$commitViewValue();
18471845
}
18481846
};
18491847

@@ -1863,7 +1861,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18631861
}
18641862

18651863
if (ctrl.$viewValue !== value) {
1866-
ctrl.$viewValue = value;
1864+
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = value;
18671865
ctrl.$render();
18681866
}
18691867
}
@@ -2001,6 +1999,16 @@ var ngModelDirective = function() {
20011999
scope.$on('$destroy', function() {
20022000
formCtrl.$removeControl(modelCtrl);
20032001
});
2002+
},
2003+
post: function(scope, element, attr, ctrls) {
2004+
var modelCtrl = ctrls[0];
2005+
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
2006+
element.on(modelCtrl.$options.updateOn, function(ev) {
2007+
scope.$apply(function() {
2008+
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
2009+
});
2010+
});
2011+
}
20042012
}
20052013
}
20062014
};
@@ -2279,14 +2287,18 @@ var ngValueDirective = function() {
22792287
*
22802288
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
22812289
* be different than the value in the actual model. This means that if you update the model you
2282-
* should also invoke {@link ngModel.NgModelController `$cancelUpdate`} on the relevant input field in
2290+
* should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
22832291
* order to make sure it is synchronized with the model and that any debounced action is canceled.
22842292
*
2285-
* The easiest way to reference the control's {@link ngModel.NgModelController `$cancelUpdate`}
2293+
* The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
22862294
* method is by making sure the input is placed inside a form that has a `name` attribute. This is
22872295
* important because `form` controllers are published to the related scope under the name in their
22882296
* `name` attribute.
22892297
*
2298+
* Any pending changes will take place immediately when an enclosing form is submitted via the
2299+
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
2300+
* to have access to the updated model.
2301+
*
22902302
* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
22912303
* - `updateOn`: string specifying which event should be the input bound to. You can set several
22922304
* events using an space delimited list. There is a special event called `default` that
@@ -2324,7 +2336,7 @@ var ngValueDirective = function() {
23242336
23252337
$scope.cancel = function (e) {
23262338
if (e.keyCode == 27) {
2327-
$scope.userForm.userName.$cancelUpdate();
2339+
$scope.userForm.userName.$rollbackViewValue();
23282340
}
23292341
};
23302342
}
@@ -2342,7 +2354,7 @@ var ngValueDirective = function() {
23422354
expect(model.getText()).toEqual('say hello');
23432355
});
23442356
2345-
it('should $cancelUpdate when model changes', function() {
2357+
it('should $rollbackViewValue when model changes', function() {
23462358
input.sendKeys(' hello');
23472359
expect(input.getAttribute('value')).toEqual('say hello');
23482360
input.sendKeys(protractor.Key.ESCAPE);
@@ -2364,7 +2376,7 @@ var ngValueDirective = function() {
23642376
<input type="text" name="userName"
23652377
ng-model="user.name"
23662378
ng-model-options="{ debounce: 1000 }" />
2367-
<button ng-click="userForm.userName.$cancelUpdate(); user.name=''">Clear</button><br />
2379+
<button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
23682380
</form>
23692381
<pre>user.name = <span ng-bind="user.name"></span></pre>
23702382
</div>
@@ -2382,13 +2394,13 @@ var ngModelOptionsDirective = function() {
23822394
var that = this;
23832395
this.$options = $scope.$eval($attrs.ngModelOptions);
23842396
// Allow adding/overriding bound events
2385-
if (this.$options.updateOn) {
2397+
if (this.$options.updateOn !== undefined) {
23862398
this.$options.updateOnDefault = false;
23872399
// extract "default" pseudo-event from list of events that can trigger a model update
2388-
this.$options.updateOn = this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
2400+
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
23892401
that.$options.updateOnDefault = true;
23902402
return ' ';
2391-
});
2403+
}));
23922404
} else {
23932405
this.$options.updateOnDefault = true;
23942406
}

0 commit comments

Comments
 (0)