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

Commit 0c24058

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 at nearer the top of the DOM and then for more specific settings to override those general settings further down in the DOM. BREAKING CHANGE: Previously, if a setting was not applied on ngModelOptions, then it would default to undefined. Now the setting will be inherited from the nearest ngModelOptions ancestor. It is possible that an ngModelOptions directive that does not set a property, has an ancestor ngModelOptions that does set this property to a value other than undefined. This would cause the ngModel and input controls below this ngModelOptions directive to display different behaviour. This is fixed by explictly setting the property in the ngModelOptions to prevent it from inheriting from the ancestor.
1 parent 2003fcf commit 0c24058

File tree

2 files changed

+209
-128
lines changed

2 files changed

+209
-128
lines changed

src/ng/directive/ngModel.js

+187-128
Original file line numberDiff line numberDiff line change
@@ -1065,9 +1065,41 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
10651065
* @name ngModelOptions
10661066
*
10671067
* @description
1068-
* Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
1069-
* events that will trigger a model update and/or a debouncing delay so that the actual update only
1070-
* takes place when a timer expires; this timer will be reset after another change takes place.
1068+
* This directive allows you to modify the behaviour of ngModel and input directives within your
1069+
* application. You can specify an ngModelOptions directive on any element and the settings affect
1070+
* the ngModel and input directives on all descendent elements.
1071+
*
1072+
* The ngModelOptions settings are found by evaluating the value of the ngModelOptions attribute as
1073+
* an Angular expression. This expression should evaluate to an object, whose properties contain
1074+
* the settings.
1075+
*
1076+
* If a setting is not specified as a property on the object for a particular ngModelOptions directive
1077+
* then it will inherit that setting from the first ngModelOptions directive found by traversing up the
1078+
* DOM tree.
1079+
*
1080+
* For example in the following fragment of HTML ...
1081+
*
1082+
*
1083+
* ```html
1084+
* <div ng-model-options="{ allowInvalid: true }">
1085+
* <form ng-model-options="{ updateOn: \'blur\' }">
1086+
* <input ng-model-options="{ updateOn: \'default\' }">
1087+
* </form>
1088+
* </div>
1089+
* ```
1090+
*
1091+
* ... the `input` element will effective have the follow settings ...
1092+
*
1093+
* ```js
1094+
* { allowInvalid: true, updateOn: 'default' }
1095+
* ```
1096+
*
1097+
*
1098+
* ## Triggering and debouncing model updates
1099+
*
1100+
* The `updateOn` and `debounce` properties allow you to specify a custom list of events that will
1101+
* trigger a model update and/or a debouncing delay so that the actual update only takes place when
1102+
* a timer expires; this timer will be reset after another change takes place.
10711103
*
10721104
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
10731105
* be different from the value in the actual model. This means that if you update the model you
@@ -1083,7 +1115,130 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
10831115
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
10841116
* to have access to the updated model.
10851117
*
1086-
* `ngModelOptions` has an effect on the element it's declared on and its descendants.
1118+
* The following example shows how to override immediate updates. Changes on the inputs within the
1119+
* form will update the model only when the control loses focus (blur event). If `escape` key is
1120+
* pressed while the input field is focused, the value is reset to the value in the current model.
1121+
*
1122+
* <example name="ngModelOptions-directive-blur" module="optionsExample">
1123+
* <file name="index.html">
1124+
* <div ng-controller="ExampleController">
1125+
* <form name="userForm">
1126+
* Name:
1127+
* <input type="text" name="userName"
1128+
* ng-model="user.name"
1129+
* ng-model-options="{ updateOn: 'blur' }"
1130+
* ng-keyup="cancel($event)" /><br />
1131+
*
1132+
* Other data:
1133+
* <input type="text" ng-model="user.data" /><br />
1134+
* </form>
1135+
* <pre>user.name = <span ng-bind="user.name"></span></pre>
1136+
* </div>
1137+
* </file>
1138+
* <file name="app.js">
1139+
* angular.module('optionsExample', [])
1140+
* .controller('ExampleController', ['$scope', function($scope) {
1141+
* $scope.user = { name: 'say', data: '' };
1142+
*
1143+
* $scope.cancel = function(e) {
1144+
* if (e.keyCode == 27) {
1145+
* $scope.userForm.userName.$rollbackViewValue();
1146+
* }
1147+
* };
1148+
* }]);
1149+
* </file>
1150+
* <file name="protractor.js" type="protractor">
1151+
* var model = element(by.binding('user.name'));
1152+
* var input = element(by.model('user.name'));
1153+
* var other = element(by.model('user.data'));
1154+
*
1155+
* it('should allow custom events', function() {
1156+
* input.sendKeys(' hello');
1157+
* input.click();
1158+
* expect(model.getText()).toEqual('say');
1159+
* other.click();
1160+
* expect(model.getText()).toEqual('say hello');
1161+
* });
1162+
*
1163+
* it('should $rollbackViewValue when model changes', function() {
1164+
* input.sendKeys(' hello');
1165+
* expect(input.getAttribute('value')).toEqual('say hello');
1166+
* input.sendKeys(protractor.Key.ESCAPE);
1167+
* expect(input.getAttribute('value')).toEqual('say');
1168+
* other.click();
1169+
* expect(model.getText()).toEqual('say');
1170+
* });
1171+
* </file>
1172+
* </example>
1173+
*
1174+
* The next example shows how to debounce model changes. Model will be updated only 1 sec after last change.
1175+
* If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
1176+
*
1177+
* <example name="ngModelOptions-directive-debounce" module="optionsExample">
1178+
* <file name="index.html">
1179+
* <div ng-controller="ExampleController">
1180+
* <form name="userForm">
1181+
* Name:
1182+
* <input type="text" name="userName"
1183+
* ng-model="user.name"
1184+
* ng-model-options="{ debounce: 1000 }" />
1185+
* <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
1186+
* </form>
1187+
* <pre>user.name = <span ng-bind="user.name"></span></pre>
1188+
* </div>
1189+
* </file>
1190+
* <file name="app.js">
1191+
* angular.module('optionsExample', [])
1192+
* .controller('ExampleController', ['$scope', function($scope) {
1193+
* $scope.user = { name: 'say' };
1194+
* }]);
1195+
* </file>
1196+
* </example>
1197+
*
1198+
* ## Model updates and validation
1199+
*
1200+
* The default behaviour in `ngModel` is that the model value is set to `null` when the validation
1201+
* determines that the value is invalid. By setting the `allowInvalid` property to true, the model
1202+
* will still be updated even if the value is invalid.
1203+
*
1204+
*
1205+
* ## Connecting to the scope
1206+
*
1207+
* By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression
1208+
* on the scope actually refers to a "getter/setter" function rather than the actual value itself.
1209+
*
1210+
* The following example shows how to bind to getter/setters:
1211+
*
1212+
* <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
1213+
* <file name="index.html">
1214+
* <div ng-controller="ExampleController">
1215+
* <form name="userForm">
1216+
* Name:
1217+
* <input type="text" name="userName"
1218+
* ng-model="user.name"
1219+
* ng-model-options="{ getterSetter: true }" />
1220+
* </form>
1221+
* <pre>user.name = <span ng-bind="user.name()"></span></pre>
1222+
* </div>
1223+
* </file>
1224+
* <file name="app.js">
1225+
* angular.module('getterSetterExample', [])
1226+
* .controller('ExampleController', ['$scope', function($scope) {
1227+
* var _name = 'Brian';
1228+
* $scope.user = {
1229+
* name: function(newName) {
1230+
* return angular.isDefined(newName) ? (_name = newName) : _name;
1231+
* }
1232+
* };
1233+
* }]);
1234+
* </file>
1235+
* </example>
1236+
*
1237+
*
1238+
* ## Specifying timezones
1239+
*
1240+
* You can specify the timezone that date/time input directives expect by providing its name in the
1241+
* `timezone` property.
10871242
*
10881243
* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
10891244
* - `updateOn`: string specifying which event should the input be bound to. You can set several
@@ -1096,138 +1251,42 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
10961251
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
10971252
* not validate correctly instead of the default behavior of setting the model to undefined.
10981253
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
1099-
`ngModel` as getters/setters.
1254+
* `ngModel` as getters/setters.
11001255
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
11011256
* `<input type="date">`, `<input type="time">`, ... . Right now, the only supported value is `'UTC'`,
11021257
* otherwise the default timezone of the browser will be used.
1103-
*
1104-
* @example
1105-
1106-
The following example shows how to override immediate updates. Changes on the inputs within the
1107-
form will update the model only when the control loses focus (blur event). If `escape` key is
1108-
pressed while the input field is focused, the value is reset to the value in the current model.
1109-
1110-
<example name="ngModelOptions-directive-blur" module="optionsExample">
1111-
<file name="index.html">
1112-
<div ng-controller="ExampleController">
1113-
<form name="userForm">
1114-
Name:
1115-
<input type="text" name="userName"
1116-
ng-model="user.name"
1117-
ng-model-options="{ updateOn: 'blur' }"
1118-
ng-keyup="cancel($event)" /><br />
1119-
1120-
Other data:
1121-
<input type="text" ng-model="user.data" /><br />
1122-
</form>
1123-
<pre>user.name = <span ng-bind="user.name"></span></pre>
1124-
</div>
1125-
</file>
1126-
<file name="app.js">
1127-
angular.module('optionsExample', [])
1128-
.controller('ExampleController', ['$scope', function($scope) {
1129-
$scope.user = { name: 'say', data: '' };
1130-
1131-
$scope.cancel = function(e) {
1132-
if (e.keyCode == 27) {
1133-
$scope.userForm.userName.$rollbackViewValue();
1134-
}
1135-
};
1136-
}]);
1137-
</file>
1138-
<file name="protractor.js" type="protractor">
1139-
var model = element(by.binding('user.name'));
1140-
var input = element(by.model('user.name'));
1141-
var other = element(by.model('user.data'));
1142-
1143-
it('should allow custom events', function() {
1144-
input.sendKeys(' hello');
1145-
input.click();
1146-
expect(model.getText()).toEqual('say');
1147-
other.click();
1148-
expect(model.getText()).toEqual('say hello');
1149-
});
1150-
1151-
it('should $rollbackViewValue when model changes', function() {
1152-
input.sendKeys(' hello');
1153-
expect(input.getAttribute('value')).toEqual('say hello');
1154-
input.sendKeys(protractor.Key.ESCAPE);
1155-
expect(input.getAttribute('value')).toEqual('say');
1156-
other.click();
1157-
expect(model.getText()).toEqual('say');
1158-
});
1159-
</file>
1160-
</example>
1161-
1162-
This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
1163-
If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
1164-
1165-
<example name="ngModelOptions-directive-debounce" module="optionsExample">
1166-
<file name="index.html">
1167-
<div ng-controller="ExampleController">
1168-
<form name="userForm">
1169-
Name:
1170-
<input type="text" name="userName"
1171-
ng-model="user.name"
1172-
ng-model-options="{ debounce: 1000 }" />
1173-
<button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
1174-
</form>
1175-
<pre>user.name = <span ng-bind="user.name"></span></pre>
1176-
</div>
1177-
</file>
1178-
<file name="app.js">
1179-
angular.module('optionsExample', [])
1180-
.controller('ExampleController', ['$scope', function($scope) {
1181-
$scope.user = { name: 'say' };
1182-
}]);
1183-
</file>
1184-
</example>
1185-
1186-
This one shows how to bind to getter/setters:
1187-
1188-
<example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
1189-
<file name="index.html">
1190-
<div ng-controller="ExampleController">
1191-
<form name="userForm">
1192-
Name:
1193-
<input type="text" name="userName"
1194-
ng-model="user.name"
1195-
ng-model-options="{ getterSetter: true }" />
1196-
</form>
1197-
<pre>user.name = <span ng-bind="user.name()"></span></pre>
1198-
</div>
1199-
</file>
1200-
<file name="app.js">
1201-
angular.module('getterSetterExample', [])
1202-
.controller('ExampleController', ['$scope', function($scope) {
1203-
var _name = 'Brian';
1204-
$scope.user = {
1205-
name: function(newName) {
1206-
return angular.isDefined(newName) ? (_name = newName) : _name;
1207-
}
1208-
};
1209-
}]);
1210-
</file>
1211-
</example>
12121258
*/
12131259
var ngModelOptionsDirective = function() {
12141260
return {
12151261
restrict: 'A',
1216-
controller: ['$scope', '$attrs', function($scope, $attrs) {
1217-
var that = this;
1218-
this.$options = copy($scope.$eval($attrs.ngModelOptions));
1219-
// Allow adding/overriding bound events
1220-
if (this.$options.updateOn !== undefined) {
1221-
this.$options.updateOnDefault = false;
1222-
// extract "default" pseudo-event from list of events that can trigger a model update
1223-
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
1224-
that.$options.updateOnDefault = true;
1225-
return ' ';
1226-
}));
1227-
} else {
1228-
this.$options.updateOnDefault = true;
1262+
// ngModelOptions needs to run before ngModel and input directives
1263+
priority: 10,
1264+
require: ['ngModelOptions', '?^^ngModelOptions'],
1265+
controller: function() {},
1266+
link: {
1267+
pre: function ngModelOptionsPreLinkFn(scope, element, attrs, ctrls) {
1268+
var optionsCtrl = ctrls[0];
1269+
var parentOptions = ctrls[1] ? ctrls[1].$localOptions : {};
1270+
1271+
// Store the raw options taken from the attributes (after inheriting parent options)
1272+
optionsCtrl.$localOptions = extend({}, scope.$eval(attrs.ngModelOptions), parentOptions);
1273+
1274+
// Make a copy and manipulate the options to make them ready to be consumed
1275+
optionsCtrl.$options = copy(optionsCtrl.$localOptions);
1276+
1277+
// Allow adding/overriding bound events
1278+
if (optionsCtrl.$options.updateOn !== undefined) {
1279+
optionsCtrl.$options.updateOnDefault = false;
1280+
// extract "default" pseudo-event from list of events that can trigger a model update
1281+
optionsCtrl.$options.updateOn = trim(optionsCtrl.$options.updateOn.replace(DEFAULT_REGEXP, function() {
1282+
optionsCtrl.$options.updateOnDefault = true;
1283+
return ' ';
1284+
}));
1285+
} else {
1286+
optionsCtrl.$options.updateOnDefault = true;
1287+
}
12291288
}
1230-
}]
1289+
}
12311290
};
12321291
};
12331292

