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

Commit 245b271

Browse files
authored
fix(ngOptions): don't add comment nodes as empty options
When the "empty option" element contains a transclusion directive, the result of the compilation always includes a comment node. Since we are adding / removing the "selected" attribute on the empty option, we need to make sure it's an actual element. To solve this, we take advantage of the fact the each option element has an option directive that tries to register the option with the selectController. With ngOptions, this registerOption function is normally noop'd since it's not possible to add dynamic options. Now if the result of the empty option compilation is a comment, we re-define the function so that it catches empty options when they are actually linked / rendered. Closes #15454 Closes #15459
1 parent f5d2bf3 commit 245b271

File tree

3 files changed

+131
-11
lines changed

3 files changed

+131
-11
lines changed

src/ng/directive/ngOptions.js

+30-3
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
420420
// option when the viewValue does not match any of the option values.
421421
for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
422422
if (children[i].value === '') {
423+
selectCtrl.hasEmptyOption = true;
423424
selectCtrl.emptyOption = children.eq(i);
424425
break;
425426
}
@@ -556,9 +557,35 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
556557
// compile the element since there might be bindings in it
557558
$compile(selectCtrl.emptyOption)(scope);
558559

559-
// remove the class, which is added automatically because we recompile the element and it
560-
// becomes the compilation root
561-
selectCtrl.emptyOption.removeClass('ng-scope');
560+
if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
561+
// This means the empty option has currently no actual DOM node, probably because
562+
// it has been modified by a transclusion directive.
563+
selectCtrl.hasEmptyOption = false;
564+
565+
// Redefine the registerOption function, which will catch
566+
// options that are added by ngIf etc. (rendering of the node is async because of
567+
// lazy transclusion)
568+
selectCtrl.registerOption = function(optionScope, optionEl) {
569+
if (optionEl.val() === '') {
570+
selectCtrl.hasEmptyOption = true;
571+
selectCtrl.emptyOption = optionEl;
572+
selectCtrl.emptyOption.removeClass('ng-scope');
573+
// This ensures the new empty option is selected if previously no option was selected
574+
ngModelCtrl.$render();
575+
576+
optionEl.on('$destroy', function() {
577+
selectCtrl.hasEmptyOption = false;
578+
selectCtrl.emptyOption = undefined;
579+
});
580+
}
581+
};
582+
583+
} else {
584+
// remove the class, which is added automatically because we recompile the element and it
585+
// becomes the compilation root
586+
selectCtrl.emptyOption.removeClass('ng-scope');
587+
}
588+
562589
}
563590

564591
selectElement.empty();

src/ng/directive/select.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ var SelectController =
3232
// to create it in <select> and IE barfs otherwise.
3333
self.unknownOption = jqLite(window.document.createElement('option'));
3434

35+
// The empty option is an option with the value '' that te application developer can
36+
// provide inside the select. When the model changes to a value that doesn't match an option,
37+
// it is selected - so if an empty option is provided, no unknown option is generated.
38+
// However, the empty option is not removed when the model matches an option. It is always selectable
39+
// and indicates that a "null" selection has been made.
40+
self.hasEmptyOption = false;
41+
self.emptyOption = undefined;
42+
3543
self.renderUnknownOption = function(val) {
3644
var unknownVal = self.generateUnknownOptionValue(val);
3745
self.unknownOption.val(unknownVal);
@@ -55,13 +63,6 @@ var SelectController =
5563
if (self.unknownOption.parent()) self.unknownOption.remove();
5664
};
5765

