diff --git a/angularFiles.js b/angularFiles.js
index 4769808ad391..311a39139322 100644
--- a/angularFiles.js
+++ b/angularFiles.js
@@ -70,6 +70,7 @@ var angularFiles = {
'src/ng/directive/ngInit.js',
'src/ng/directive/ngList.js',
'src/ng/directive/ngModel.js',
+ 'src/ng/directive/ngModelOptions.js',
'src/ng/directive/ngNonBindable.js',
'src/ng/directive/ngOptions.js',
'src/ng/directive/ngPluralize.js',
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index 3edd34e3abe9..b1b5f332ce8a 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -79,7 +79,6 @@
$jsonpCallbacksProvider,
$LocationProvider,
$LogProvider,
- $ModelOptionsProvider,
$ParseProvider,
$RootScopeProvider,
$QProvider,
@@ -247,7 +246,6 @@ function publishExternalAPI(angular) {
$jsonpCallbacks: $jsonpCallbacksProvider,
$location: $LocationProvider,
$log: $LogProvider,
- $modelOptions: $ModelOptionsProvider,
$parse: $ParseProvider,
$rootScope: $RootScopeProvider,
$q: $QProvider,
diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js
index 887646416faf..f332b823866e 100644
--- a/src/ng/directive/ngModel.js
+++ b/src/ng/directive/ngModel.js
@@ -7,11 +7,12 @@
UNTOUCHED_CLASS: true,
TOUCHED_CLASS: true,
PENDING_CLASS: true,
- $ModelOptionsProvider: true,
addSetValidityMethod: true,
- setupValidity: true
+ setupValidity: true,
+ $defaultModelOptions: false
*/
+
var VALID_CLASS = 'ng-valid',
INVALID_CLASS = 'ng-invalid',
PRISTINE_CLASS = 'ng-pristine',
@@ -223,8 +224,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
*
*
*/
-NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate', '$modelOptions'];
-function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate, $modelOptions) {
+NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate'];
+function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
@@ -244,7 +245,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
this.$pending = undefined; // keep pending keys here
this.$name = $interpolate($attr.name || '', false)($scope);
this.$$parentForm = nullFormCtrl;
- this.$options = $modelOptions;
+ this.$options = $defaultModelOptions;
this.$$parsedNgModel = $parse($attr.ngModel);
this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
@@ -1158,330 +1159,3 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
}
};
}];
-
-
-/**
- * @ngdoc directive
- * @name ngModelOptions
- *
- * @description
- * 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
- * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
- * order to make sure it is synchronized with the model and that any debounced action is canceled.
- *
- * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
- * method is by making sure the input is placed inside a form that has a `name` attribute. This is
- * important because `form` controllers are published to the related scope under the name in their
- * `name` attribute.
- *
- * Any pending changes will take place immediately when an enclosing form is submitted via the
- * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
- * to have access to the updated model.
- *
- * 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 `undefined` 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
- * events using an space delimited list. There is a special event called `default` that
- * matches the default events belonging to the control.
- * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
- * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
- * custom value for each event. For example:
- * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
- * - `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.
- * - `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.
- *
- */
-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));
- }
- }
- };
-}];
-
-
-/**
- * @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
- },
-
- /**
- * @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);
- }
- };
-}
-
-
-/**
- * @ngdoc type
- * @name ModelOptions
- * @description
- * A container for the options set by the {@link ngModelOptions} directive
- * and the {@link $modelOptions} service.
- */
-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);
- };
-}
diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js
new file mode 100644
index 000000000000..cc3fe53bd2ec
--- /dev/null
+++ b/src/ng/directive/ngModelOptions.js
@@ -0,0 +1,353 @@
+'use strict';
+
+/* exported $defaultModelOptions */
+var $defaultModelOptions;
+var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
+
+/**
+ * @ngdoc type
+ * @name ModelOptions
+ * @description
+ * A container for the options set by the {@link ngModelOptions} directive
+ */
+function ModelOptions(options) {
+ this.$$options = options;
+}
+
+ModelOptions.prototype = {
+
+ /**
+ * @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
+ */
+ getOption: function(name) {
+ return this.$$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.
+ */
+ createChild: function(options) {
+ var inheritAll = false;
+
+ // make a shallow copy
+ options = extend({}, options);
+
+ // Inherit options from the parent if specified by the value `"$inherit"`
+ forEach(options, /* @this */ function(option, key) {
+ if (option === '$inherit') {
+ if (key === '*') {
+ inheritAll = true;
+ } else {
+ options[key] = this.$$options[key];
+ // `updateOn` is special so we must also inherit the `updateOnDefault` option
+ if (key === 'updateOn') {
+ options.updateOnDefault = this.$$options.updateOnDefault;
+ }
+ }
+ } else {
+ if (key === 'updateOn') {
+ // If the `updateOn` property contains the `default` event then we have to remove
+ // it from the event list and set the `updateOnDefault` flag.
+ options.updateOnDefault = false;
+ options[key] = trim(option.replace(DEFAULT_REGEXP, function() {
+ options.updateOnDefault = true;
+ return ' ';
+ }));
+ }
+ }
+ }, this);
+
+ if (inheritAll) {
+ // We have a property of the form: `"*": "$inherit"`
+ delete options['*'];
+ defaults(options, this.$$options);
+ }
+
+ // Finally add in any missing defaults
+ defaults(options, $defaultModelOptions.$$options);
+
+ return new ModelOptions(options);
+ }
+};
+
+
+$defaultModelOptions = new ModelOptions({
+ updateOn: '',
+ updateOnDefault: true,
+ debounce: 0,
+ getterSetter: false,
+ allowInvalid: false,
+ timezone: null
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ngModelOptions
+ *
+ * @description
+ * This directive allows you to modify the behaviour of {@link ngModel} directives within your
+ * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel}
+ * directives will use the options of their nearest `ngModelOptions` ancestor.
+ *
+ * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as
+ * an Angular expression. This expression should evaluate to an object, whose properties contain
+ * the settings. For example: `
+ *
+ *
+ * ```
+ *
+ * the `input` element will have the following settings
+ *
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 0 }
+ * ```
+ *
+ * Notice that the `debounce` setting was not inherited and used the default value instead.
+ *
+ * You can specify that all undefined settings are automatically inherited from an ancestor by
+ * including a property with key of `"*"` and value of `"$inherit"`.
+ *
+ * For example given the following fragment of HTML
+ *
+ *
+ * ```html
+ *
+ *
+ *
+ * ```
+ *
+ * the `input` element will have the following settings
+ *
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 200 }
+ * ```
+ *
+ * Notice that the `debounce` setting now inherits the value from the outer `
` element.
+ *
+ * If you are creating a reusable component then you should be careful when using `"*": "$inherit"`
+ * since you may inadvertently inherit a setting in the future that changes the behavior of your component.
+ *
+ *
+ * ## 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
+ * should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in
+ * order to make sure it is synchronized with the model and that any debounced action is canceled.
+ *
+ * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue}
+ * method is by making sure the input is placed inside a form that has a `name` attribute. This is
+ * important because `form` controllers are published to the related scope under the name in their
+ * `name` attribute.
+ *
+ * Any pending changes will take place immediately when an enclosing form is submitted via the
+ * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
+ * to have access to the updated model.
+ *
+ * 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 `undefined` 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 {@link ngModel} directives on this element and
+ * and its descendents. Valid keys are:
+ * - `updateOn`: string specifying which event should the input be bound to. You can set several
+ * events using an space delimited list. There is a special event called `default` that
+ * matches the default events belonging to the control.
+ * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
+ * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
+ * custom value for each event. For example:
+ * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
+ * - `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.
+ * - `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.
+ *
+ */
+var ngModelOptionsDirective = function() {
+ 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 : $defaultModelOptions;
+ optionsCtrl.$options = parentOptions.createChild(scope.$eval(attrs.ngModelOptions));
+ }
+ }
+ };
+};
+
+
+// shallow copy over values from `src` that are not already specified on `dst`
+function defaults(dst, src) {
+ forEach(src, function(value, key) {
+ if (!isDefined(dst[key])) {
+ dst[key] = value;
+ }
+ });
+}
diff --git a/test/ng/directive/ngModelOptionsSpec.js b/test/ng/directive/ngModelOptionsSpec.js
new file mode 100644
index 000000000000..7d813d9b459e
--- /dev/null
+++ b/test/ng/directive/ngModelOptionsSpec.js
@@ -0,0 +1,810 @@
+'use strict';
+
+/* globals
+ generateInputCompilerHelper: false,
+ $defaultModelOptions: false
+ */
+describe('ngModelOptions', function() {
+
+ describe('$defaultModelOptions', function() {
+ it('should provide default values', function() {
+ expect($defaultModelOptions.getOption('updateOn')).toEqual('');
+ expect($defaultModelOptions.getOption('updateOnDefault')).toEqual(true);
+ expect($defaultModelOptions.getOption('debounce')).toBe(0);
+ expect($defaultModelOptions.getOption('getterSetter')).toBe(false);
+ expect($defaultModelOptions.getOption('allowInvalid')).toBe(false);
+ expect($defaultModelOptions.getOption('timezone')).toBe(null);
+ });
+ });
+
+ describe('directive', function() {
+
+ var helper = {}, $rootScope, $compile, $timeout, $q;
+
+ generateInputCompilerHelper(helper);
+
+ beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_, _$q_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $timeout = _$timeout_;
+ $q = _$q_;
+ }));
+
+
+ describe('should fall back to `$defaultModelOptions`', function() {
+ it('if there is no `ngModelOptions` directive', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ var inputOptions = $rootScope.form.alias.$options;
+ expect(inputOptions.getOption('updateOn')).toEqual($defaultModelOptions.getOption('updateOn'));
+ expect(inputOptions.getOption('updateOnDefault')).toEqual($defaultModelOptions.getOption('updateOnDefault'));
+ expect(inputOptions.getOption('debounce')).toEqual($defaultModelOptions.getOption('debounce'));
+ expect(inputOptions.getOption('getterSetter')).toEqual($defaultModelOptions.getOption('getterSetter'));
+ expect(inputOptions.getOption('allowInvalid')).toEqual($defaultModelOptions.getOption('allowInvalid'));
+ expect(inputOptions.getOption('timezone')).toEqual($defaultModelOptions.getOption('timezone'));
+ });
+
+
+ it('if `ngModelOptions` on the same element does not specify the option', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ var inputOptions = $rootScope.form.alias.$options;
+ expect(inputOptions.getOption('debounce')).toEqual($defaultModelOptions.getOption('debounce'));
+ expect(inputOptions.getOption('updateOnDefault')).toBe(false);
+ expect(inputOptions.getOption('updateOnDefault')).not.toEqual($defaultModelOptions.getOption('updateOnDefault'));
+ });
+
+
+ it('if the first `ngModelOptions` ancestor does not specify the option', function() {
+ var form = $compile('')($rootScope);
+ var inputOptions = $rootScope.form.alias.$options;
+
+ expect(inputOptions.getOption('debounce')).toEqual($defaultModelOptions.getOption('debounce'));
+ expect(inputOptions.getOption('updateOnDefault')).toBe(false);
+ expect(inputOptions.getOption('updateOnDefault')).not.toEqual($defaultModelOptions.getOption('updateOnDefault'));
+ dealoc(form);
+ });
+ });
+
+
+ describe('sharing and inheritance', function() {
+
+ it('should not inherit options from ancestor `ngModelOptions` directives by default', function() {
+ var container = $compile(
+ '
' +
+ '' +
+ '
')($rootScope);
+
+ var form = container.find('form');
+ var input = container.find('input');
+
+ var containerOptions = container.controller('ngModelOptions').$options;
+ var formOptions = form.controller('ngModelOptions').$options;
+ var inputOptions = input.controller('ngModelOptions').$options;
+
+ expect(containerOptions.getOption('allowInvalid')).toEqual(true);
+ expect(formOptions.getOption('allowInvalid')).toEqual(false);
+ expect(inputOptions.getOption('allowInvalid')).toEqual(false);
+
+ expect(containerOptions.getOption('updateOn')).toEqual('');
+ expect(containerOptions.getOption('updateOnDefault')).toEqual(true);
+ expect(formOptions.getOption('updateOn')).toEqual('blur');
+ expect(formOptions.getOption('updateOnDefault')).toEqual(false);
+ expect(inputOptions.getOption('updateOn')).toEqual('');
+ expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
+
+ dealoc(container);
+ });
+
+ it('should inherit options that are marked with "$inherit" from the nearest ancestor `ngModelOptions` directive', function() {
+ var container = $compile(
+ '
' +
+ '' +
+ '
')($rootScope);
+
+ var form = container.find('form');
+ var input = container.find('input');
+
+ var containerOptions = container.controller('ngModelOptions').$options;
+ var formOptions = form.controller('ngModelOptions').$options;
+ var inputOptions = input.controller('ngModelOptions').$options;
+
+ expect(containerOptions.getOption('allowInvalid')).toEqual(true);
+ expect(formOptions.getOption('allowInvalid')).toEqual(true);
+ expect(inputOptions.getOption('allowInvalid')).toEqual(false);
+
+ expect(containerOptions.getOption('updateOn')).toEqual('');
+ expect(containerOptions.getOption('updateOnDefault')).toEqual(true);
+ expect(formOptions.getOption('updateOn')).toEqual('blur');
+ expect(formOptions.getOption('updateOnDefault')).toEqual(false);
+ expect(inputOptions.getOption('updateOn')).toEqual('');
+ expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
+
+ dealoc(container);
+ });
+
+ it('should inherit all unspecified options if the options object contains a `"*"` property with value "$inherit"', function() {
+ var container = $compile(
+ '
' +
+ '' +
+ '
')($rootScope);
+
+ var form = container.find('form');
+ var input = container.find('input');
+
+ var containerOptions = container.controller('ngModelOptions').$options;
+ var formOptions = form.controller('ngModelOptions').$options;
+ var inputOptions = input.controller('ngModelOptions').$options;
+
+ expect(containerOptions.getOption('allowInvalid')).toEqual(true);
+ expect(formOptions.getOption('allowInvalid')).toEqual(true);
+ expect(inputOptions.getOption('allowInvalid')).toEqual(false);
+
+ expect(containerOptions.getOption('debounce')).toEqual(100);
+ expect(formOptions.getOption('debounce')).toEqual(100);
+ expect(inputOptions.getOption('debounce')).toEqual(0);
+
+ expect(containerOptions.getOption('updateOn')).toEqual('keyup');
+ expect(containerOptions.getOption('updateOnDefault')).toEqual(false);
+ expect(formOptions.getOption('updateOn')).toEqual('blur');
+ expect(formOptions.getOption('updateOnDefault')).toEqual(false);
+ expect(inputOptions.getOption('updateOn')).toEqual('');
+ expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
+
+ dealoc(container);
+ });
+
+ it('should correctly inherit default and another specified event for `updateOn`', function() {
+ var container = $compile(
+ '
' +
+ '' +
+ '
')($rootScope);
+
+ var input = container.find('input');
+ var inputOptions = input.controller('ngModelOptions').$options;
+
+ expect(inputOptions.getOption('updateOn')).toEqual('blur');
+ expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
+
+ dealoc(container);
+ });
+
+
+ it('should `updateOnDefault` as well if we have `updateOn: "$inherit"`', function() {
+ var container = $compile(
+ '
' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '
')($rootScope);
+
+ var input1 = container.find('input').eq(0);
+ var inputOptions1 = input1.controller('ngModelOptions').$options;
+
+ expect(inputOptions1.getOption('updateOn')).toEqual('keyup');
+ expect(inputOptions1.getOption('updateOnDefault')).toEqual(false);
+
+ var input2 = container.find('input').eq(1);
+ var inputOptions2 = input2.controller('ngModelOptions').$options;
+
+ expect(inputOptions2.getOption('updateOn')).toEqual('blur');
+ expect(inputOptions2.getOption('updateOnDefault')).toEqual(true);
+
+ dealoc(container);
+ });
+
+
+ it('should make a copy of the options object', function() {
+ $rootScope.options = {updateOn: 'default'};
+ var inputElm = helper.compileInput(
+ '');
+ expect($rootScope.options).toEqual({updateOn: 'default'});
+ expect($rootScope.form.alias.$options).not.toBe($rootScope.options);
+ });
+
+ it('should be retrieved from an ancestor element containing an `ngModelOptions` directive', function() {
+ var doc = $compile(
+ '')($rootScope);
+ $rootScope.$digest();
+
+ var inputElm = doc.find('input');
+ helper.changeGivenInputTo(inputElm, 'a');
+ expect($rootScope.name).toEqual(undefined);
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.name).toBeUndefined();
+ $timeout.flush(2000);
+ expect($rootScope.name).toBeUndefined();
+ $timeout.flush(9000);
+ expect($rootScope.name).toEqual('a');
+ dealoc(doc);
+ });
+
+ it('should allow sharing options between multiple inputs', function() {
+ $rootScope.options = {updateOn: 'default'};
+ var inputElm = helper.compileInput(
+ '' +
+ '');
+
+ helper.changeGivenInputTo(inputElm.eq(0), 'a');
+ helper.changeGivenInputTo(inputElm.eq(1), 'b');
+ expect($rootScope.name1).toEqual('a');
+ expect($rootScope.name2).toEqual('b');
+ });
+ });
+
+
+ describe('updateOn', function() {
+ it('should allow overriding the model update trigger event on text inputs', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.name).toBeUndefined();
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.name).toEqual('a');
+ });
+
+
+ it('should not dirty the input if nothing was changed before updateOn trigger', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.form.alias.$pristine).toBeTruthy();
+ });
+
+
+ it('should allow overriding the model update trigger event on text areas', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.name).toBeUndefined();
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.name).toEqual('a');
+ });
+
+
+ it('should bind the element to a list of events', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.name).toBeUndefined();
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.name).toEqual('a');
+
+ helper.changeInputValueTo('b');
+ expect($rootScope.name).toEqual('a');
+ browserTrigger(inputElm, 'mousemove');
+ expect($rootScope.name).toEqual('b');
+ });
+
+
+ it('should allow keeping the default update behavior on text inputs', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.name).toEqual('a');
+ });
+
+
+ it('should allow overriding the model update trigger event on checkboxes', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ browserTrigger(inputElm, 'click');
+ expect($rootScope.checkbox).toBeUndefined();
+
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.checkbox).toBe(true);
+
+ browserTrigger(inputElm, 'click');
+ expect($rootScope.checkbox).toBe(true);
+ });
+
+
+ it('should allow keeping the default update behavior on checkboxes', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ browserTrigger(inputElm, 'click');
+ expect($rootScope.checkbox).toBe(true);
+
+ browserTrigger(inputElm, 'click');
+ expect($rootScope.checkbox).toBe(false);
+ });
+
+
+ it('should allow overriding the model update trigger event on radio buttons', function() {
+ var inputElm = helper.compileInput(
+ '' +
+ '' +
+ '');
+
+ $rootScope.$apply('color = \'white\'');
+ browserTrigger(inputElm[2], 'click');
+ expect($rootScope.color).toBe('white');
+
+ browserTrigger(inputElm[2], 'blur');
+ expect($rootScope.color).toBe('blue');
+
+ });
+
+
+ it('should allow keeping the default update behavior on radio buttons', function() {
+ var inputElm = helper.compileInput(
+ '' +
+ '' +
+ '');
+
+ $rootScope.$apply('color = \'white\'');
+ browserTrigger(inputElm[2], 'click');
+ expect($rootScope.color).toBe('blue');
+ });
+ });
+
+
+ describe('debounce', function() {
+ it('should trigger only after timeout in text inputs', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ helper.changeInputValueTo('b');
+ helper.changeInputValueTo('c');
+ expect($rootScope.name).toEqual(undefined);
+ $timeout.flush(2000);
+ expect($rootScope.name).toEqual(undefined);
+ $timeout.flush(9000);
+ expect($rootScope.name).toEqual('c');
+ });
+
+
+ it('should trigger only after timeout in checkboxes', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ browserTrigger(inputElm, 'click');
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(2000);
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(9000);
+ expect($rootScope.checkbox).toBe(true);
+ });
+
+
+ it('should trigger only after timeout in radio buttons', function() {
+ var inputElm = helper.compileInput(
+ '' +
+ '' +
+ '');
+
+ browserTrigger(inputElm[0], 'click');
+ expect($rootScope.color).toBe('white');
+ browserTrigger(inputElm[1], 'click');
+ expect($rootScope.color).toBe('white');
+ $timeout.flush(12000);
+ expect($rootScope.color).toBe('white');
+ $timeout.flush(10000);
+ expect($rootScope.color).toBe('red');
+
+ });
+
+
+ it('should not trigger digest while debouncing', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ var watchSpy = jasmine.createSpy('watchSpy');
+ $rootScope.$watch(watchSpy);
+
+ helper.changeInputValueTo('a');
+ expect(watchSpy).not.toHaveBeenCalled();
+
+ $timeout.flush(10000);
+ expect(watchSpy).toHaveBeenCalled();
+ });
+
+
+ it('should allow selecting different debounce timeouts for each event',
+ function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(6000);
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(4000);
+ expect($rootScope.name).toEqual('a');
+ helper.changeInputValueTo('b');
+ browserTrigger(inputElm, 'blur');
+ $timeout.flush(4000);
+ expect($rootScope.name).toEqual('a');
+ $timeout.flush(2000);
+ expect($rootScope.name).toEqual('b');
+ });
+
+
+ it('should allow selecting different debounce timeouts for each event on checkboxes', function() {
+ var inputElm = helper.compileInput('');
+
+ inputElm[0].checked = false;
+ browserTrigger(inputElm, 'click');
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(8000);
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(3000);
+ expect($rootScope.checkbox).toBe(true);
+ inputElm[0].checked = true;
+ browserTrigger(inputElm, 'click');
+ browserTrigger(inputElm, 'blur');
+ $timeout.flush(3000);
+ expect($rootScope.checkbox).toBe(true);
+ $timeout.flush(3000);
+ expect($rootScope.checkbox).toBe(false);
+ });
+
+
+ it('should allow selecting 0 for non-default debounce timeouts for each event on checkboxes', function() {
+ var inputElm = helper.compileInput('');
+
+ inputElm[0].checked = false;
+ browserTrigger(inputElm, 'click');
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(8000);
+ expect($rootScope.checkbox).toBeUndefined();
+ $timeout.flush(3000);
+ expect($rootScope.checkbox).toBe(true);
+ inputElm[0].checked = true;
+ browserTrigger(inputElm, 'click');
+ browserTrigger(inputElm, 'blur');
+ $timeout.flush(0);
+ expect($rootScope.checkbox).toBe(false);
+ });
+
+
+ it('should flush debounced events when calling $commitViewValue directly', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.name).toEqual(undefined);
+ $rootScope.form.alias.$commitViewValue();
+ expect($rootScope.name).toEqual('a');
+ });
+
+ it('should cancel debounced events when calling $commitViewValue', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ $rootScope.form.alias.$commitViewValue();
+ expect($rootScope.name).toEqual('a');
+
+ $rootScope.form.alias.$setPristine();
+ $timeout.flush(1000);
+ expect($rootScope.form.alias.$pristine).toBeTruthy();
+ });
+
+
+ it('should reset input val if rollbackViewValue called during pending update', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect(inputElm.val()).toBe('a');
+ $rootScope.form.alias.$rollbackViewValue();
+ expect(inputElm.val()).toBe('');
+ browserTrigger(inputElm, 'blur');
+ expect(inputElm.val()).toBe('');
+ });
+
+
+ it('should allow canceling pending updates', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.name).toEqual(undefined);
+ $rootScope.form.alias.$rollbackViewValue();
+ expect($rootScope.name).toEqual(undefined);
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.name).toEqual(undefined);
+ });
+
+
+ it('should allow canceling debounced updates', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect($rootScope.name).toEqual(undefined);
+ $timeout.flush(2000);
+ $rootScope.form.alias.$rollbackViewValue();
+ expect($rootScope.name).toEqual(undefined);
+ $timeout.flush(10000);
+ expect($rootScope.name).toEqual(undefined);
+ });
+
+
+ it('should handle model updates correctly even if rollbackViewValue is not invoked', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ $rootScope.$apply('name = \'b\'');
+ browserTrigger(inputElm, 'blur');
+ expect($rootScope.name).toBe('b');
+ });
+
+
+ it('should reset input val if rollbackViewValue called during debounce', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ helper.changeInputValueTo('a');
+ expect(inputElm.val()).toBe('a');
+ $rootScope.form.alias.$rollbackViewValue();
+ expect(inputElm.val()).toBe('');
+ $timeout.flush(3000);
+ expect(inputElm.val()).toBe('');
+ });
+ });
+
+
+ describe('getterSetter', function() {
+ it('should not try to invoke a model if getterSetter is false', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ var spy = $rootScope.name = jasmine.createSpy('setterSpy');
+ helper.changeInputValueTo('a');
+ expect(spy).not.toHaveBeenCalled();
+ expect(inputElm.val()).toBe('a');
+ });
+
+
+ it('should not try to invoke a model if getterSetter is not set', function() {
+ var inputElm = helper.compileInput('');
+
+ var spy = $rootScope.name = jasmine.createSpy('setterSpy');
+ helper.changeInputValueTo('a');
+ expect(spy).not.toHaveBeenCalled();
+ expect(inputElm.val()).toBe('a');
+ });
+
+
+ it('should try to invoke a function model if getterSetter is true', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ var spy = $rootScope.name = jasmine.createSpy('setterSpy').and.callFake(function() {
+ return 'b';
+ });
+ $rootScope.$apply();
+ expect(inputElm.val()).toBe('b');
+
+ helper.changeInputValueTo('a');
+ expect(inputElm.val()).toBe('b');
+ expect(spy).toHaveBeenCalledWith('a');
+ expect($rootScope.name).toBe(spy);
+ });
+
+
+ it('should assign to non-function models if getterSetter is true', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ $rootScope.name = 'c';
+ helper.changeInputValueTo('d');
+ expect(inputElm.val()).toBe('d');
+ expect($rootScope.name).toBe('d');
+ });
+
+
+ it('should fail on non-assignable model binding if getterSetter is false', function() {
+ expect(function() {
+ var inputElm = helper.compileInput('');
+ }).toThrowMinErr('ngModel', 'nonassign', 'Expression \'accessor(user, \'name\')\' is non-assignable.');
+ });
+
+
+ it('should not fail on non-assignable model binding if getterSetter is true', function() {
+ var inputElm = helper.compileInput(
+ '');
+ });
+
+
+ it('should invoke a model in the correct context if getterSetter is true', function() {
+ var inputElm = helper.compileInput(
+ '');
+
+ $rootScope.someService = {
+ value: 'a',
+ getterSetter: function(newValue) {
+ this.value = newValue || this.value;
+ return this.value;
+ }
+ };
+ spyOn($rootScope.someService, 'getterSetter').and.callThrough();
+ $rootScope.$apply();
+
+ expect(inputElm.val()).toBe('a');
+ expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
+ expect($rootScope.someService.value).toBe('a');
+
+ helper.changeInputValueTo('b');
+ expect($rootScope.someService.getterSetter).toHaveBeenCalledWith('b');
+ expect($rootScope.someService.value).toBe('b');
+
+ $rootScope.someService.value = 'c';
+ $rootScope.$apply();
+ expect(inputElm.val()).toBe('c');
+ expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
+ });
+ });
+
+
+ describe('allowInvalid', function() {
+ it('should assign invalid values to the scope if allowInvalid is true', function() {
+ var inputElm = helper.compileInput('');
+ helper.changeInputValueTo('12345');
+
+ expect($rootScope.value).toBe('12345');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should not assign not parsable values to the scope if allowInvalid is true', function() {
+ var inputElm = helper.compileInput('', {
+ valid: false,
+ badInput: true
+ });
+ helper.changeInputValueTo('abcd');
+
+ expect($rootScope.value).toBeUndefined();
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should update the scope before async validators execute if allowInvalid is true', function() {
+ var inputElm = helper.compileInput('');
+ var defer;
+ $rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
+ defer = $q.defer();
+ return defer.promise;
+ };
+ helper.changeInputValueTo('12345');
+
+ expect($rootScope.value).toBe('12345');
+ expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
+ defer.reject();
+ $rootScope.$digest();
+ expect($rootScope.value).toBe('12345');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should update the view before async validators execute if allowInvalid is true', function() {
+ var inputElm = helper.compileInput('');
+ var defer;
+ $rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
+ defer = $q.defer();
+ return defer.promise;
+ };
+ $rootScope.$apply('value = \'12345\'');
+
+ expect(inputElm.val()).toBe('12345');
+ expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
+ defer.reject();
+ $rootScope.$digest();
+ expect(inputElm.val()).toBe('12345');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should not call ng-change listeners twice if the model did not change with allowInvalid', function() {
+ var inputElm = helper.compileInput('');
+ $rootScope.changed = jasmine.createSpy('changed');
+ $rootScope.form.input.$parsers.push(function(value) {
+ return 'modelValue';
+ });
+
+ helper.changeInputValueTo('input1');
+ expect($rootScope.value).toBe('modelValue');
+ expect($rootScope.changed).toHaveBeenCalledOnce();
+
+ helper.changeInputValueTo('input2');
+ expect($rootScope.value).toBe('modelValue');
+ expect($rootScope.changed).toHaveBeenCalledOnce();
+ });
+ });
+ });
+});
diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js
index 9486b4d71f60..0e7d9f053952 100644
--- a/test/ng/directive/ngModelSpec.js
+++ b/test/ng/directive/ngModelSpec.js
@@ -1783,711 +1783,3 @@ describe('ngModel', function() {
}));
});
});
-
-
-describe('$modelOptions', function() {
-
- it('should use the values in ngModelOptionsProvider.defaultOptions if not overridden', function() {
- inject(function($modelOptions) {
- expect($modelOptions.getOption('updateOn')).toEqual('');
- expect($modelOptions.getOption('updateOnDefault')).toEqual(true);
- expect($modelOptions.getOption('debounce')).toBe(0);
- });
- });
-
- it('should allow the defaults to be updated by replacing the object in ngModelOptionsProvider.defaultOptions', function() {
- module(function($modelOptionsProvider) {
- $modelOptionsProvider.defaultOptions = {
- updateOn: 'blur'
- };
- });
- inject(function($modelOptions) {
- expect($modelOptions.getOption('updateOn')).toEqual('blur');
- expect($modelOptions.getOption('updateOnDefault')).toEqual(false);
- });
- });
-
- it('should update on default in case default options do not include updateOn', function() {
- module(function($modelOptionsProvider) {
- delete $modelOptionsProvider.defaultOptions.updateOn;
- });
- inject(function($modelOptions) {
- expect($modelOptions.getOption('updateOnDefault')).toEqual(true);
- });
- });
-});
-
-describe('ngModelOptions attributes', function() {
-
- var helper = {}, $rootScope, $compile, $timeout, $q;
-
- generateInputCompilerHelper(helper);
-
- beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_, _$q_) {
- $compile = _$compile_;
- $rootScope = _$rootScope_;
- $timeout = _$timeout_;
- $q = _$q_;
- }));
-
-
- it('should inherit options from ngModelOptions directives declared on ancestor elements', function() {
- var container = $compile('
' +
- '' +
- '
')($rootScope);
-
- var form = container.find('form');
- var input = container.find('input');
-
- var containerOptions = container.controller('ngModelOptions').$options;
- var formOptions = form.controller('ngModelOptions').$options;
- var inputOptions = input.controller('ngModelOptions').$options;
-
- expect(inputOptions.getOption('allowInvalid')).toEqual(true);
- expect(formOptions.getOption('allowInvalid')).toEqual(true);
- expect(containerOptions.getOption('allowInvalid')).toEqual(true);
-
- expect(inputOptions.getOption('updateOn')).toEqual('');
- expect(inputOptions.getOption('updateOnDefault')).toEqual(true);
- expect(formOptions.getOption('updateOn')).toEqual('blur');
- expect(formOptions.getOption('updateOnDefault')).toEqual(false);
- expect(containerOptions.getOption('updateOn')).toEqual('');
- expect(containerOptions.getOption('updateOnDefault')).toEqual(true);
-
- dealoc(container);
- });
-
-
- it('should inherit options from $modelOptions.defaultOptions if there is no ngModelOptions directive', inject(function($modelOptions) {
- var inputElm = helper.compileInput(
- '');
-
- var inputOptions = $rootScope.form.alias.$options;
- expect(inputOptions.getOption('updateOn')).toEqual($modelOptions.getOption('updateOn'));
- expect(inputOptions.getOption('updateOnDefault')).toEqual($modelOptions.getOption('updateOnDefault'));
- expect(inputOptions.getOption('debounce')).toEqual($modelOptions.getOption('debounce'));
- }));
-
-
- it('should inherit options from $modelOptions.defaultOptions that are not specified on the input element', inject(function($modelOptions) {
- var inputElm = helper.compileInput(
- '');
-
- 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);
- }));
-
-
- it('should inherit options from $modelOptions.defaultOptions that are not specified in an ngModelOptions directive', inject(function($modelOptions) {
- 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(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.name).toBeUndefined();
- browserTrigger(inputElm, 'blur');
- expect($rootScope.name).toEqual('a');
- });
-
-
- it('should not dirty the input if nothing was changed before updateOn trigger', function() {
- var inputElm = helper.compileInput(
- '');
-
- browserTrigger(inputElm, 'blur');
- expect($rootScope.form.alias.$pristine).toBeTruthy();
- });
-
-
- it('should allow overriding the model update trigger event on text areas', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.name).toBeUndefined();
- browserTrigger(inputElm, 'blur');
- expect($rootScope.name).toEqual('a');
- });
-
-
- it('should bind the element to a list of events', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.name).toBeUndefined();
- browserTrigger(inputElm, 'blur');
- expect($rootScope.name).toEqual('a');
-
- helper.changeInputValueTo('b');
- expect($rootScope.name).toEqual('a');
- browserTrigger(inputElm, 'mousemove');
- expect($rootScope.name).toEqual('b');
- });
-
-
- it('should allow keeping the default update behavior on text inputs', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.name).toEqual('a');
- });
-
-
- it('should allow sharing options between multiple inputs', function() {
- $rootScope.options = {updateOn: 'default'};
- var inputElm = helper.compileInput(
- '' +
- '');
-
- helper.changeGivenInputTo(inputElm.eq(0), 'a');
- helper.changeGivenInputTo(inputElm.eq(1), 'b');
- expect($rootScope.name1).toEqual('a');
- expect($rootScope.name2).toEqual('b');
- });
-
-
- it('should hold a copy of the options object', function() {
- $rootScope.options = {updateOn: 'default'};
- var inputElm = helper.compileInput(
- '');
- expect($rootScope.options).toEqual({updateOn: 'default'});
- expect($rootScope.form.alias.$options).not.toBe($rootScope.options);
- });
-
-
- it('should allow overriding the model update trigger event on checkboxes', function() {
- var inputElm = helper.compileInput(
- '');
-
- browserTrigger(inputElm, 'click');
- expect($rootScope.checkbox).toBeUndefined();
-
- browserTrigger(inputElm, 'blur');
- expect($rootScope.checkbox).toBe(true);
-
- browserTrigger(inputElm, 'click');
- expect($rootScope.checkbox).toBe(true);
- });
-
-
- it('should allow keeping the default update behavior on checkboxes', function() {
- var inputElm = helper.compileInput(
- '');
-
- browserTrigger(inputElm, 'click');
- expect($rootScope.checkbox).toBe(true);
-
- browserTrigger(inputElm, 'click');
- expect($rootScope.checkbox).toBe(false);
- });
-
-
- it('should allow overriding the model update trigger event on radio buttons', function() {
- var inputElm = helper.compileInput(
- '' +
- '' +
- '');
-
- $rootScope.$apply('color = \'white\'');
- browserTrigger(inputElm[2], 'click');
- expect($rootScope.color).toBe('white');
-
- browserTrigger(inputElm[2], 'blur');
- expect($rootScope.color).toBe('blue');
-
- });
-
-
- it('should allow keeping the default update behavior on radio buttons', function() {
- var inputElm = helper.compileInput(
- '' +
- '' +
- '');
-
- $rootScope.$apply('color = \'white\'');
- browserTrigger(inputElm[2], 'click');
- expect($rootScope.color).toBe('blue');
- });
-
-
- it('should trigger only after timeout in text inputs', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- helper.changeInputValueTo('b');
- helper.changeInputValueTo('c');
- expect($rootScope.name).toEqual(undefined);
- $timeout.flush(2000);
- expect($rootScope.name).toEqual(undefined);
- $timeout.flush(9000);
- expect($rootScope.name).toEqual('c');
- });
-
-
- it('should trigger only after timeout in checkboxes', function() {
- var inputElm = helper.compileInput(
- '');
-
- browserTrigger(inputElm, 'click');
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(2000);
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(9000);
- expect($rootScope.checkbox).toBe(true);
- });
-
-
- it('should trigger only after timeout in radio buttons', function() {
- var inputElm = helper.compileInput(
- '' +
- '' +
- '');
-
- browserTrigger(inputElm[0], 'click');
- expect($rootScope.color).toBe('white');
- browserTrigger(inputElm[1], 'click');
- expect($rootScope.color).toBe('white');
- $timeout.flush(12000);
- expect($rootScope.color).toBe('white');
- $timeout.flush(10000);
- expect($rootScope.color).toBe('red');
-
- });
-
-
- it('should not trigger digest while debouncing', function() {
- var inputElm = helper.compileInput(
- '');
-
- var watchSpy = jasmine.createSpy('watchSpy');
- $rootScope.$watch(watchSpy);
-
- helper.changeInputValueTo('a');
- expect(watchSpy).not.toHaveBeenCalled();
-
- $timeout.flush(10000);
- expect(watchSpy).toHaveBeenCalled();
- });
-
-
- it('should allow selecting different debounce timeouts for each event',
- function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(6000);
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(4000);
- expect($rootScope.name).toEqual('a');
- helper.changeInputValueTo('b');
- browserTrigger(inputElm, 'blur');
- $timeout.flush(4000);
- expect($rootScope.name).toEqual('a');
- $timeout.flush(2000);
- expect($rootScope.name).toEqual('b');
- });
-
-
- it('should allow selecting different debounce timeouts for each event on checkboxes', function() {
- var inputElm = helper.compileInput('');
-
- inputElm[0].checked = false;
- browserTrigger(inputElm, 'click');
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(8000);
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(3000);
- expect($rootScope.checkbox).toBe(true);
- inputElm[0].checked = true;
- browserTrigger(inputElm, 'click');
- browserTrigger(inputElm, 'blur');
- $timeout.flush(3000);
- expect($rootScope.checkbox).toBe(true);
- $timeout.flush(3000);
- expect($rootScope.checkbox).toBe(false);
- });
-
-
- it('should allow selecting 0 for non-default debounce timeouts for each event on checkboxes', function() {
- var inputElm = helper.compileInput('');
-
- inputElm[0].checked = false;
- browserTrigger(inputElm, 'click');
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(8000);
- expect($rootScope.checkbox).toBeUndefined();
- $timeout.flush(3000);
- expect($rootScope.checkbox).toBe(true);
- inputElm[0].checked = true;
- browserTrigger(inputElm, 'click');
- browserTrigger(inputElm, 'blur');
- $timeout.flush(0);
- expect($rootScope.checkbox).toBe(false);
- });
-
-
- it('should inherit model update settings from ancestor elements', function() {
- var doc = $compile(
- '')($rootScope);
- $rootScope.$digest();
-
- var inputElm = doc.find('input').eq(0);
- helper.changeGivenInputTo(inputElm, 'a');
- expect($rootScope.name).toEqual(undefined);
- browserTrigger(inputElm, 'blur');
- expect($rootScope.name).toBeUndefined();
- $timeout.flush(2000);
- expect($rootScope.name).toBeUndefined();
- $timeout.flush(9000);
- expect($rootScope.name).toEqual('a');
- dealoc(doc);
- });
-
-
- it('should flush debounced events when calling $commitViewValue directly', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.name).toEqual(undefined);
- $rootScope.form.alias.$commitViewValue();
- expect($rootScope.name).toEqual('a');
- });
-
-
- it('should cancel debounced events when calling $commitViewValue', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- $rootScope.form.alias.$commitViewValue();
- expect($rootScope.name).toEqual('a');
-
- $rootScope.form.alias.$setPristine();
- $timeout.flush(1000);
- expect($rootScope.form.alias.$pristine).toBeTruthy();
- });
-
-
- it('should reset input val if rollbackViewValue called during pending update', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect(inputElm.val()).toBe('a');
- $rootScope.form.alias.$rollbackViewValue();
- expect(inputElm.val()).toBe('');
- browserTrigger(inputElm, 'blur');
- expect(inputElm.val()).toBe('');
- });
-
-
- it('should allow canceling pending updates', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.name).toEqual(undefined);
- $rootScope.form.alias.$rollbackViewValue();
- expect($rootScope.name).toEqual(undefined);
- browserTrigger(inputElm, 'blur');
- expect($rootScope.name).toEqual(undefined);
- });
-
-
- it('should allow canceling debounced updates', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect($rootScope.name).toEqual(undefined);
- $timeout.flush(2000);
- $rootScope.form.alias.$rollbackViewValue();
- expect($rootScope.name).toEqual(undefined);
- $timeout.flush(10000);
- expect($rootScope.name).toEqual(undefined);
- });
-
-
- it('should handle model updates correctly even if rollbackViewValue is not invoked', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- $rootScope.$apply('name = \'b\'');
- browserTrigger(inputElm, 'blur');
- expect($rootScope.name).toBe('b');
- });
-
-
- it('should reset input val if rollbackViewValue called during debounce', function() {
- var inputElm = helper.compileInput(
- '');
-
- helper.changeInputValueTo('a');
- expect(inputElm.val()).toBe('a');
- $rootScope.form.alias.$rollbackViewValue();
- expect(inputElm.val()).toBe('');
- $timeout.flush(3000);
- expect(inputElm.val()).toBe('');
- });
-
-
- it('should not try to invoke a model if getterSetter is false', function() {
- var inputElm = helper.compileInput(
- '');
-
- var spy = $rootScope.name = jasmine.createSpy('setterSpy');
- helper.changeInputValueTo('a');
- expect(spy).not.toHaveBeenCalled();
- expect(inputElm.val()).toBe('a');
- });
-
-
- it('should not try to invoke a model if getterSetter is not set', function() {
- var inputElm = helper.compileInput('');
-
- var spy = $rootScope.name = jasmine.createSpy('setterSpy');
- helper.changeInputValueTo('a');
- expect(spy).not.toHaveBeenCalled();
- expect(inputElm.val()).toBe('a');
- });
-
-
- it('should try to invoke a function model if getterSetter is true', function() {
- var inputElm = helper.compileInput(
- '');
-
- var spy = $rootScope.name = jasmine.createSpy('setterSpy').and.callFake(function() {
- return 'b';
- });
- $rootScope.$apply();
- expect(inputElm.val()).toBe('b');
-
- helper.changeInputValueTo('a');
- expect(inputElm.val()).toBe('b');
- expect(spy).toHaveBeenCalledWith('a');
- expect($rootScope.name).toBe(spy);
- });
-
-
- it('should assign to non-function models if getterSetter is true', function() {
- var inputElm = helper.compileInput(
- '');
-
- $rootScope.name = 'c';
- helper.changeInputValueTo('d');
- expect(inputElm.val()).toBe('d');
- expect($rootScope.name).toBe('d');
- });
-
-
- it('should fail on non-assignable model binding if getterSetter is false', function() {
- expect(function() {
- var inputElm = helper.compileInput('');
- }).toThrowMinErr('ngModel', 'nonassign', 'Expression \'accessor(user, \'name\')\' is non-assignable.');
- });
-
-
- it('should not fail on non-assignable model binding if getterSetter is true', function() {
- var inputElm = helper.compileInput(
- '');
- });
-
-
- it('should invoke a model in the correct context if getterSetter is true', function() {
- var inputElm = helper.compileInput(
- '');
-
- $rootScope.someService = {
- value: 'a',
- getterSetter: function(newValue) {
- this.value = newValue || this.value;
- return this.value;
- }
- };
- spyOn($rootScope.someService, 'getterSetter').and.callThrough();
- $rootScope.$apply();
-
- expect(inputElm.val()).toBe('a');
- expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
- expect($rootScope.someService.value).toBe('a');
-
- helper.changeInputValueTo('b');
- expect($rootScope.someService.getterSetter).toHaveBeenCalledWith('b');
- expect($rootScope.someService.value).toBe('b');
-
- $rootScope.someService.value = 'c';
- $rootScope.$apply();
- expect(inputElm.val()).toBe('c');
- expect($rootScope.someService.getterSetter).toHaveBeenCalledWith();
- });
-
-
- it('should assign invalid values to the scope if allowInvalid is true', function() {
- var inputElm = helper.compileInput('');
- helper.changeInputValueTo('12345');
-
- expect($rootScope.value).toBe('12345');
- expect(inputElm).toBeInvalid();
- });
-
-
- it('should not assign not parsable values to the scope if allowInvalid is true', function() {
- var inputElm = helper.compileInput('', {
- valid: false,
- badInput: true
- });
- helper.changeInputValueTo('abcd');
-
- expect($rootScope.value).toBeUndefined();
- expect(inputElm).toBeInvalid();
- });
-
-
- it('should update the scope before async validators execute if allowInvalid is true', function() {
- var inputElm = helper.compileInput('');
- var defer;
- $rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
- defer = $q.defer();
- return defer.promise;
- };
- helper.changeInputValueTo('12345');
-
- expect($rootScope.value).toBe('12345');
- expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
- defer.reject();
- $rootScope.$digest();
- expect($rootScope.value).toBe('12345');
- expect(inputElm).toBeInvalid();
- });
-
-
- it('should update the view before async validators execute if allowInvalid is true', function() {
- var inputElm = helper.compileInput('');
- var defer;
- $rootScope.form.input.$asyncValidators.promiseValidator = function(value) {
- defer = $q.defer();
- return defer.promise;
- };
- $rootScope.$apply('value = \'12345\'');
-
- expect(inputElm.val()).toBe('12345');
- expect($rootScope.form.input.$pending.promiseValidator).toBe(true);
- defer.reject();
- $rootScope.$digest();
- expect(inputElm.val()).toBe('12345');
- expect(inputElm).toBeInvalid();
- });
-
-
- it('should not call ng-change listeners twice if the model did not change with allowInvalid', function() {
- var inputElm = helper.compileInput('');
- $rootScope.changed = jasmine.createSpy('changed');
- $rootScope.form.input.$parsers.push(function(value) {
- return 'modelValue';
- });
-
- helper.changeInputValueTo('input1');
- expect($rootScope.value).toBe('modelValue');
- expect($rootScope.changed).toHaveBeenCalledOnce();
-
- helper.changeInputValueTo('input2');
- expect($rootScope.value).toBe('modelValue');
- expect($rootScope.changed).toHaveBeenCalledOnce();
- });
-});