Skip to content
This repository has been 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

perf(ngOptions): prevent initial options repaiting #15812

wants to merge 3 commits into from

Conversation

pbr1111
Copy link
Contributor

@pbr1111 pbr1111 commented Mar 14, 2017

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.

@Narretz
Copy link
Contributor

Narretz commented Mar 15, 2017

Hi,
thanks for the Pr. As you can see in the Travis logs, there are a lot of test failures. Most of these come from the fact that there's a different writeSelectValue for select multiple, so you'll to make the change to this fn too.

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 grunt autotest. This runs the test every time you change a file.

@pbr1111
Copy link
Contributor Author

pbr1111 commented Mar 15, 2017

Hi, let's see if I can fix it. Thanks for the info!

@pbr1111 pbr1111 closed this Mar 17, 2017
@pbr1111 pbr1111 reopened this Mar 17, 2017
@pbr1111 pbr1111 closed this Mar 17, 2017
@pbr1111 pbr1111 reopened this Mar 17, 2017
@pbr1111 pbr1111 closed this Mar 17, 2017
@pbr1111 pbr1111 reopened this Mar 17, 2017
@pbr1111
Copy link
Contributor Author

pbr1111 commented Mar 17, 2017

The code just passed all the tests. This solution avoids double repainting of <options> and also makes ng-if work correctly in the empty option.

@Narretz
Copy link
Contributor

Narretz commented Mar 17, 2017

Thx @pbr1111, I will review this next week. Was there any reason for closing / reopening the issue so often?

@Narretz Narretz self-assigned this Mar 17, 2017
@pbr1111
Copy link
Contributor Author

pbr1111 commented Mar 17, 2017

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.

@Narretz
Copy link
Contributor

Narretz commented Mar 17, 2017

If you push new / changed code, it'll run Travis again, no need to close / reopen the issue.

@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

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

Why this change?

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

You should use isDefinedhere

break;
}
var optionsToRemove = [];
for (var i = 0, children = selectElement[0].childNodes, ii = children.length; i < ii; i++) {
Copy link
Member

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?

Copy link
Contributor Author

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.

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)) {
Copy link
Member

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.

Copy link
Contributor Author

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.

@pbr1111
Copy link
Contributor Author

pbr1111 commented Mar 20, 2017

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.

@Narretz
Copy link
Contributor

Narretz commented Mar 20, 2017

@pbr1111 You can just push a new commit and you / we will squash before merging.

it works because the options haven't yet been compiled in postLink, my previous solution was totally incorrect

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.

@pbr1111
Copy link
Contributor Author

pbr1111 commented Mar 24, 2017

I added some tests to check both cases. The second test 2ec5c07 should fail with the current implementation, but it doesn't fail with eb3661c commit.

Copy link
Contributor

@Narretz Narretz left a 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

@@ -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) {
Copy link
Contributor

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.

];
scope.isBlank = true;

createSingleSelect('<!-- test comment --><option ng-if="isBlank" value="">blank</option><!-- test comment 2 -->');
Copy link
Contributor

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

@@ -2641,6 +2658,23 @@ describe('ngOptions', function() {
dealoc(element);
}));


it('should ignore the comments within the select', function() {
Copy link
Contributor

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?

@@ -779,6 +779,23 @@ describe('ngOptions', function() {
expect(options[2]).not.toBeMarkedAsSelected();
});


it('should render the options only one time', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

the initial options

'ng-model': 'value'
});

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

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.

@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You should use isDefinedhere

@@ -428,6 +428,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
}
}

// Remove all the elements inside the select.
selectElement.empty();
Copy link
Contributor

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"

var repaint = 0;
element[0].addEventListener('DOMSubtreeModified', function(ev) {
repaint++;
expect(repaint).toBeLessThan(2);
Copy link
Contributor

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 DOMSubtreeModifiedbehaves differently in it. Maybe we should use MutationObserver instead for Browsers that support it?

Avoid double execution of `updateOptions()` method, which causes a complete repainting of all `<option>` elements.

Fixes #15801
ngOptions should ignore comments within a `<select>`element and keep only the `<!-- ngIf -->` comment of the empty option.

Ref #15812
…tions

Since 97b3e00, ngOptions are repainting the options twice when it initializes. This test verifies that the options arent unnecessarily repainted.

Ref #15812
'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.

@Narretz
Copy link
Contributor

Narretz commented Apr 4, 2017

Unfortunately, Firefox still reports 2 paints ... :(

Narretz added a commit to Narretz/angular.js that referenced this pull request Apr 26, 2017
Narretz pushed a commit to Narretz/angular.js that referenced this pull request Jun 27, 2017
Avoid double execution of `updateOptions()` method,
which causes a complete repainting of all `<option>` elements.

Fixes angular#15801
Closes angular#15812
Narretz pushed a commit to Narretz/angular.js that referenced this pull request Jun 27, 2017
Avoid double execution of `updateOptions()` method,
which causes a complete repainting of all `<option>` elements.

Fixes angular#15801
Closes angular#15812
Close angular#16071
@Narretz Narretz closed this in 6572838 Jun 29, 2017
Narretz pushed a commit that referenced this pull request Jun 29, 2017
Avoid double execution of `updateOptions()` method,
which causes a complete repainting of all `<option>` elements.

Fixes #15801
Closes #15812
Close #16071
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants