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

fix(ngOptions): remove selected attribute from unselected options #14894

Merged
merged 1 commit into from
Jul 23, 2016
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
9 changes: 8 additions & 1 deletion src/ng/directive/ngOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,11 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var removeEmptyOption = function() {
if (!providedEmptyOption) {
emptyOption.remove();
} else {
emptyOption.removeAttr('selected');
}
};


var renderUnknownOption = function() {
selectElement.prepend(unknownOption);
selectElement.val('?');
Expand All @@ -467,8 +468,13 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
if (!multiple) {

selectCtrl.writeValue = function writeNgOptionsValue(value) {
var selectedOption = options.selectValueMap[selectElement.val()];
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 (option) {
// Don't update the option when it is already selected.
// For example, the browser will select the first option by default. In that case,
Expand Down Expand Up @@ -509,6 +515,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,

// If we are using `track by` then we must watch the tracked value on the model
// since ngModel only watches for object identity change
// FIXME: When a user selects an option, this watch will fire needlessly
if (ngOptions.trackBy) {
scope.$watch(
function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
Expand Down
96 changes: 96 additions & 0 deletions test/ng/directive/ngOptionsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,47 @@ describe('ngOptions', function() {
return { pass: errors.length === 0, message: message };
}
};
},
toBeMarkedAsSelected: function() {
// Selected is special because the element property and attribute reflect each other's state.
// IE9 will wrongly report hasAttribute('selected') === true when the property is
// undefined or null, and the dev tools show that no attribute is set
return {
compare: function(actual) {
var errors = [];
if (actual.selected === null || typeof actual.selected === 'undefined' || actual.selected === false) {
errors.push('Expected option property "selected" to be truthy');
}

if (msie !== 9 && actual.hasAttribute('selected') === false) {
errors.push('Expected option to have attribute "selected"');
}

var result = {
pass: errors.length === 0,
message: errors.join('\n')
};

return result;
},
negativeCompare: function(actual) {
var errors = [];
if (actual.selected) {
errors.push('Expected option property "selected" to be falsy');
}

if (msie !== 9 && actual.hasAttribute('selected')) {
errors.push('Expected option not to have attribute "selected"');
}

var result = {
pass: errors.length === 0,
message: errors.join('\n')
};

return result;
}
};
}
});
});
Expand Down Expand Up @@ -744,6 +785,41 @@ describe('ngOptions', function() {
});


it('should remove the "selected" attribute from the previous option when the model changes', function() {
scope.values = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];

createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in values'
Copy link

Choose a reason for hiding this comment

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

I think is missing the "track by id", sorry if I wrong

Copy link
Contributor Author

@Narretz Narretz Jul 11, 2016 via email

Choose a reason for hiding this comment

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

}, true);

var options = element.find('option');
expect(options[0]).toBeMarkedAsSelected();
expect(options[1]).not.toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();

scope.selected = scope.values[0];
scope.$digest();

expect(options[0]).not.toBeMarkedAsSelected();
expect(options[1]).toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();

scope.selected = scope.values[1];
scope.$digest();

expect(options[0]).not.toBeMarkedAsSelected();
expect(options[1]).not.toBeMarkedAsSelected();
expect(options[2]).toBeMarkedAsSelected();

scope.selected = 'no match';
scope.$digest();

expect(options[0]).toBeMarkedAsSelected();
expect(options[1]).not.toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();
});

describe('disableWhen expression', function() {

describe('on single select', function() {
Expand Down Expand Up @@ -1395,6 +1471,26 @@ describe('ngOptions', function() {
});
}).not.toThrow();
});

it('should remove the "selected" attribute when the model changes', function() {
Copy link
Member

Choose a reason for hiding this comment

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

when the model changes --> when the view changes (?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically, the problem appears when you update the model (which then updates the view)

createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in arr track by item.id'
});

var options = element.find('option');
browserTrigger(options[2], 'click');

expect(scope.selected).toEqual(scope.arr[1]);

scope.selected = {};
scope.$digest();

expect(options[0]).toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();
});

});


Expand Down