Skip to content

Commit 24be474

Browse files
feat(ngModelOptions): allow options to be inherited from ancestor ngModelOptions
Previously, you had to apply the complete set of `ngModelOptions` at every point where you might want to modify just one or two settings. This change allows more general settings to be applied nearer to the top of the DOM and then for more specific settings to override those general settings further down in the DOM. To prevent unwanted inheritance you must opt-in on a case by case basis: * To inherit as single property you simply provide the special value `"$inherit"`. * To inherit all properties not specified locally then include a property `"*": "$inherit"`. Closes angular#10922 Closes angular#15389 BREAKING CHANGE: The programmatic API for `ngModelOptions` has changed. You must now read options via the `ngModelController.getOption(name)` method, rather than accessing the option directly as a property of the `ngModelContoller.$options` object. This does not affect the usage in templates and only affects custom directives that might have been reading options for their own purposes.
1 parent 31f061c commit 24be474

File tree

7 files changed

+1175
-824
lines changed

7 files changed

+1175
-824
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ var angularFiles = {
7070
'src/ng/directive/ngInit.js',
7171
'src/ng/directive/ngList.js',
7272
'src/ng/directive/ngModel.js',
73+
'src/ng/directive/ngModelOptions.js',
7374
'src/ng/directive/ngNonBindable.js',
7475
'src/ng/directive/ngOptions.js',
7576
'src/ng/directive/ngPluralize.js',

src/AngularPublic.js

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
$jsonpCallbacksProvider,
8080
$LocationProvider,
8181
$LogProvider,
82+
$DefaultModelOptionsProvider,
8283
$ParseProvider,
8384
$RootScopeProvider,
8485
$QProvider,
@@ -246,6 +247,7 @@ function publishExternalAPI(angular) {
246247
$jsonpCallbacks: $jsonpCallbacksProvider,
247248
$location: $LocationProvider,
248249
$log: $LogProvider,
250+
$defaultModelOptions: $DefaultModelOptionsProvider,
249251
$parse: $ParseProvider,
250252
$rootScope: $RootScopeProvider,
251253
$q: $QProvider,

src/ng/directive/input.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1427,7 +1427,7 @@ function createDateInputType(type, regexp, parseDate, format) {
14271427
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
14281428
badInputChecker(scope, element, attr, ctrl);
14291429
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1430-
var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
1430+
var timezone = ctrl && ctrl.$options.getOption('timezone');
14311431
var previousDate;
14321432

14331433
ctrl.$$parserName = type;

src/ng/directive/ngModel.js

+23-211
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
222222
*
223223
*
224224
*/
225-
NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate'];
226-
function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) {
225+
NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate', '$defaultModelOptions'];
226+
function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate, $defaultModelOptions) {
227227
this.$viewValue = Number.NaN;
228228
this.$modelValue = Number.NaN;
229229
this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
@@ -243,6 +243,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
243243
this.$pending = undefined; // keep pending keys here
244244
this.$name = $interpolate($attr.name || '', false)($scope);
245245
this.$$parentForm = nullFormCtrl;
246+
this.$options = $defaultModelOptions;
246247

247248
this.$$parsedNgModel = $parse($attr.ngModel);
248249
this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
@@ -267,9 +268,8 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
267268
}
268269

269270
NgModelController.prototype = {
270-
$$setOptions: function(options) {
271-
this.$options = options;
272-
if (options && options.getterSetter) {
271+
$$initGetterSetters: function() {
272+
if (this.$options.getOption('getterSetter')) {
273273
var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'),
274274
invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)');
275275

@@ -543,7 +543,7 @@ NgModelController.prototype = {
543543
var prevValid = this.$valid;
544544
var prevModelValue = this.$modelValue;
545545

546-
var allowInvalid = this.$options && this.$options.allowInvalid;
546+
var allowInvalid = this.$options.getOption('allowInvalid');
547547

548548
var that = this;
549549
this.$$runValidators(modelValue, viewValue, function(allValid) {
@@ -708,7 +708,7 @@ NgModelController.prototype = {
708708
this.$modelValue = this.$$ngModelGet(this.$$scope);
709709
}
710710
var prevModelValue = this.$modelValue;
711-
var allowInvalid = this.$options && this.$options.allowInvalid;
711+
var allowInvalid = this.$options.getOption('allowInvalid');
712712
this.$$rawModelValue = modelValue;
713713

714714
if (allowInvalid) {
@@ -800,25 +800,18 @@ NgModelController.prototype = {
800800
*/
801801
$setViewValue: function(value, trigger) {
802802
this.$viewValue = value;
803-
if (!this.$options || this.$options.updateOnDefault) {
803+
if (this.$options.getOption('updateOnDefault')) {
804804
this.$$debounceViewValueCommit(trigger);
805805
}
806806
},
807807

808808
$$debounceViewValueCommit: function(trigger) {
809-
var debounceDelay = 0,
810-
options = this.$options,
811-
debounce;
812-
813-
if (options && isDefined(options.debounce)) {
814-
debounce = options.debounce;
815-
if (isNumber(debounce)) {
816-
debounceDelay = debounce;
817-
} else if (isNumber(debounce[trigger])) {
818-
debounceDelay = debounce[trigger];
819-
} else if (isNumber(debounce['default'])) {
820-
debounceDelay = debounce['default'];
821-
}
809+
var debounceDelay = this.$options.getOption('debounce');
810+
811+
if (isNumber(debounceDelay[trigger])) {
812+
debounceDelay = debounceDelay[trigger];
813+
} else if (isNumber(debounceDelay['default'])) {
814+
debounceDelay = debounceDelay['default'];
822815
}
823816

824817
this.$$timeout.cancel(this.$$pendingDebounce);
@@ -1116,9 +1109,14 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
11161109
return {
11171110
pre: function ngModelPreLink(scope, element, attr, ctrls) {
11181111
var modelCtrl = ctrls[0],
1119-
formCtrl = ctrls[1] || modelCtrl.$$parentForm;
1112+
formCtrl = ctrls[1] || modelCtrl.$$parentForm,
1113+
optionsCtrl = ctrls[2];
1114+
1115+
if (optionsCtrl) {
1116+
modelCtrl.$options = optionsCtrl.$options;
1117+
}
11201118

1121-
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
1119+
modelCtrl.$$initGetterSetters();
11221120

11231121
// notify others, especially parent forms
11241122
formCtrl.$addControl(modelCtrl);
@@ -1135,8 +1133,8 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
11351133
},
11361134
post: function ngModelPostLink(scope, element, attr, ctrls) {
11371135
var modelCtrl = ctrls[0];
1138-
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
1139-
element.on(modelCtrl.$options.updateOn, function(ev) {
1136+
if (modelCtrl.$options.getOption('updateOn')) {
1137+
element.on(modelCtrl.$options.getOption('updateOn'), function(ev) {
11401138
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
11411139
});
11421140
}
@@ -1159,189 +1157,3 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
11591157
}
11601158
};
11611159
}];
1162-
1163-
1164-
1165-
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
1166-
1167-
/**
1168-
* @ngdoc directive
1169-
* @name ngModelOptions
1170-
*
1171-
* @description
1172-
* Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
1173-
* events that will trigger a model update and/or a debouncing delay so that the actual update only
1174-
* takes place when a timer expires; this timer will be reset after another change takes place.
1175-
*
1176-
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
1177-
* be different from the value in the actual model. This means that if you update the model you
1178-
* should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
1179-
* order to make sure it is synchronized with the model and that any debounced action is canceled.
1180-
*
1181-
* The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
1182-
* method is by making sure the input is placed inside a form that has a `name` attribute. This is
1183-
* important because `form` controllers are published to the related scope under the name in their
1184-
* `name` attribute.
1185-
*
1186-
* Any pending changes will take place immediately when an enclosing form is submitted via the
1187-
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
1188-
* to have access to the updated model.
1189-
*
1190-
* `ngModelOptions` has an effect on the element it's declared on and its descendants.
1191-
*
1192-
* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
1193-
* - `updateOn`: string specifying which event should the input be bound to. You can set several
1194-
* events using an space delimited list. There is a special event called `default` that
1195-
* matches the default events belonging to the control.
1196-
* - `debounce`: integer value which contains the debounce model update value in milliseconds. A
1197-
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
1198-
* custom value for each event. For example:
1199-
* `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
1200-
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
1201-
* not validate correctly instead of the default behavior of setting the model to undefined.
1202-
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
1203-
`ngModel` as getters/setters.
1204-
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
1205-
* `<input type="date" />`, `<input type="time" />`, ... . It understands UTC/GMT and the
1206-
* continental US time zone abbreviations, but for general use, use a time zone offset, for
1207-
* example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
1208-
* If not specified, the timezone of the browser will be used.
1209-
*
1210-
* @example
1211-
1212-
The following example shows how to override immediate updates. Changes on the inputs within the
1213-
form will update the model only when the control loses focus (blur event). If `escape` key is
1214-
pressed while the input field is focused, the value is reset to the value in the current model.
1215-
1216-
<example name="ngModelOptions-directive-blur" module="optionsExample">
1217-
<file name="index.html">
1218-
<div ng-controller="ExampleController">
1219-
<form name="userForm">
1220-
<label>Name:
1221-
<input type="text" name="userName"
1222-
ng-model="user.name"
1223-
ng-model-options="{ updateOn: 'blur' }"
1224-
ng-keyup="cancel($event)" />
1225-
</label><br />
1226-
<label>Other data:
1227-
<input type="text" ng-model="user.data" />
1228-
</label><br />
1229-
</form>
1230-
<pre>user.name = <span ng-bind="user.name"></span></pre>
1231-
<pre>user.data = <span ng-bind="user.data"></span></pre>
1232-
</div>
1233-
</file>
1234-
<file name="app.js">
1235-
angular.module('optionsExample', [])
1236-
.controller('ExampleController', ['$scope', function($scope) {
1237-
$scope.user = { name: 'John', data: '' };
1238-
1239-
$scope.cancel = function(e) {
1240-
if (e.keyCode === 27) {
1241-
$scope.userForm.userName.$rollbackViewValue();
1242-
}
1243-
};
1244-
}]);
1245-
</file>
1246-
<file name="protractor.js" type="protractor">
1247-
var model = element(by.binding('user.name'));
1248-
var input = element(by.model('user.name'));
1249-
var other = element(by.model('user.data'));
1250-
1251-
it('should allow custom events', function() {
1252-
input.sendKeys(' Doe');
1253-
input.click();
1254-
expect(model.getText()).toEqual('John');
1255-
other.click();
1256-
expect(model.getText()).toEqual('John Doe');
1257-
});
1258-
1259-
it('should $rollbackViewValue when model changes', function() {
1260-
input.sendKeys(' Doe');
1261-
expect(input.getAttribute('value')).toEqual('John Doe');
1262-
input.sendKeys(protractor.Key.ESCAPE);
1263-
expect(input.getAttribute('value')).toEqual('John');
1264-
other.click();
1265-
expect(model.getText()).toEqual('John');
1266-
});
1267-
</file>
1268-
</example>
1269-
1270-
This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
1271-
If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
1272-
1273-
<example name="ngModelOptions-directive-debounce" module="optionsExample">
1274-
<file name="index.html">
1275-
<div ng-controller="ExampleController">
1276-
<form name="userForm">
1277-
<label>Name:
1278-
<input type="text" name="userName"
1279-
ng-model="user.name"
1280-
ng-model-options="{ debounce: 1000 }" />
1281-
</label>
1282-
<button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button>
1283-
<br />
1284-
</form>
1285-
<pre>user.name = <span ng-bind="user.name"></span></pre>
1286-
</div>
1287-
</file>
1288-
<file name="app.js">
1289-
angular.module('optionsExample', [])
1290-
.controller('ExampleController', ['$scope', function($scope) {
1291-
$scope.user = { name: 'Igor' };
1292-
}]);
1293-
</file>
1294-
</example>
1295-
1296-
This one shows how to bind to getter/setters:
1297-
1298-
<example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
1299-
<file name="index.html">
1300-
<div ng-controller="ExampleController">
1301-
<form name="userForm">
1302-
<label>Name:
1303-
<input type="text" name="userName"
1304-
ng-model="user.name"
1305-
ng-model-options="{ getterSetter: true }" />
1306-
</label>
1307-
</form>
1308-
<pre>user.name = <span ng-bind="user.name()"></span></pre>
1309-
</div>
1310-
</file>
1311-
<file name="app.js">
1312-
angular.module('getterSetterExample', [])
1313-
.controller('ExampleController', ['$scope', function($scope) {
1314-
var _name = 'Brian';
1315-
$scope.user = {
1316-
name: function(newName) {
1317-
// Note that newName can be undefined for two reasons:
1318-
// 1. Because it is called as a getter and thus called with no arguments
1319-
// 2. Because the property should actually be set to undefined. This happens e.g. if the
1320-
// input is invalid
1321-
return arguments.length ? (_name = newName) : _name;
1322-
}
1323-
};
1324-
}]);
1325-
</file>
1326-
</example>
1327-
*/
1328-
var ngModelOptionsDirective = function() {
1329-
return {
1330-
restrict: 'A',
1331-
controller: ['$scope', '$attrs', function NgModelOptionsController($scope, $attrs) {
1332-
var that = this;
1333-
this.$options = copy($scope.$eval($attrs.ngModelOptions));
1334-
// Allow adding/overriding bound events
1335-
if (isDefined(this.$options.updateOn)) {
1336-
this.$options.updateOnDefault = false;
1337-
// extract "default" pseudo-event from list of events that can trigger a model update
1338-
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
1339-
that.$options.updateOnDefault = true;
1340-
return ' ';
1341-
}));
1342-
} else {
1343-
this.$options.updateOnDefault = true;
1344-
}
1345-
}]
1346-
};
1347-
};

0 commit comments

Comments
 (0)