fix(ngOptions): select unknown option if unmatched model does not match empty option
When a regular / ngOptions select has an explicit *empty* option, this option can be selected by the user and will set the model to `null`. It is also selected when the model is set to `null` or `undefined`. When the model is set to a value that does not match any option value, and is also not `null` or `undefined`, the *unknown* option is inserted and selected - this is an explicit marker that the select is in an invalid / unknown state, which is different from an allowed empty state. Previously, regular selects followed this logic, whereas ngOptions selects selected the empty option in the case described above. This patch makes the behavior consistent between regular / ngOptions select - the latter will now insert and select the unknown option. The order of the options has been fixed to unknown -> empty -> actual options.
This commit is contained in:
committed by
Martin Staffa
parent
8d7c7f4a8e
commit
5878f07474
@@ -473,7 +473,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
|
||||
option.element.setAttribute('selected', 'selected');
|
||||
} else {
|
||||
|
||||
if (providedEmptyOption) {
|
||||
if (value == null && providedEmptyOption) {
|
||||
selectCtrl.removeUnknownOption();
|
||||
selectCtrl.selectEmptyOption();
|
||||
} else if (selectCtrl.unknownOption.parent().length) {
|
||||
selectCtrl.updateUnknownOption(value);
|
||||
@@ -657,7 +658,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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -400,6 +400,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
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -773,12 +773,32 @@ describe('ngOptions', function() {
|
||||
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 = scope.values[1];
|
||||
scope.$digest();
|
||||
options = element.find('option');
|
||||
|
||||
expect(options[0]).not.toBeMarkedAsSelected();
|
||||
expect(options[1]).not.toBeMarkedAsSelected();
|
||||
expect(options[2]).toBeMarkedAsSelected();
|
||||
});
|
||||
|
||||
describe('disableWhen expression', function() {
|
||||
@@ -2195,6 +2215,20 @@ describe('ngOptions', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should insert and select temporary unknown option when no options-model match, empty ' +
|
||||
'option is present and model is defined', function() {
|
||||
scope.selected = 'C';
|
||||
scope.values = [{name: 'A'}, {name: 'B'}];
|
||||
createSingleSelect(true);
|
||||
|
||||
expect(element).toEqualSelect(['?'], '', 'object:3', 'object:4');
|
||||
|
||||
scope.$apply('selected = values[1]');
|
||||
|
||||
expect(element).toEqualSelect('', 'object:3', ['object:4']);
|
||||
});
|
||||
|
||||
|
||||
it('should select correct input if previously selected option was "?"', function() {
|
||||
createSingleSelect();
|
||||
|
||||
@@ -2214,6 +2248,19 @@ describe('ngOptions', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should remove unknown option when empty option exists and model is undefined', function() {
|
||||
scope.selected = 'C';
|
||||
scope.values = [{name: 'A'}, {name: 'B'}];
|
||||
createSingleSelect(true);
|
||||
|
||||
expect(element).toEqualSelect(['?'], '', 'object:3', 'object:4');
|
||||
|
||||
scope.selected = undefined;
|
||||
scope.$digest();
|
||||
|
||||
expect(element).toEqualSelect([''], 'object:3', 'object:4');
|
||||
});
|
||||
|
||||
it('should use exact same values as values in scope with one-time bindings', function() {
|
||||
scope.values = [{name: 'A'}, {name: 'B'}];
|
||||
scope.selected = scope.values[0];
|
||||
@@ -2955,7 +3002,7 @@ describe('ngOptions', function() {
|
||||
expect(ngModelCtrl.$error.required).toBeFalsy();
|
||||
|
||||
// model -> view
|
||||
scope.$apply('selection = "unmatched value"');
|
||||
scope.$apply('selection = null');
|
||||
expect(options[0]).toBeMarkedAsSelected();
|
||||
expect(element).toBeInvalid();
|
||||
expect(ngModelCtrl.$error.required).toBeTruthy();
|
||||
|
||||
@@ -86,28 +86,6 @@ describe('select', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
jasmine.addMatchers({
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
toEqualSelectWithOptions: function() {
|
||||
return {
|
||||
compare: function(actual, expected) {
|
||||
@@ -396,6 +374,7 @@ describe('select', function() {
|
||||
|
||||
it('should remove the "selected" attribute from the previous option when the model changes', function() {
|
||||
compile('<select name="select" ng-model="selected">' +
|
||||
'<option value="">--empty--</option>' +
|
||||
'<option value="a">A</option>' +
|
||||
'<option value="b">B</option>' +
|
||||
'</select>');
|
||||
@@ -411,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();
|
||||
|
||||
options = element.find('option');
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user