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

Commit 1d29c91

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 #15456
1 parent 0e666eb commit 1d29c91

File tree

2 files changed

+124
-6
lines changed

2 files changed

+124
-6
lines changed

src/ng/directive/ngOptions.js

+35-6
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
429429
}
430430

431431
var providedEmptyOption = !!emptyOption;
432+
var emptyOptionRendered = false;
432433

433434
var unknownOption = jqLite(optionTemplate.cloneNode(false));
434435
unknownOption.val('?');
@@ -445,14 +446,16 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
445446
selectElement.prepend(emptyOption);
446447
}
447448
selectElement.val('');
448-
emptyOption.prop('selected', true); // needed for IE
449-
emptyOption.attr('selected', true);
449+
if (emptyOptionRendered) {
450+
emptyOption.prop('selected', true); // needed for IE
451+
emptyOption.attr('selected', true);
452+
}
450453
};
451454

452455
var removeEmptyOption = function() {
453456
if (!providedEmptyOption) {
454457
emptyOption.remove();
455-
} else {
458+
} else if (emptyOptionRendered) {
456459
emptyOption.removeAttr('selected');
457460
}
458461
};
@@ -587,9 +590,35 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
587590
// compile the element since there might be bindings in it
588591
$compile(emptyOption)(scope);
589592

590-
// remove the class, which is added automatically because we recompile the element and it
591-
// becomes the compilation root
592-
emptyOption.removeClass('ng-scope');
593+
if (emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
594+
// This means the empty option has currently no actual DOM node, probably because
595+
// it has been modified by a transclusion directive.
596+
597+
emptyOptionRendered = false;
598+
599+
// Redefine the registerOption function, which will catch
600+
// options that are added by ngIf etc. (rendering of the node is async because of
601+
// lazy transclusion)
602+
selectCtrl.registerOption = function(optionScope, optionEl) {
603+
if (optionEl.val() === '') {
604+
emptyOptionRendered = true;
605+
emptyOption = optionEl;
606+
emptyOption.removeClass('ng-scope');
607+
// This ensures the new empty option is selected if previously no option was selected
608+
ngModelCtrl.$render();
609+
610+
optionEl.on('$destroy', function() {
611+
emptyOption = undefined;
612+
emptyOptionRendered = false;
613+
});
614+
}
615+
};
616+
617+
} else {
618+
emptyOption.removeClass('ng-scope');
619+
emptyOptionRendered = true;
620+
}
621+
593622
} else {
594623
emptyOption = jqLite(optionTemplate.cloneNode(false));
595624
}

test/ng/directive/ngOptionsSpec.js

+89
Original file line numberDiff line numberDiff line change
@@ -2558,6 +2558,95 @@ describe('ngOptions', function() {
25582558
});
25592559

25602560

2561+
it('should select the correct option after linking when the ngIf expression is initially falsy', function() {
2562+
scope.values = [
2563+
{name:'black'},
2564+
{name:'white'},
2565+
{name:'red'}
2566+
];
2567+
scope.selected = scope.values[2];
2568+
2569+
expect(function() {
2570+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2571+
scope.$apply();
2572+
}).not.toThrow();
2573+
2574+
expect(element.find('option')[2]).toBeMarkedAsSelected();
2575+
expect(linkLog).toEqual(['linkNgOptions']);
2576+
});
2577+
2578+
2579+
it('should add / remove the "selected" attribute on empty option which has an initially falsy ngIf expression', function() {
2580+
scope.values = [
2581+
{name:'black'},
2582+
{name:'white'},
2583+
{name:'red'}
2584+
];
2585+
scope.selected = scope.values[2];
2586+
2587+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2588+
scope.$apply();
2589+
2590+
expect(element.find('option')[2]).toBeMarkedAsSelected();
2591+
2592+
scope.$apply('isBlank = true');
2593+
expect(element.find('option')[0].value).toBe('');
2594+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2595+
2596+
scope.$apply('selected = null');
2597+
expect(element.find('option')[0].value).toBe('');
2598+
expect(element.find('option')[0]).toBeMarkedAsSelected();
2599+
2600+
scope.selected = scope.values[1];
2601+
scope.$apply();
2602+
expect(element.find('option')[0].value).toBe('');
2603+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2604+
expect(element.find('option')[2]).toBeMarkedAsSelected();
2605+
});
2606+
2607+
2608+
it('should add / remove the "selected" attribute on empty option which has an initially truthy ngIf expression when no option is selected', function() {
2609+
scope.values = [
2610+
{name:'black'},
2611+
{name:'white'},
2612+
{name:'red'}
2613+
];
2614+
scope.isBlank = true;
2615+
2616+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2617+
scope.$apply();
2618+
2619+
expect(element.find('option')[0].value).toBe('');
2620+
expect(element.find('option')[0]).toBeMarkedAsSelected();
2621+
2622+
scope.selected = scope.values[2];
2623+
scope.$apply();
2624+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2625+
expect(element.find('option')[3]).toBeMarkedAsSelected();
2626+
});
2627+
2628+
2629+
it('should add the "selected" attribute on empty option which has an initially falsy ngIf expression when no option is selected', function() {
2630+
scope.values = [
2631+
{name:'black'},
2632+
{name:'white'},
2633+
{name:'red'}
2634+
];
2635+
2636+
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2637+
scope.$apply();
2638+
2639+
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
2640+
2641+
scope.isBlank = true;
2642+
scope.$apply();
2643+
2644+
expect(element.find('option')[0].value).toBe('');
2645+
expect(element.find('option')[0]).toBeMarkedAsSelected();
2646+
expect(element.find('option')[1]).not.toBeMarkedAsSelected();
2647+
});
2648+
2649+
25612650
it('should not throw with a directive that replaces', inject(function($templateCache, $httpBackend) {
25622651
$templateCache.put('select_template.html', '<select ng-options="option as option for option in selectable_options"> <option value="">This is a test</option> </select>');
25632652

0 commit comments

Comments
 (0)