test/ng/directive/ngModelSpec.js

+22
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,28 @@ describe('ngModelOptions attributes', function() {
16671667
}));
16681668

16691669

1670+
it('should inherit options from ngModelOptions directives declared on ancestor elements', function() {
1671+
var container = $compile('<div ng-model-options="{ allowInvalid: true }">' +
1672+
'<form ng-model-options="{ updateOn: \'blur\' }">' +
1673+
'<input ng-model-options="{ updateOn: \'default\' }">' +
1674+
'</form>' +
1675+
'</div>')($rootScope);
1676+
1677+
var form = container.find('form');
1678+
var input = container.find('input');
1679+
1680+
var containerOptions = container.controller('ngModelOptions').$options;
1681+
var formOptions = form.controller('ngModelOptions').$options;
1682+
var inputOptions = input.controller('ngModelOptions').$options;
1683+
1684+
expect(containerOptions.allowInvalid).toEqual(true);
1685+
expect(formOptions.allowInvalid).toEqual(true);
1686+
expect(inputOptions.allowInvalid).toEqual(true);
1687+
1688+
dealoc(container);
1689+
});
1690+
1691+
16701692
it('should allow overriding the model update trigger event on text inputs', function() {
16711693
var inputElm = helper.compileInput(
16721694
'<input type="text" ng-model="name" name="alias" ' +

0 commit comments

Comments
 (0)