58-
// The empty option is an option with the value '' that te application developer can
59-
// provide inside the select. When the model changes to a value that doesn't match an option,
60-
// it is selected - so if an empty option is provided, no unknown option is generated.
61-
// However, the empty option is not removed when the model matches an option. It is always selectable
62-
// and indicates that a "null" selection has been made.
63-
self.emptyOption = undefined;
64-
6566
self.selectEmptyOption = function() {
6667
if (self.emptyOption) {
6768
$element.val('');
@@ -70,7 +71,7 @@ var SelectController =
7071
};
7172

7273
self.unselectEmptyOption = function() {
73-
if (self.emptyOption) {
74+
if (self.hasEmptyOption) {
7475
self.emptyOption.removeAttr('selected');
7576
}
7677
};
@@ -132,6 +133,7 @@ var SelectController =
132133

133134
assertNotHasOwnProperty(value, '"option value"');
134135
if (value === '') {
136+
self.hasEmptyOption = true;
135137
self.emptyOption = element;
136138
}
137139
var count = optionsMap.get(value) || 0;
@@ -148,6 +150,7 @@ var SelectController =
148150
if (count === 1) {
149151
optionsMap.remove(value);
150152
if (value === '') {
153+
self.hasEmptyOption = false;
151154
self.emptyOption = undefined;
152155
}
153156
} else {

test/ng/directive/ngOptionsSpec.js

+90
Original file line numberDiff line numberDiff line change
@@ -2524,6 +2524,96 @@ describe('ngOptions', function() {
25242524
}
25252525
);
25262526

2527+
2528+
it('should select the correct option after linking when the ngIf expression is initially falsy', function() {
2529+
scope.values = [
2530+
{name:'black'},
2531+
{name:'white'},
2532+
{name:'red'}
2533+
];
2534+
scope.selected = scope.values[2];
2535+
2536+
expect(function() {
2537+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2538+
scope.$apply();
2539+
}).not.toThrow();
2540+
2541+
expect(element.find('option')[2]).toBeMarkedAsSelected();
2542+
expect(linkLog).toEqual(['linkNgOptions']);
2543+
});
2544+
2545+
2546+
it('should add / remove the "selected" attribute on empty option which has an initially falsy ngIf expression', function() {
2547+
scope.values = [
2548+
{name:'black'},
2549+
{name:'white'},
2550+
{name:'red'}
2551+
];
2552+
scope.selected = scope.values[2];
2553+
2554+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2555+
scope.$apply();
2556+
2557+
expect(element.find('option')[2]).toBeMarkedAsSelected();
2558+
2559+
scope.$apply('isBlank = true');
2560+
expect(element.find('option')[0].value).toBe('');
2561+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2562+
2563+
scope.$apply('selected = null');
2564+
expect(element.find('option')[0].value).toBe('');
2565+
expect(element.find('option')[0]).toBeMarkedAsSelected();
2566+
2567+
scope.selected = scope.values[1];
2568+
scope.$apply();
2569+
expect(element.find('option')[0].value).toBe('');
2570+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2571+
expect(element.find('option')[2]).toBeMarkedAsSelected();
2572+
});
2573+
2574+
2575+
it('should add / remove the "selected" attribute on empty option which has an initially truthy ngIf expression when no option is selected', function() {
2576+
scope.values = [
2577+
{name:'black'},
2578+
{name:'white'},
2579+
{name:'red'}
2580+
];
2581+
scope.isBlank = true;
2582+
2583+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2584+
scope.$apply();
2585+
2586+
expect(element.find('option')[0].value).toBe('');
2587+
expect(element.find('option')[0]).toBeMarkedAsSelected();
2588+
2589+
scope.selected = scope.values[2];
2590+
scope.$apply();
2591+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2592+
expect(element.find('option')[3]).toBeMarkedAsSelected();
2593+
});
2594+
2595+
2596+
it('should add the "selected" attribute on empty option which has an initially falsy ngIf expression when no option is selected', function() {
2597+
scope.values = [
2598+
{name:'black'},
2599+
{name:'white'},
2600+
{name:'red'}
2601+
];
2602+
2603+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2604+
scope.$apply();
2605+
2606+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2607+
2608+
scope.isBlank = true;
2609+
scope.$apply();
2610+
2611+
expect(element.find('option')[0].value).toBe('');
2612+
expect(element.find('option')[0]).toBeMarkedAsSelected();
2613+
expect(element.find('option')[1]).not.toBeMarkedAsSelected();
2614+
});
2615+
2616+
25272617
it('should not throw when a directive compiles the blank option before ngOptions is linked', function() {
25282618
expect(function() {
25292619
createSelect({

0 commit comments

Comments
 (0)