Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Fixes to select and ngOptions - "required" validation and behavior with empty / unknown option #15922

Merged
merged 4 commits into from
Apr 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions src/ng/directive/ngOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to skip jqLite/jQuery here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't. I just didn't think to use jQuery here. How do you get the selected option with jquery?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I was just comparing it to the previous code which used options.selectValueMap. Could you explain the change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. Regular select was using the same technique - getting the option directly from the DOM, so I made it consistent. I don't think there's a huge difference, it just felt right to look at the actual DOM

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.
Expand All @@ -464,22 +464,14 @@ 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;
}

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);
}
};

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -704,7 +701,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
ngModelCtrl.$render();
}
}

}
}

Expand Down
33 changes: 19 additions & 14 deletions src/ng/directive/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ var SelectController =
// to create it in <select> and IE barfs otherwise.
self.unknownOption = jqLite(window.document.createElement('option'));

// The empty option is an option with the value '' that te application developer can
// provide inside the select. When the model changes to a value that doesn't match an option,
// it is selected - so if an empty option is provided, no unknown option is generated.
// However, the empty option is not removed when the model matches an option. It is always selectable
// and indicates that a "null" selection has been made.
// The empty option is an option with the value '' that the application developer can
// provide inside the select. It is always selectable and indicates that a "null" selection has
// been made by the user.
// If the select has an empty option, and the model of the select is set to "undefined" or "null",
// the empty option is selected.
// If the model is set to a different unmatched value, the unknown option is rendered and
// selected, i.e both are present, because a "null" selection and an unknown value are different.
self.hasEmptyOption = false;
self.emptyOption = undefined;

Expand Down Expand Up @@ -84,7 +86,7 @@ var SelectController =

self.unselectEmptyOption = function() {
if (self.hasEmptyOption) {
self.emptyOption.removeAttr('selected');
setOptionSelectedStatus(self.emptyOption, false);
}
};

Expand Down Expand Up @@ -126,14 +128,7 @@ var SelectController =
var selectedOption = $element[0].options[$element[0].selectedIndex];
setOptionSelectedStatus(jqLite(selectedOption), true);
} else {
if (value == null && self.emptyOption) {
self.removeUnknownOption();
self.selectEmptyOption();
} else if (self.unknownOption.parent().length) {
self.updateUnknownOption(value);
} else {
self.renderUnknownOption(value);
}
self.selectUnknownOrEmptyOption(value);
}
};

Expand Down Expand Up @@ -176,6 +171,16 @@ var SelectController =
return !!optionsMap.get(value);
};

self.selectUnknownOrEmptyOption = function(value) {
if (value == null && self.emptyOption) {
self.removeUnknownOption();
self.selectEmptyOption();
} else if (self.unknownOption.parent().length) {
self.updateUnknownOption(value);
} else {
self.renderUnknownOption(value);
}
};

var renderScheduled = false;
function scheduleRender() {
Expand Down
33 changes: 29 additions & 4 deletions test/helpers/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,13 +365,15 @@ beforeEach(function() {
return {
compare: function(actual) {
var errors = [];
var optionVal = toJson(actual.value);

if (actual.selected === null || typeof actual.selected === 'undefined' || actual.selected === false) {
errors.push('Expected option property "selected" to be truthy');
errors.push('Expected option with value ' + optionVal + ' to have property "selected" set to truthy');
}

// Support: IE 9 only
if (msie !== 9 && actual.hasAttribute('selected') === false) {
errors.push('Expected option to have attribute "selected"');
errors.push('Expected option with value ' + optionVal + ' to have attribute "selected"');
}

var result = {
Expand All @@ -383,13 +385,15 @@ beforeEach(function() {
},
negativeCompare: function(actual) {
var errors = [];
var optionVal = toJson(actual.value);

if (actual.selected) {
errors.push('Expected option property "selected" to be falsy');
errors.push('Expected option with value ' + optionVal + ' property "selected" to be falsy');
}

// Support: IE 9 only
if (msie !== 9 && actual.hasAttribute('selected')) {
errors.push('Expected option not to have attribute "selected"');
errors.push('Expected option with value ' + optionVal + ' not to have attribute "selected"');
}

var result = {
Expand All @@ -400,6 +404,27 @@ beforeEach(function() {
return result;
}
};
},
toEqualSelect: function() {
return {
compare: function(actual, expected) {
var actualValues = [],
expectedValues = [].slice.call(arguments, 1);

forEach(actual.find('option'), function(option) {
actualValues.push(option.selected ? [option.value] : option.value);
});

var message = function() {
return 'Expected ' + toJson(actualValues) + ' to equal ' + toJson(expectedValues) + '.';
};

return {
pass: equals(expectedValues, actualValues),
message: message
};
}
};
}
});
});
Expand Down
Loading