-
Notifications
You must be signed in to change notification settings - Fork 27.4k
perf(ngOptions): prevent initial options repaiting #15812
Conversation
Hi, Please also note that we definitely need to support an empty option with ngIf - there are some tests for this, and I assume some of them will fail in the current state. You can run the unit tests locally with |
Hi, let's see if I can fix it. Thanks for the info! |
The code just passed all the tests. This solution avoids double repainting of |
Thx @pbr1111, I will review this next week. Was there any reason for closing / reopening the issue so often? |
I close and reopen because eslint tests fails several times and I've made a rebase of my branch each time it fails to upload the new commit with the errors solved. |
If you push new / changed code, it'll run Travis again, no need to close / reopen the issue. |
src/ng/directive/ngOptions.js
Outdated
@@ -624,7 +629,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 (option.label !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
element.label always will be undefined (because we create a new empty option element each time), then it's useless to read element.label value if we know that its value is undefined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use isDefined
here
src/ng/directive/ngOptions.js
Outdated
break; | ||
} | ||
var optionsToRemove = []; | ||
for (var i = 0, children = selectElement[0].childNodes, ii = children.length; i < ii; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the move from jqLite/jQuery APIs to native APIs part of the perf fix or is it just a "generally good idea" type of change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jqLite/jQuery children() method doesn't return the nodes of nodeType equal to NODE_TYPE_COMMENT. We can use jqLite/jQuery contents() method instead, but I prefer to use native API.
src/ng/directive/ngOptions.js
Outdated
var optionsToRemove = []; | ||
for (var i = 0, children = selectElement[0].childNodes, ii = children.length; i < ii; i++) { | ||
if (!selectCtrl.hasEmptyOption | ||
&& (children[i].value === '' || children[i].nodeType === NODE_TYPE_COMMENT)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, any comment will be interpreted as empty option? This doesn't seem right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I'm doing changes to my commit. I will simplify this whole part and leave it as close to the previous code.
I just modified the previous commit. Now the select element is emptied after it founds (or not) an empty option (the code is the same as before, it works because the options haven't yet been compiled in postLink, my previous solution was totally incorrect). The empty option element is readded if it's provided. |
@pbr1111 You can just push a new commit and you / we will squash before merging.
You should include a test that works with the correct version but fails with the incorrect one - it sounds like we are missing a test there We should also have a test that ensures we don't paint the option twice. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good so far! Some minor clarifications and changes nedded. I would like for @petebacondarwin too also take a look because I remember he once wasn't a fan of doing if (!options) return
src/ng/directive/ngOptions.js
Outdated
@@ -449,36 +452,38 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, | |||
if (!multiple) { | |||
|
|||
selectCtrl.writeValue = function writeNgOptionsValue(value) { | |||
var selectedOption = options.selectValueMap[selectElement.val()]; | |||
var option = options.getOptionFromViewValue(value); | |||
if (options) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change to if (!options) return
? This will reduce the whitespace changes.
test/ng/directive/ngOptionsSpec.js
Outdated
]; | ||
scope.isBlank = true; | ||
|
||
createSingleSelect('<!-- test comment --><option ng-if="isBlank" value="">blank</option><!-- test comment 2 -->'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long, please split at around 100 chars
test/ng/directive/ngOptionsSpec.js
Outdated
@@ -2641,6 +2658,23 @@ describe('ngOptions', function() { | |||
dealoc(element); | |||
})); | |||
|
|||
|
|||
it('should ignore the comments within the select', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no expectations regarding the comments you added. Should this test that all comments that are not part of the empty option are removed?
test/ng/directive/ngOptionsSpec.js
Outdated
@@ -779,6 +779,23 @@ describe('ngOptions', function() { | |||
expect(options[2]).not.toBeMarkedAsSelected(); | |||
}); | |||
|
|||
|
|||
it('should render the options only one time', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the initial options
test/ng/directive/ngOptionsSpec.js
Outdated
'ng-model': 'value' | ||
}); | ||
|
||
element.attr('ng-options', 'value for value in values'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should pass the ngoptions expression like you did with ng-model. Or use createSingleSelect which has useful defaults.
src/ng/directive/ngOptions.js
Outdated
@@ -624,7 +629,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 (option.label !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use isDefined
here
src/ng/directive/ngOptions.js
Outdated
@@ -428,6 +428,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, | |||
} | |||
} | |||
|
|||
// Remove all the elements inside the select. | |||
selectElement.empty(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add something like:
"The empty option will be compiled and rendered before we first generate the options"
test/ng/directive/ngOptionsSpec.js
Outdated
var repaint = 0; | ||
element[0].addEventListener('DOMSubtreeModified', function(ev) { | ||
repaint++; | ||
expect(repaint).toBeLessThan(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firefox 47 failed this expect. Probably because DOMSubtreeModified
behaves differently in it. Maybe we should use MutationObserver instead for Browsers that support it?
'ng-model': 'value' | ||
}); | ||
|
||
// Add ngOptions as attribute before recompiling the select element |
There was a problem hiding this comment.
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.
Unfortunately, Firefox still reports 2 paints ... :( |
Avoid double execution of `updateOptions()` method, which causes a complete repainting of all `<option>` elements. Fixes angular#15801 Closes angular#15812
Avoid double execution of `updateOptions()` method, which causes a complete repainting of all `<option>` elements. Fixes angular#15801 Closes angular#15812 Close angular#16071
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Bug fix.
What is the current behavior? (You can also link to an open issue here)
Refers to this issue: ngOptions slow performance in IE due to option rerendering
What is the new behavior (if this is a feature change)?
Now the options are loaded once when we create the
<select ng-options>
directive. Also, if we add an<option>
element with an ng-if directive, it is not deleted now.Does this PR introduce a breaking change?
No.
Please check if the PR fulfills these requirements
Other information:
None.