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. - * - * - * - *
- *
- * Name: - * - *
- *
- *
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. + * + * + * + *
+ *
+ * Name: + * + *
+ *
+ *
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( + '