diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index e82d5e49813a..eecc6ccaa8c6 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -449,12 +449,12 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, if (!multiple) { selectCtrl.writeValue = function writeNgOptionsValue(value) { - var selectedOption = options.selectValueMap[selectElement.val()]; + var selectedOption = selectElement[0].options[selectElement[0].selectedIndex]; var option = options.getOptionFromViewValue(value); // Make sure to remove the selected attribute from the previously selected option // Otherwise, screen readers might get confused - if (selectedOption) selectedOption.element.removeAttribute('selected'); + if (selectedOption) selectedOption.removeAttribute('selected'); if (option) { // Don't update the option when it is already selected. @@ -464,7 +464,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, if (selectElement[0].value !== option.selectValue) { selectCtrl.removeUnknownOption(); - selectCtrl.unselectEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; @@ -472,14 +471,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, option.element.setAttribute('selected', 'selected'); } else { - - if (providedEmptyOption) { - selectCtrl.selectEmptyOption(); - } else if (selectCtrl.unknownOption.parent().length) { - selectCtrl.updateUnknownOption(value); - } else { - selectCtrl.renderUnknownOption(value); - } + selectCtrl.selectUnknownOrEmptyOption(value); } }; @@ -657,7 +649,12 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // Ensure that the empty option is always there if it was explicitly provided if (providedEmptyOption) { - selectElement.prepend(selectCtrl.emptyOption); + + if (selectCtrl.unknownOption.parent().length) { + selectCtrl.unknownOption.after(selectCtrl.emptyOption); + } else { + selectElement.prepend(selectCtrl.emptyOption); + } } options.items.forEach(function addOption(option) { @@ -704,7 +701,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, ngModelCtrl.$render(); } } - } } diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index dc828764d3da..d8a19605f96d 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -44,11 +44,13 @@ var SelectController = // to create it in ' + - '' + - '' + - ''); + describe('required state', function() { - scope.change = function() { - scope.log += 'change;'; - }; + it('should set the error if the empty option is selected', function() { + compile( + ''); - scope.$apply(function() { - scope.log = ''; - scope.selection = 'c'; + scope.$apply(function() { + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + var options = element.find('option'); + + // view -> model + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + + browserTrigger(options[1], 'click'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + // model -> view + scope.$apply('selection = null'); + options = element.find('option'); + expect(options[0]).toBeMarkedAsSelected(); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); }); - expect(scope.form.select.$error.required).toBeFalsy(); - expect(element).toBeValid(); - expect(element).toBePristine(); - scope.$apply(function() { - scope.selection = ''; + it('should validate with empty option and bound ngRequired', function() { + compile( + ''); + + scope.$apply(function() { + scope.required = false; + }); + + var options = element.find('option'); + + browserTrigger(options[0], 'click'); + expect(element).toBeValid(); + + scope.$apply('required = true'); + expect(element).toBeInvalid(); + + scope.$apply('selection = "a"'); + expect(element).toBeValid(); + expect(element).toEqualSelect('', ['a'], 'b'); + + browserTrigger(options[0], 'click'); + expect(element).toBeInvalid(); + + scope.$apply('required = false'); + expect(element).toBeValid(); }); - expect(scope.form.select.$error.required).toBeTruthy(); - expect(element).toBeInvalid(); - expect(element).toBePristine(); - expect(scope.log).toEqual(''); - element[0].value = 'c'; - browserTrigger(element, 'change'); - expect(element).toBeValid(); - expect(element).toBeDirty(); - expect(scope.log).toEqual('change;'); - }); + it('should not be invalid if no required attribute is present', function() { + compile( + ''); + expect(element).toBeValid(); + expect(element).toBePristine(); + }); - it('should not be invalid if no require', function() { - compile( - ''); - expect(element).toBeValid(); - expect(element).toBePristine(); - }); + it('should NOT set the error if the unknown option is selected', function() { + compile( + ''); + + scope.$apply(function() { + scope.selection = 'a'; + }); + + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + scope.$apply('selection = "c"'); + expect(element).toEqualSelect([unknownValue('c')], 'a', 'b'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + }); + + }); it('should work with repeated value options', function() { scope.robots = ['c3p0', 'r2d2']; @@ -340,6 +374,7 @@ describe('select', function() { it('should remove the "selected" attribute from the previous option when the model changes', function() { compile(''); @@ -355,24 +390,45 @@ describe('select', function() { scope.$digest(); options = element.find('option'); - expect(options.length).toBe(2); - expect(options[0]).toBeMarkedAsSelected(); - expect(options[1]).not.toBeMarkedAsSelected(); + expect(options.length).toBe(3); + expect(options[0]).not.toBeMarkedAsSelected(); + expect(options[1]).toBeMarkedAsSelected(); + expect(options[2]).not.toBeMarkedAsSelected(); scope.selected = 'b'; scope.$digest(); options = element.find('option'); expect(options[0]).not.toBeMarkedAsSelected(); - expect(options[1]).toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).toBeMarkedAsSelected(); - scope.selected = 'no match'; + // This will select the empty option + scope.selected = null; scope.$digest(); + expect(options[0]).toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).not.toBeMarkedAsSelected(); + + // This will add and select the unknown option + scope.selected = 'unmatched value'; + scope.$digest(); options = element.find('option'); + expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); + expect(options[3]).not.toBeMarkedAsSelected(); + + // Back to matched value + scope.selected = 'b'; + scope.$digest(); + options = element.find('option'); + + expect(options[0]).not.toBeMarkedAsSelected(); + expect(options[1]).not.toBeMarkedAsSelected(); + expect(options[2]).toBeMarkedAsSelected(); }); describe('empty option', function() { @@ -2358,6 +2414,35 @@ describe('select', function() { expect(previouslySelectedOptionElement).not.toBe(optionElements[0]); }); + + it('should validate when the options change', function() { + scope.values = ['A', 'B']; + scope.selection = 'A'; + + compile( + '' + ); + + expect(element).toEqualSelect('', ['A'], 'B'); + expect(element).toBeValid(); + expect(ngModelCtrl.$error.required).toBeFalsy(); + + scope.$apply(function() { + // Only when new objects are used, ngRepeat re-creates the element from scratch + scope.values = ['B', 'C']; + }); + + expect(element).toEqualSelect([''], 'B', 'C'); + expect(element).toBeInvalid(); + expect(ngModelCtrl.$error.required).toBeTruthy(); + // ngModel sets undefined for invalid values + expect(scope.selection).toBeUndefined(); + }); + + });