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

Commit 8e2100d

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 8e2100d

File tree

2 files changed

+32
-82
lines changed

2 files changed

+32
-82
lines changed

src/ng/directive/ngOptions.js

+30-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,37 @@ 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() {
612+
// console.log('update options', options && options.items);
638613

614+
var previousValue = options && selectCtrl.readValue();
639615

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;
616+
// We must remove all current options, but cannot simply set innerHTML = null
617+
// since the providedOption might have an ngIf on it that inserts comments, which we must
618+
// preserve
619+
// Instead iterate over the current option elements and remove them respectively their
620+
// optgroup parents
621+
if (options) {
622+
for (var i = options.items.length - 1; i >= 0; i--) {
623+
var option = options.items[i];
624+
if (option.group) {
625+
jqLiteRemove(option.element.parentNode);
626+
} else {
627+
jqLiteRemove(options.items[i].element);
628+
}
654629
}
655630
}
656-
return current;
657-
}
658-
659-
660-
function updateOptions() {
661-
662-
var previousValue = options && selectCtrl.readValue();
663631

664632
options = ngOptions.getOptions();
665633

666634
var groupMap = {};
667-
var currentElement = selectElement[0].firstChild;
635+
var listFragment = $document[0].createDocumentFragment();
668636

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

674-
currentElement = skipEmptyAndUnknownOptions(currentElement);
675-
676642
options.items.forEach(function updateOption(option) {
677643
var group;
678644
var groupElement;
@@ -686,53 +652,35 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
686652

687653
if (!group) {
688654

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

697661
// Update the label on the group element
698662
groupElement.label = option.group;
699663

700664
// Store it for use later
701665
group = groupMap[option.group] = {
702-
groupElement: groupElement,
703-
currentOptionElement: groupElement.firstChild
666+
groupElement: groupElement
704667
};
705668

706669
}
707670

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);
671+
optionElement = optionTemplate.cloneNode(false);
672+
group.groupElement.appendChild(optionElement);
713673
updateOptionElement(option, optionElement);
714-
// Move to the next element
715-
group.currentOptionElement = optionElement.nextSibling;
716674

717675
} else {
718676

719-
// This option is not in a group
720-
optionElement = addOrReuseElement(selectElement[0],
721-
currentElement,
722-
'option',
723-
optionTemplate);
677+
optionElement = optionTemplate.cloneNode(false);
678+
listFragment.appendChild(optionElement);
724679
updateOptionElement(option, optionElement);
725-
// Move to the next element
726-
currentElement = optionElement.nextSibling;
727680
}
728681
});
729682

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);
683+
selectElement[0].appendChild(listFragment);
736684

737685
ngModelCtrl.$render();
738686

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)