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

perf(ngOptions): prevent initial options repaiting #15812

Closed
wants to merge 3 commits into from
Closed
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
29 changes: 13 additions & 16 deletions src/ng/directive/ngOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
}
}

// The empty option will be compiled and rendered before we first generate the options
selectElement.empty();

var providedEmptyOption = !!selectCtrl.emptyOption;

var unknownOption = jqLite(optionTemplate.cloneNode(false));
Expand All @@ -449,6 +452,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
if (!multiple) {

selectCtrl.writeValue = function writeNgOptionsValue(value) {
if (!options)
return;

var selectedOption = options.selectValueMap[selectElement.val()];
var option = options.getOptionFromViewValue(value);

Expand Down Expand Up @@ -508,6 +514,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
} else {

selectCtrl.writeValue = function writeNgOptionsMultiple(values) {
if (!options)
return;

// Only set `<option>.selected` if necessary, in order to prevent some browsers from
// scrolling to `<option>` elements that are outside the `<select>` element's viewport.

Expand Down Expand Up @@ -552,13 +561,12 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,

if (providedEmptyOption) {

// we need to remove it before calling selectElement.empty() because otherwise IE will
// remove the label from the element. wtf?
selectCtrl.emptyOption.remove();

// compile the element since there might be bindings in it
$compile(selectCtrl.emptyOption)(scope);

// Ensure that the empty option is always there if it was explicitly provided
selectElement.prepend(selectCtrl.emptyOption);

if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
// This means the empty option has currently no actual DOM node, probably because
// it has been modified by a transclusion directive.
Expand Down Expand Up @@ -590,12 +598,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,

}

selectElement.empty();

// We need to do this here to ensure that the options object is defined
// when we first hit it in writeNgOptionsValue
updateOptions();

// We will re-render the option elements if the option values or labels change
scope.$watchCollection(ngOptions.getWatchables, updateOptions);

Expand Down Expand Up @@ -624,7 +626,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
// the option list in listbox style, i.e. the select is [multiple], or specifies a [size].
// See https://github.com/angular/angular.js/issues/11314 for more info.
// This is unfortunately untestable with unit / e2e tests
if (option.label !== element.label) {
if (isDefined(option.label)) {
element.label = option.label;
element.textContent = option.label;
}
Expand Down Expand Up @@ -655,11 +657,6 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,

var groupElementMap = {};

// Ensure that the empty option is always there if it was explicitly provided
if (providedEmptyOption) {
selectElement.prepend(selectCtrl.emptyOption);
}

options.items.forEach(function addOption(option) {
var groupElement;

Expand Down
45 changes: 45 additions & 0 deletions test/ng/directive/ngOptionsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,32 @@ describe('ngOptions', function() {
expect(options[2]).not.toBeMarkedAsSelected();
});


it('should render the initial options only one time', function() {
scope.values = ['black', 'white', 'red'];
// Insert a select element into the DOM without ng-options to suscribe to DOM change events
createSelect({
'ng-model': 'value'
});

// Add ngOptions as attribute before recompiling the select element
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for clarifying why you did it like this ;)
However, recompiling is generally a bad idea, and I wouldn't put it in a test. The better option is to add the listener via a custom directive.

element.attr('ng-options', 'value for value in values');

var repaint = 0;
// Detect only when the childNodes count changes
var lastElementChildrenCount = -1;
element[0].addEventListener('DOMSubtreeModified', function(ev) {
var childrenCount = jqLite(ev.target).contents().length;
if (childrenCount !== lastElementChildrenCount) {
repaint++;
lastElementChildrenCount = childrenCount;
expect(repaint).toBeLessThan(2);
}
});

$compile(element)(scope);
});

describe('disableWhen expression', function() {

describe('on single select', function() {
Expand Down Expand Up @@ -2641,6 +2667,25 @@ describe('ngOptions', function() {
dealoc(element);
}));


it('should remove all comments inside the select element except the initial falsy ngIf empty option', function() {
scope.values = [
{name:'black'},
{name:'white'},
{name:'red'}
];
scope.isBlank = false;

createSingleSelect('<!-- test comment -->'
+ '<option ng-if="isBlank" value="">blank</option>'
+ '<!-- test comment 2 -->');
scope.$apply('isBlank = true');

expect(element.find('option')[0].value).toBe('');
expect(element.find('option')[0].textContent).toBe('blank');
expect(element.find('option')[1].textContent).toBe('black');
});

});


Expand Down