diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index b1b5f332ce8a..3edd34e3abe9 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -79,6 +79,7 @@
$jsonpCallbacksProvider,
$LocationProvider,
$LogProvider,
+ $ModelOptionsProvider,
$ParseProvider,
$RootScopeProvider,
$QProvider,
@@ -246,6 +247,7 @@ function publishExternalAPI(angular) {
$jsonpCallbacks: $jsonpCallbacksProvider,
$location: $LocationProvider,
$log: $LogProvider,
+ $modelOptions: $ModelOptionsProvider,
$parse: $ParseProvider,
$rootScope: $RootScopeProvider,
$q: $QProvider,
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 13990b4c0b68..dfb0aadda4ca 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -1424,7 +1424,7 @@ function createDateInputType(type, regexp, parseDate, format) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
badInputChecker(scope, element, attr, ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
- var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
+ var timezone = ctrl && ctrl.$options.getOption('timezone');
var previousDate;
ctrl.$$parserName = type;
diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js
index 6ef8ba408187..59424d0e2173 100644
--- a/src/ng/directive/ngModel.js
+++ b/src/ng/directive/ngModel.js
@@ -5,7 +5,8 @@
PRISTINE_CLASS: true,
DIRTY_CLASS: true,
UNTOUCHED_CLASS: true,
- TOUCHED_CLASS: true
+ TOUCHED_CLASS: true,
+ $ModelOptionsProvider: true
*/
var VALID_CLASS = 'ng-valid',
@@ -220,8 +221,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
*
*
*/
-var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
- /** @this */ function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
+var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', '$modelOptions',
+ /** @this */ function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate, $modelOptions) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
@@ -241,6 +242,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$pending = undefined; // keep pending keys here
this.$name = $interpolate($attr.name || '', false)($scope);
this.$$parentForm = nullFormCtrl;
+ this.$options = $modelOptions;
var parsedNgModel = $parse($attr.ngModel),
parsedNgModelAssign = parsedNgModel.assign,
@@ -250,9 +252,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
parserValid,
ctrl = this;
- this.$$setOptions = function(options) {
- ctrl.$options = options;
- if (options && options.getterSetter) {
+
+ this.$$initGetterSetters = function() {
+
+ if (ctrl.$options.getOption('getterSetter')) {
var invokeModelGetter = $parse($attr.ngModel + '()'),
invokeModelSetter = $parse($attr.ngModel + '($$$p)');
@@ -276,6 +279,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
}
};
+
/**
* @ngdoc method
* @name ngModel.NgModelController#$render
@@ -562,7 +566,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
var prevValid = ctrl.$valid;
var prevModelValue = ctrl.$modelValue;
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
+ var allowInvalid = ctrl.$options.getOption('allowInvalid');
ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
// If there was no change in validity, don't update the model
@@ -724,7 +728,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$modelValue = ngModelGet($scope);
}
var prevModelValue = ctrl.$modelValue;
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
+ var allowInvalid = ctrl.$options.getOption('allowInvalid');
ctrl.$$rawModelValue = modelValue;
if (allowInvalid) {
@@ -815,25 +819,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
*/
this.$setViewValue = function(value, trigger) {
ctrl.$viewValue = value;
- if (!ctrl.$options || ctrl.$options.updateOnDefault) {
+ if (ctrl.$options.getOption('updateOnDefault')) {
ctrl.$$debounceViewValueCommit(trigger);
}
};
this.$$debounceViewValueCommit = function(trigger) {
- var debounceDelay = 0,
- options = ctrl.$options,
- debounce;
-
- if (options && isDefined(options.debounce)) {
- debounce = options.debounce;
- if (isNumber(debounce)) {
- debounceDelay = debounce;
- } else if (isNumber(debounce[trigger])) {
- debounceDelay = debounce[trigger];
- } else if (isNumber(debounce['default'])) {
- debounceDelay = debounce['default'];
- }
+ var options = ctrl.$options,
+ debounceDelay = options.getOption('debounce');
+
+ if (isNumber(debounceDelay[trigger])) {
+ debounceDelay = debounceDelay[trigger];
+ } else if (isNumber(debounceDelay['default'])) {
+ debounceDelay = debounceDelay['default'];
}
$timeout.cancel(pendingDebounce);
@@ -1096,9 +1094,14 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
return {
pre: function ngModelPreLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0],
- formCtrl = ctrls[1] || modelCtrl.$$parentForm;
+ formCtrl = ctrls[1] || modelCtrl.$$parentForm,
+ optionsCtrl = ctrls[2];
+
+ if (optionsCtrl) {
+ modelCtrl.$options = optionsCtrl.$options;
+ }
- modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
+ modelCtrl.$$initGetterSetters();
// notify others, especially parent forms
formCtrl.$addControl(modelCtrl);
@@ -1115,8 +1118,8 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
},
post: function ngModelPostLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
- if (modelCtrl.$options && modelCtrl.$options.updateOn) {
- element.on(modelCtrl.$options.updateOn, function(ev) {
+ if (modelCtrl.$options.getOption('updateOn')) {
+ element.on(modelCtrl.$options.getOption('updateOn'), function(ev) {
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
});
}
@@ -1137,17 +1140,47 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
}];
-
-var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
-
/**
* @ngdoc directive
* @name ngModelOptions
*
* @description
- * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
- * events that will trigger a model update and/or a debouncing delay so that the actual update only
- * takes place when a timer expires; this timer will be reset after another change takes place.
+ * This directive allows you to modify the behaviour of ngModel and input directives within your
+ * application. You can specify an ngModelOptions directive on any element and the settings affect
+ * the ngModel and input directives on all descendent elements.
+ *
+ * The ngModelOptions settings are found by evaluating the value of the ngModelOptions attribute as
+ * an Angular expression. This expression should evaluate to an object, whose properties contain
+ * the settings.
+ *
+ * If a setting is not specified as a property on the object for a particular ngModelOptions directive
+ * then it will inherit that setting from the first ngModelOptions directive found by traversing up the
+ * DOM tree. If there is no ancestor element containing an ngModelOptions directive then the settings in
+ * {@link $modelOptions} will be used.
+ *
+ * For example given the following fragment of HTML
+ *
+ *
+ * ```html
+ *
+ *
+ *
+ * ```
+ *
+ * the `input` element will have the following settings
+ *
+ * ```js
+ * { allowInvalid: true, updateOn: 'default' }
+ * ```
+ *
+ *
+ * ## Triggering and debouncing model updates
+ *
+ * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will
+ * trigger a model update and/or a debouncing delay so that the actual update only takes place when
+ * a timer expires; this timer will be reset after another change takes place.
*
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
* be different from the value in the actual model. This means that if you update the model you
@@ -1163,7 +1196,130 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
* to have access to the updated model.
*
- * `ngModelOptions` has an effect on the element it's declared on and its descendants.
+ * The following example shows how to override immediate updates. Changes on the inputs within the
+ * form will update the model only when the control loses focus (blur event). If `escape` key is
+ * pressed while the input field is focused, the value is reset to the value in the current model.
+ *
+ *
+ *
+ *
+ *
+ *
user.name =
+ *
+ *
+ *
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say', data: '' };
+ *
+ * $scope.cancel = function(e) {
+ * if (e.keyCode === 27) {
+ * $scope.userForm.userName.$rollbackViewValue();
+ * }
+ * };
+ * }]);
+ *
+ *
+ * var model = element(by.binding('user.name'));
+ * var input = element(by.model('user.name'));
+ * var other = element(by.model('user.data'));
+ *
+ * it('should allow custom events', function() {
+ * input.sendKeys(' hello');
+ * input.click();
+ * expect(model.getText()).toEqual('say');
+ * other.click();
+ * expect(model.getText()).toEqual('say hello');
+ * });
+ *
+ * it('should $rollbackViewValue when model changes', function() {
+ * input.sendKeys(' hello');
+ * expect(input.getAttribute('value')).toEqual('say hello');
+ * input.sendKeys(protractor.Key.ESCAPE);
+ * expect(input.getAttribute('value')).toEqual('say');
+ * other.click();
+ * expect(model.getText()).toEqual('say');
+ * });
+ *
+ *
+ *
+ * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change.
+ * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
+ *
+ *
+ *
+ *
+ *
+ *
user.name =
+ *
+ *
+ *
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say' };
+ * }]);
+ *
+ *
+ *
+ * ## Model updates and validation
+ *
+ * The default behaviour in `ngModel` is that the model value is set to `null` when the validation
+ * determines that the value is invalid. By setting the `allowInvalid` property to true, the model
+ * will still be updated even if the value is invalid.
+ *
+ *
+ * ## Connecting to the scope
+ *
+ * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression
+ * on the scope refers to a "getter/setter" function rather than the value itself.
+ *
+ * The following example shows how to bind to getter/setters:
+ *
+ *
+ *
+ *
+ *
+ *
user.name =
+ *
+ *
+ *
+ * angular.module('getterSetterExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * var _name = 'Brian';
+ * $scope.user = {
+ * name: function(newName) {
+ * return angular.isDefined(newName) ? (_name = newName) : _name;
+ * }
+ * };
+ * }]);
+ *
+ *
+ *
+ *
+ * ## Specifying timezones
+ *
+ * You can specify the timezone that date/time input directives expect by providing its name in the
+ * `timezone` property.
*
* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
* - `updateOn`: string specifying which event should the input be bound to. You can set several
@@ -1176,153 +1332,134 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
* not validate correctly instead of the default behavior of setting the model to undefined.
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
- `ngModel` as getters/setters.
+ * `ngModel` as getters/setters.
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
* ``, ``, ... . It understands UTC/GMT and the
* continental US time zone abbreviations, but for general use, use a time zone offset, for
* example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
* If not specified, the timezone of the browser will be used.
*
- * @example
-
- The following example shows how to override immediate updates. Changes on the inputs within the
- form will update the model only when the control loses focus (blur event). If `escape` key is
- pressed while the input field is focused, the value is reset to the value in the current model.
-
-
-
-
-
-
user.name =
-
user.data =
-
-
-
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'John', data: '' };
-
- $scope.cancel = function(e) {
- if (e.keyCode === 27) {
- $scope.userForm.userName.$rollbackViewValue();
- }
- };
- }]);
-
-
- var model = element(by.binding('user.name'));
- var input = element(by.model('user.name'));
- var other = element(by.model('user.data'));
-
- it('should allow custom events', function() {
- input.sendKeys(' Doe');
- input.click();
- expect(model.getText()).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John Doe');
- });
+ */
+var ngModelOptionsDirective = ['$modelOptions', function($modelOptions) {
+ return {
+ restrict: 'A',
+ // ngModelOptions needs to run before ngModel and input directives
+ priority: 10,
+ require: ['ngModelOptions', '?^^ngModelOptions'],
+ controller: function NgModelOptionsController() {},
+ link: {
+ pre: function ngModelOptionsPreLinkFn(scope, element, attrs, ctrls) {
+ var optionsCtrl = ctrls[0];
+ var parentOptions = ctrls[1] ? ctrls[1].$options : $modelOptions;
+ optionsCtrl.$options = parentOptions.createChild(scope.$eval(attrs.ngModelOptions));
+ }
+ }
+ };
+}];
- it('should $rollbackViewValue when model changes', function() {
- input.sendKeys(' Doe');
- expect(input.getAttribute('value')).toEqual('John Doe');
- input.sendKeys(protractor.Key.ESCAPE);
- expect(input.getAttribute('value')).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John');
- });
-
-
- This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
- If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
+/**
+ * @ngdoc provider
+ * @name $modelOptionsProvider
+ * @description
+ *
+ * Here, you can change the default settings from which {@link ngModelOptions}
+ * directives inherit.
+ *
+ * See the {@link ngModelOptions} directive for a list of the available options.
+ */
+function $ModelOptionsProvider() {
+ return {
+ /**
+ * @ngdoc property
+ * @name $modelOptionsProvider#defaultOptions
+ * @type {Object}
+ * @description
+ * The default options to fall back on when there are no more ngModelOption
+ * directives as ancestors.
+ * Use this property to specify the defaultOptions for the application as a whole.
+ *
+ * The initial default options are:
+ *
+ * * `updateOn`: `default`
+ * * `debounce`: `0`
+ * * `allowInvalid`: `undefined`
+ * * `getterSetter`: `undefined`
+ * * `timezone`: 'undefined'
+ */
+ defaultOptions: {
+ updateOn: 'default',
+ debounce: 0
+ },
-
-
-
-
-
user.name =
-
-
-
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'Igor' };
- }]);
-
-
+ /**
+ * @ngdoc service
+ * @name $modelOptions
+ * @type ModelOptions
+ * @description
+ *
+ * This service provides the application wide default {@link ModelOptions} options that
+ * will be used by {@link ngModel} directives if no {@link ngModelOptions} directive is
+ * specified.
+ */
+ $get: function() {
+ return new ModelOptions(this.defaultOptions);
+ }
+ };
+}
- This one shows how to bind to getter/setters:
-
-
-
-
-
user.name =
-
-
-
- angular.module('getterSetterExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- var _name = 'Brian';
- $scope.user = {
- name: function(newName) {
- // Note that newName can be undefined for two reasons:
- // 1. Because it is called as a getter and thus called with no arguments
- // 2. Because the property should actually be set to undefined. This happens e.g. if the
- // input is invalid
- return arguments.length ? (_name = newName) : _name;
- }
- };
- }]);
-
-
+/**
+ * @ngdoc type
+ * @name ModelOptions
+ * @description
+ * A container for the options set by the {@link ngModelOptions} directive
+ * and the {@link $modelOptions} service.
*/
-var ngModelOptionsDirective = function() {
- return {
- restrict: 'A',
- controller: ['$scope', '$attrs', function NgModelOptionsController($scope, $attrs) {
- var that = this;
- this.$options = copy($scope.$eval($attrs.ngModelOptions));
- // Allow adding/overriding bound events
- if (isDefined(this.$options.updateOn)) {
- this.$options.updateOnDefault = false;
- // extract "default" pseudo-event from list of events that can trigger a model update
- this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
- that.$options.updateOnDefault = true;
- return ' ';
- }));
- } else {
- this.$options.updateOnDefault = true;
- }
- }]
- };
-};
+var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
+function ModelOptions(options, parentOptions) {
+
+ // Extend the parent's options with these new ones
+ var _options = extend({}, parentOptions, options);
+
+ // do extra processing on the options
+
+ // updateOn and updateOnDefault
+ if (isDefined(_options.updateOn) && _options.updateOn.trim()) {
+ _options.updateOnDefault = false;
+ // extract "default" pseudo-event from list of events that can trigger a model update
+ _options.updateOn = trim(_options.updateOn.replace(DEFAULT_REGEXP, function() {
+ _options.updateOnDefault = true;
+ return ' ';
+ }));
+ } else if (parentOptions) {
+ _options.updateOn = parentOptions.updateOn;
+ _options.updateOnDefault = parentOptions.updateOnDefault;
+ } else {
+ _options.updateOnDefault = true;
+ }
+ /**
+ * @ngdoc method
+ * @name ModelOptions#getOption
+ * @param {string} name the name of the option to retrieve
+ * @returns {*} the value of the option
+ * @description
+ * Returns the value of the given option
+ */
+ this.getOption = function(name) { return _options[name]; };
+
+ /**
+ * @ngdoc method
+ * @name ModelOptions#createChild
+ * @param {Object} options a hash of options for the new child that will override the parent's options
+ * @return {ModelOptions} a new `ModelOptions` object initialized with the given options.
+ */
+ this.createChild = function(options) {
+ return new ModelOptions(options, _options);
+ };
+}
// helper methods
function addSetValidityMethod(context) {
diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js
index f878c4826070..e1d681956b23 100644
--- a/test/ng/directive/formSpec.js
+++ b/test/ng/directive/formSpec.js
@@ -290,7 +290,7 @@ describe('form', function() {
describe('triggering commit value on submit', function() {
it('should trigger update on form submit', function() {
var form = $compile(
- '')(scope);
scope.$digest();
@@ -305,7 +305,7 @@ describe('form', function() {
it('should trigger update on form submit with nested forms', function() {
var form = $compile(
- '')(scope);
scope.$digest();
@@ -342,7 +342,7 @@ describe('form', function() {
describe('rollback view value', function() {
it('should trigger rollback on form controls', function() {
var form = $compile(
- '')(scope);
@@ -358,7 +358,7 @@ describe('form', function() {
it('should trigger rollback on form controls with nested forms', function() {
var form = $compile(
- '
' +
+ '' +
+ '
')($rootScope);
+ var inputOptions = $rootScope.form.alias.$options;
+
+ expect(inputOptions.getOption('debounce')).toEqual($modelOptions.getOption('debounce'));
+ expect($modelOptions.getOption('updateOnDefault')).toBe(true);
+ expect(inputOptions.getOption('updateOnDefault')).toBe(false);
+ dealoc(form);
+ }));
+
+
it('should allow overriding the model update trigger event on text inputs', function() {
var inputElm = helper.compileInput(
'