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

Commit 1026b4d

Browse files
committed
perf(ngOptions): use documentFragment to populate select
This changes the way option elements are generated when the ngOption collection changes. Previously, we would re-use option elements when possible (updating their text and label). Now, we first remove all currently displayed options and the create new options for the collection. The new options are first appended to a documentFragment, which is in the end appended to the selectElement. Using documentFragment improves render performance in IE with large option collections (> 100 elements) considerably. Always creating new options fixes issues in IE where the select would become unresponsive to user input. Fixes #13607 Fixes #12076
1 parent 8dc08fb commit 1026b4d

File tree

2 files changed

+30
-82
lines changed

2 files changed

+30
-82
lines changed

src/ng/directive/ngOptions.js

+28-82
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s
245245
// jshint maxlen: 100
246246

247247

248-
var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
248+
var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
249249

250250
function parseOptionsExpression(optionsExp, selectElement, scope) {
251251

@@ -581,6 +581,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
581581
emptyOption = jqLite(optionTemplate.cloneNode(false));
582582
}
583583

584+
selectElement[0].innerHTML = '';
585+
584586
// We need to do this here to ensure that the options object is defined
585587
// when we first hit it in writeNgOptionsValue
586588
updateOptions();
@@ -606,73 +608,35 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
606608
if (option.value !== element.value) element.value = option.selectValue;
607609
}
608610

609-
function addOrReuseElement(parent, current, type, templateElement) {
610-
var element;
611-
// Check whether we can reuse the next element
612-
if (current && lowercase(current.nodeName) === type) {
613-
// The next element is the right type so reuse it
614-
element = current;
615-
} else {
616-
// The next element is not the right type so create a new one
617-
element = templateElement.cloneNode(false);
618-
if (!current) {
619-
// There are no more elements so just append it to the select
620-
parent.appendChild(element);
621-
} else {
622-
// The next element is not a group so insert the new one
623-
parent.insertBefore(element, current);
624-
}
625-
}
626-
return element;
627-
}
628-
629-
630-
function removeExcessElements(current) {
631-
var next;
632-
while (current) {
633-
next = current.nextSibling;
634-
jqLiteRemove(current);
635-
current = next;
636-
}
637-
}
611+
function updateOptions() {
638612

613+
var previousValue = options && selectCtrl.readValue();
639614

640-
function skipEmptyAndUnknownOptions(current) {
641-
var emptyOption_ = emptyOption && emptyOption[0];
642-
var unknownOption_ = unknownOption && unknownOption[0];
643-
644-
// We cannot rely on the extracted empty option being the same as the compiled empty option,
645-
// because the compiled empty option might have been replaced by a comment because
646-
// it had an "element" transclusion directive on it (such as ngIf)
647-
if (emptyOption_ || unknownOption_) {
648-
while (current &&
649-
(current === emptyOption_ ||
650-
current === unknownOption_ ||
651-
current.nodeType === NODE_TYPE_COMMENT ||
652-
(nodeName_(current) === 'option' && current.value === ''))) {
653-
current = current.nextSibling;
615+
// We must remove all current options, but cannot simply set innerHTML = null
616+
// since the providedOption might have an ngIf on it that inserts comments, which we must
617+
// preserve
618+
// Instead iterate over the current option elements and remove them respectively their optgroup parents
619+
if (options) {
620+
for (var i = options.items.length - 1; i >= 0; i--) {
621+
var option = options.items[i];
622+
if (option.group) {
623+
jqLiteRemove(option.element.parentNode);
624+
} else {
625+
jqLiteRemove(options.items[i].element);
626+
}
654627
}
655628
}
656-
return current;
657-
}
658-
659-
660-
function updateOptions() {
661-
662-
var previousValue = options && selectCtrl.readValue();
663629

664630
options = ngOptions.getOptions();
665631

666632
var groupMap = {};
667-
var currentElement = selectElement[0].firstChild;
633+
var listFragment = $document[0].createDocumentFragment();
668634

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

674-
currentElement = skipEmptyAndUnknownOptions(currentElement);
675-
676640
options.items.forEach(function updateOption(option) {
677641
var group;
678642
var groupElement;
@@ -686,53 +650,35 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
686650

687651
if (!group) {
688652

689-
// We have not already created this group
690-
groupElement = addOrReuseElement(selectElement[0],
691-
currentElement,
692-
'optgroup',
693-
optGroupTemplate);
653+
groupElement = optGroupTemplate.cloneNode(false);
654+
listFragment.appendChild(groupElement);
655+
694656
// Move to the next element
695-
currentElement = groupElement.nextSibling;
657+
// currentElement = groupElement.nextSibling;
696658

697659
// Update the label on the group element
698660
groupElement.label = option.group;
699661

700662
// Store it for use later
701663
group = groupMap[option.group] = {
702-
groupElement: groupElement,
703-
currentOptionElement: groupElement.firstChild
664+
groupElement: groupElement
704665
};
705666

706667
}
707668

708-
// So now we have a group for this option we add the option to the group
709-
optionElement = addOrReuseElement(group.groupElement,
710-
group.currentOptionElement,
711-
'option',
712-
optionTemplate);
669+
optionElement = optionTemplate.cloneNode(false);
670+
group.groupElement.appendChild(optionElement);
713671
updateOptionElement(option, optionElement);
714-
// Move to the next element
715-
group.currentOptionElement = optionElement.nextSibling;
716672

717673
} else {
718674

719-
// This option is not in a group
720-
optionElement = addOrReuseElement(selectElement[0],
721-
currentElement,
722-
'option',
723-
optionTemplate);
675+
optionElement = optionTemplate.cloneNode(false);
676+
listFragment.appendChild(optionElement);
724677
updateOptionElement(option, optionElement);
725-
// Move to the next element
726-
currentElement = optionElement.nextSibling;
727678
}
728679
});
729680

730-
731-
// Now remove all excess options and group
732-
Object.keys(groupMap).forEach(function(key) {
733-
removeExcessElements(groupMap[key].currentOptionElement);
734-
});
735-
removeExcessElements(currentElement);
681+
selectElement[0].appendChild(listFragment);
736682

737683
ngModelCtrl.$render();
738684

test/ng/directive/ngOptionsSpec.js

+2
Original file line numberDiff line numberDiff line change
@@ -1946,6 +1946,8 @@ describe('ngOptions', function() {
19461946
scope.options[1].unavailable = false;
19471947
});
19481948

1949+
options = element.find('option');
1950+
19491951
expect(scope.options[1].unavailable).toEqual(false);
19501952
expect(options.eq(1).prop('disabled')).toEqual(false);
19511953
});

0 commit comments

Comments
 (0)