From 50af955d250186eedc1ee5cbc6b7e837d36b55b9 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Sun, 11 Dec 2016 22:10:45 +0200 Subject: [PATCH 1/3] refactor(testabilityPatch): remove code duplication --- test/helpers/testabilityPatch.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/helpers/testabilityPatch.js b/test/helpers/testabilityPatch.js index d589ac2c67df..91fd5661d516 100644 --- a/test/helpers/testabilityPatch.js +++ b/test/helpers/testabilityPatch.js @@ -391,8 +391,7 @@ function generateInputCompilerHelper(helper) { }; helper.changeInputValueTo = function(value) { - helper.inputElm.val(value); - browserTrigger(helper.inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); + helper.changeGivenInputTo(helper.inputElm, value); }; helper.changeGivenInputTo = function(inputElm, value) { From 4ac35eb53cecb8865f2f262c27c76788a019dab2 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Sun, 11 Dec 2016 22:11:16 +0200 Subject: [PATCH 2/3] fix(ngModelOptions): work correctly when on the template of `replace` directives Previously, in order for `ngModel` and `ngModelOptions` to work correctly together, the latter's pre-linking function should be run before the former's pre-linking function. This was typically what happened, except when `ngModel` was used on an element which also had a `replace` directive, whose template included `ngModelOptions`. In that case, the order was reversed. This commit fixes it by moving the initialization logic of `ngModelOptions` from its pre-linking function to its controller's `$onInit()` lifecycle hook. Fixes #15492 --- src/ng/directive/ngModelOptions.js | 27 +- test/ng/directive/ngModelOptionsSpec.js | 1345 ++++++++++++----------- 2 files changed, 705 insertions(+), 667 deletions(-) diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index 91b138a241ee..4f25e3a06129 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -331,19 +331,28 @@ defaultModelOptions = new ModelOptions({ * */ var ngModelOptionsDirective = function() { + NgModelOptionsController.$inject = ['$attrs', '$scope']; + function NgModelOptionsController($attrs, $scope) { + this.$$attrs = $attrs; + this.$$scope = $scope; + } + NgModelOptionsController.prototype = { + $onInit: function() { + var parentCtrl = this.parentOptionsCtrl; + var parentOptions = parentCtrl ? parentCtrl.$options : defaultModelOptions; + var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions); + + this.$options = parentOptions.createChild(modelOptionsDefinition); + } + }; + 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)); - } - } + require: {parentOptionsCtrl: '?^^ngModelOptions'}, + bindToController: true, + controller: NgModelOptionsController }; }; diff --git a/test/ng/directive/ngModelOptionsSpec.js b/test/ng/directive/ngModelOptionsSpec.js index ad5d0f7e7cfb..62ff8779f3b2 100644 --- a/test/ng/directive/ngModelOptionsSpec.js +++ b/test/ng/directive/ngModelOptionsSpec.js @@ -19,817 +19,846 @@ describe('ngModelOptions', function() { 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')); - }); + describe('basic usage', function() { + var helper = {}, $rootScope, $compile, $timeout, $q; - it('if `ngModelOptions` on the same element does not specify the option', function() { - var inputElm = helper.compileInput( - ''); + generateInputCompilerHelper(helper); - 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')); - }); + beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_, _$q_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $timeout = _$timeout_; + $q = _$q_; + })); - it('if the first `ngModelOptions` ancestor does not specify the option', function() { - var form = $compile('
' + - '' + - '
')($rootScope); - var inputOptions = $rootScope.form.alias.$options; + describe('should fall back to `defaultModelOptions`', function() { + it('if there is no `ngModelOptions` directive', function() { + var inputElm = helper.compileInput( + ''); - 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); - }); - }); + 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')); + }); - describe('sharing and inheritance', function() { + it('if `ngModelOptions` on the same element does not specify the option', function() { + var inputElm = helper.compileInput( + ''); - it('should not inherit options from ancestor `ngModelOptions` directives by default', function() { - var container = $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')); + }); - 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; + it('if the first `ngModelOptions` ancestor does not specify the option', function() { + var form = $compile('
' + + '' + + '
')($rootScope); + var inputOptions = $rootScope.form.alias.$options; - expect(containerOptions.getOption('allowInvalid')).toEqual(true); - expect(formOptions.getOption('allowInvalid')).toEqual(false); - expect(inputOptions.getOption('allowInvalid')).toEqual(false); + 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); + }); + }); - 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); - }); + describe('sharing and inheritance', function() { - 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 not inherit options from ancestor `ngModelOptions` directives by default', function() { + var container = $compile( + '
' + + '
' + + '' + + '
' + + '
')($rootScope); - 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); - }); + var form = container.find('form'); + var input = container.find('input'); - it('should correctly inherit default and another specified event for `updateOn`', function() { - var container = $compile( - '
' + - '' + - '
')($rootScope); + var containerOptions = container.controller('ngModelOptions').$options; + var formOptions = form.controller('ngModelOptions').$options; + var inputOptions = input.controller('ngModelOptions').$options; - var input = container.find('input'); - 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(inputOptions.getOption('updateOn')).toEqual('blur'); - expect(inputOptions.getOption('updateOnDefault')).toEqual(true); + 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); - }); + 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 `updateOnDefault` as well if we have `updateOn: "$inherit"`', function() { - var container = $compile( - '
' + - '' + + 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); + '
' + + '' + + '
' + + '')($rootScope); - var input1 = container.find('input').eq(0); - var inputOptions1 = input1.controller('ngModelOptions').$options; + 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); + 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; + 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); + expect(inputOptions2.getOption('updateOn')).toEqual('blur'); + expect(inputOptions2.getOption('updateOnDefault')).toEqual(true); - dealoc(container); - }); + 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 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 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'); + }); }); - 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'); + }); - 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 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( + '