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

Commit 5c22a78

Browse files
feat($ngModelOptions): add new service to support the ngModelOptions directive
1 parent f5ca853 commit 5c22a78

File tree

4 files changed

+112
-34
lines changed

4 files changed

+112
-34
lines changed

src/AngularPublic.js

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
$HttpBackendProvider,
7070
$LocationProvider,
7171
$LogProvider,
72+
$ModelOptionsProvider,
7273
$ParseProvider,
7374
$RootScopeProvider,
7475
$QProvider,
@@ -224,6 +225,7 @@ function publishExternalAPI(angular) {
224225
$httpBackend: $HttpBackendProvider,
225226
$location: $LocationProvider,
226227
$log: $LogProvider,
228+
$modelOptions: $ModelOptionsProvider,
227229
$parse: $ParseProvider,
228230
$rootScope: $RootScopeProvider,
229231
$q: $QProvider,

src/ng/directive/input.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,7 @@ function createDateInputType(type, regexp, parseDate, format) {
11451145
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
11461146
badInputChecker(scope, element, attr, ctrl);
11471147
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1148-
var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
1148+
var timezone = ctrl && ctrl.$options.timezone;
11491149
var previousDate;
11501150

11511151
ctrl.$$parserName = type;

src/ng/directive/ngModel.js

+76-33
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
DIRTY_CLASS: true,
77
UNTOUCHED_CLASS: true,
88
TOUCHED_CLASS: true,
9+
$ModelOptionsProvider: true,
910
*/
1011

1112
var VALID_CLASS = 'ng-valid',
@@ -217,8 +218,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
217218
*
218219
*
219220
*/
220-
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
221-
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
221+
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', '$modelOptions',
222+
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate, $modelOptions) {
222223
this.$viewValue = Number.NaN;
223224
this.$modelValue = Number.NaN;
224225
this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
@@ -237,7 +238,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
237238
this.$$success = {}; // keep valid keys here
238239
this.$pending = undefined; // keep pending keys here
239240
this.$name = $interpolate($attr.name || '', false)($scope);
240-
241+
this.$options = $modelOptions.defaultOptions;
241242

242243
var parsedNgModel = $parse($attr.ngModel),
243244
parsedNgModelAssign = parsedNgModel.assign,
@@ -246,9 +247,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
246247
pendingDebounce = null,
247248
ctrl = this;
248249

249-
this.$$setOptions = function(options) {
250-
ctrl.$options = options;
251-
if (options && options.getterSetter) {
250+
251+
this.$$initGetterSetters = function() {
252+
253+
if (ctrl.$options.getterSetter) {
252254
var invokeModelGetter = $parse($attr.ngModel + '()'),
253255
invokeModelSetter = $parse($attr.ngModel + '($$$p)');
254256

@@ -272,6 +274,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
272274
}
273275
};
274276

277+
275278
/**
276279
* @ngdoc method
277280
* @name ngModel.NgModelController#$render
@@ -523,7 +526,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
523526
var prevValid = ctrl.$valid;
524527
var prevModelValue = ctrl.$modelValue;
525528

526-
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
529+
var allowInvalid = ctrl.$options.allowInvalid;
527530

528531
ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) {
529532
// If there was no change in validity, don't update the model
@@ -683,7 +686,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
683686
ctrl.$modelValue = ngModelGet($scope);
684687
}
685688
var prevModelValue = ctrl.$modelValue;
686-
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
689+
var allowInvalid = ctrl.$options.allowInvalid;
687690
ctrl.$$rawModelValue = modelValue;
688691

689692
if (allowInvalid) {
@@ -764,25 +767,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
764767
*/
765768
this.$setViewValue = function(value, trigger) {
766769
ctrl.$viewValue = value;
767-
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
770+
if (ctrl.$options.updateOnDefault) {
768771
ctrl.$$debounceViewValueCommit(trigger);
769772
}
770773
};
771774

772775
this.$$debounceViewValueCommit = function(trigger) {
773-
var debounceDelay = 0,
774-
options = ctrl.$options,
775-
debounce;
776-
777-
if (options && isDefined(options.debounce)) {
778-
debounce = options.debounce;
779-
if (isNumber(debounce)) {
780-
debounceDelay = debounce;
781-
} else if (isNumber(debounce[trigger])) {
782-
debounceDelay = debounce[trigger];
783-
} else if (isNumber(debounce['default'])) {
784-
debounceDelay = debounce['default'];
785-
}
776+
var options = ctrl.$options,
777+
debounceDelay = options.debounce;
778+
779+
if (isNumber(debounceDelay[trigger])) {
780+
debounceDelay = debounceDelay[trigger];
781+
} else if (isNumber(debounceDelay['default'])) {
782+
debounceDelay = debounceDelay['default'];
786783
}
787784

788785
$timeout.cancel(pendingDebounce);
@@ -1014,9 +1011,14 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
10141011
return {
10151012
pre: function ngModelPreLink(scope, element, attr, ctrls) {
10161013
var modelCtrl = ctrls[0],
1017-
formCtrl = ctrls[1] || nullFormCtrl;
1014+
formCtrl = ctrls[1] || nullFormCtrl,
1015+
optionsCtrl = ctrls[2];
10181016

1019-
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
1017+
if (optionsCtrl) {
1018+
modelCtrl.$options = optionsCtrl.$options;
1019+
}
1020+
1021+
modelCtrl.$$initGetterSetters();
10201022

10211023
// notify others, especially parent forms
10221024
formCtrl.$addControl(modelCtrl);
@@ -1033,7 +1035,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
10331035
},
10341036
post: function ngModelPostLink(scope, element, attr, ctrls) {
10351037
var modelCtrl = ctrls[0];
1036-
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
1038+
if (modelCtrl.$options.updateOn) {
10371039
element.on(modelCtrl.$options.updateOn, function(ev) {
10381040
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
10391041
});
@@ -1075,9 +1077,10 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
10751077
*
10761078
* If a setting is not specified as a property on the object for a particular ngModelOptions directive
10771079
* then it will inherit that setting from the first ngModelOptions directive found by traversing up the
1078-
* DOM tree.
1080+
* DOM tree. If there is no ancestor element containing an ngModelOptions directive then the settings in
1081+
* {@link $modelOptions `$modelOptions.defaultOptions`} will be used.
10791082
*
1080-
* For example in the following fragment of HTML ...
1083+
* For example given the following fragment of HTML
10811084
*
10821085
*
10831086
* ```html
@@ -1088,7 +1091,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
10881091
* </div>
10891092
* ```
10901093
*
1091-
* ... the `input` element will effective have the following settings ...
1094+
* the `input` element will have the following settings
10921095
*
10931096
* ```js
10941097
* { allowInvalid: true, updateOn: 'default' }
@@ -1205,7 +1208,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
12051208
* ## Connecting to the scope
12061209
*
12071210
* 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.
1211+
* on the scope refers to a "getter/setter" function rather than the value itself.
12091212
*
12101213
* The following example shows how to bind to getter/setters:
12111214
*
@@ -1256,7 +1259,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
12561259
* `<input type="date">`, `<input type="time">`, ... . Right now, the only supported value is `'UTC'`,
12571260
* otherwise the default timezone of the browser will be used.
12581261
*/
1259-
var ngModelOptionsDirective = function() {
1262+
var ngModelOptionsDirective = ['$modelOptions', function($modelOptions) {
12601263
return {
12611264
restrict: 'A',
12621265
// ngModelOptions needs to run before ngModel and input directives
@@ -1266,10 +1269,10 @@ var ngModelOptionsDirective = function() {
12661269
link: {
12671270
pre: function ngModelOptionsPreLinkFn(scope, element, attrs, ctrls) {
12681271
var optionsCtrl = ctrls[0];
1269-
var parentOptions = ctrls[1] ? ctrls[1].$localOptions : {};
1272+
var parentOptions = ctrls[1] ? ctrls[1].$localOptions : $modelOptions.defaultOptions;
12701273

12711274
// Store the raw options taken from the attributes (after inheriting parent options)
1272-
optionsCtrl.$localOptions = extend({}, scope.$eval(attrs.ngModelOptions), parentOptions);
1275+
optionsCtrl.$localOptions = extend({}, parentOptions, scope.$eval(attrs.ngModelOptions));
12731276

12741277
// Make a copy and manipulate the options to make them ready to be consumed
12751278
optionsCtrl.$options = copy(optionsCtrl.$localOptions);
@@ -1288,7 +1291,47 @@ var ngModelOptionsDirective = function() {
12881291
}
12891292
}
12901293
};
1291-
};
1294+
}];
1295+
1296+
1297+
/**
1298+
* @ngdoc service
1299+
* @name $modelOptions
1300+
* @description
1301+
*
1302+
* This service provides support to the {@link ngModelOptions} directive.
1303+
*
1304+
* Here, you can change the default settings from which {@link ngModelOptions}
1305+
* directives inherit.
1306+
*
1307+
* See the {@link ngModelOptions} directive for a list of the available options.
1308+
*
1309+
*/
1310+
function $ModelOptionsProvider() {
1311+
return {
1312+
$get: function() {
1313+
return {
1314+
/**
1315+
* @ngdoc property
1316+
* @name $modelOptions#defaultOptions
1317+
* @type {Object}
1318+
* @description
1319+
* The default options to fall back on when there are no more ngModelOption
1320+
* directives as ancestors
1321+
*
1322+
* The initial default options are:
1323+
*
1324+
* * `updateOneDefault`: `true`
1325+
* * `debounce`: `0`
1326+
*/
1327+
defaultOptions: {
1328+
updateOnDefault: true,
1329+
debounce: 0
1330+
}
1331+
};
1332+
}
1333+
};
1334+
}
12921335

12931336

12941337

test/ng/directive/ngModelSpec.js

+33
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,39 @@ describe('ngModelOptions attributes', function() {
16891689
});
16901690

16911691

1692+
it('should inherit options from $modelOptions.defaultOptions if there is no ngModelOptions directive', inject(function($modelOptions) {
1693+
var inputElm = helper.compileInput(
1694+
'<input type="text" ng-model="name" name="alias" />');
1695+
1696+
var inputOptions = $rootScope.form.alias.$options;
1697+
expect(inputOptions).toEqual($modelOptions.defaultOptions);
1698+
}));
1699+
1700+
1701+
it('should inherit options from $modelOptions.defaultOptions that are not specified on the input element', inject(function($modelOptions) {
1702+
var inputElm = helper.compileInput(
1703+
'<input type="text" ng-model="name" name="alias" ng-model-options="{ updateOn: \'blur\' }"/>');
1704+
1705+
var inputOptions = $rootScope.form.alias.$options;
1706+
expect(inputOptions.debounce).toEqual($modelOptions.defaultOptions.debounce);
1707+
expect($modelOptions.defaultOptions.updateOnDefault).toBe(true);
1708+
expect(inputOptions.updateOnDefault).toBe(false);
1709+
}));
1710+
1711+
1712+
it('should inherit options from $modelOptions.defaultOptions that are not specified in an ngModelOptions directive', inject(function($modelOptions) {
1713+
var form = $compile('<form name="form" ng-model-options="{ updateOn: \'blur\' }">' +
1714+
'<input name="alias" ng-model="x">' +
1715+
'</form>')($rootScope);
1716+
var inputOptions = $rootScope.form.alias.$options;
1717+
1718+
expect(inputOptions.debounce).toEqual($modelOptions.defaultOptions.debounce);
1719+
expect($modelOptions.defaultOptions.updateOnDefault).toBe(true);
1720+
expect(inputOptions.updateOnDefault).toBe(false);
1721+
dealoc(form);
1722+
}));
1723+
1724+
16921725
it('should allow overriding the model update trigger event on text inputs', function() {
16931726
var inputElm = helper.compileInput(
16941727
'<input type="text" ng-model="name" name="alias" ' +

0 commit comments

Comments
 (0)