Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug(tabs): fix inifinite tab loop #4639 #6663

Merged
merged 5 commits into from
Sep 1, 2017

Conversation

amcdnl
Copy link
Contributor

@amcdnl amcdnl commented Aug 27, 2017

@jelbourn @andrewseguin - The issue is the selectChange event is causing the ngAfterContentChecked to get recursively called because this change event is firing so quickly.

The root cause seems to be how mapping of the selectedIndexChange event was hooked up:

@Output() get selectedIndexChange(): Observable<number> {
    return map.call(this.selectChange, event => event.index);
  }

switching this to its own EventEmitter resolved the problem. However, that created an issue with a expression changed error, I was able to patch that by adding a timeout around that event emit and solve the issue.

@amcdnl amcdnl self-assigned this Aug 27, 2017
@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Aug 27, 2017
}

// Setup the position for each tab and optionally setup an origin on the next selected tab.
// Setup the position for each tab and optionally setup an origin on the next selected tab.
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove line change - accidental indent

@@ -70,6 +70,25 @@ describe('MdTabGroup', () => {
});
}));

it('should support two-way binding for selectedIndex', async(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Description matches the previous unit test

let indexToSelect = this._indexToSelect =
Math.min(this._tabs.length - 1, Math.max(this._indexToSelect || 0, 0));

// If there is a change in selected index, emit a change event. Should not trigger if
// the selected index has not yet been initialized.
if (this._selectedIndex != indexToSelect && this._selectedIndex != null) {
this.selectChange.emit(this._createChangeEvent(indexToSelect));
// prevent expression changed error
this._zone.run(() => this.selectedIndexChange.emit(indexToSelect));
Copy link
Contributor

@andrewseguin andrewseguin Aug 28, 2017

Choose a reason for hiding this comment

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

This makes me wonder if we should bother clamping the tab index, which is the reason why wait for checked content to emit this value. Maybe we should be allowing the dev to input a wrong index like how we do with paginator.

@jelbourn Tabs are trying to make sure they do not get into an error state. If the selectedIndex Input is set to 10 but there are only 5 tabs, we clamp it down to the last tab. This means we need to wait for content to be checked so we know how many tabs there are. What do you think about removing this convenience and just letting the dev select tab 10 and let the tab group fail?

Copy link
Contributor Author

@amcdnl amcdnl Aug 28, 2017

Choose a reason for hiding this comment

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

Personally I prefer safety checking but if we do not do that on purpose in other places, we should follow the pattern IMO.

Removing that check, could clean this up though.

let indexToSelect = this._indexToSelect =
Math.min(this._tabs.length - 1, Math.max(this._indexToSelect || 0, 0));

// If there is a change in selected index, emit a change event. Should not trigger if
// the selected index has not yet been initialized.
if (this._selectedIndex != indexToSelect && this._selectedIndex != null) {
this.selectChange.emit(this._createChangeEvent(indexToSelect));
// prevent expression changed error
Copy link
Contributor

Choose a reason for hiding this comment

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

Change comment: "Emitting this value after change detection has run since the checked content may contain this variable"

// Clamp the next selected index to the bounds of 0 and the tabs length. Note the `|| 0`, which
// ensures that values like NaN can't get through and which would otherwise throw the
// component into an infinite loop (since Math.max(NaN, 0) === NaN).
// Clamp the next selected index to the boundsof 0 and the tabs length.
Copy link
Contributor

Choose a reason for hiding this comment

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

Did the comment 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.

No, I had changed it and had to reformat it to fix length but then backed that out and didn't undo the reformat. Sorry.

component.selectedIndex = 0;
fixture.detectChanges();

setTimeout(() => {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this setTimeout instead of using fixture.whenStable()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used setTimeout because thats what his demo used.

@Output() get selectedIndexChange(): Observable<number> {
return map.call(this.selectChange, event => event.index);
}
@Output() selectedIndexChange = new EventEmitter();
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean to omit the emitted type?

let indexToSelect = this._indexToSelect =
Math.min(this._tabs.length - 1, Math.max(this._indexToSelect || 0, 0));

// If there is a change in selected index, emit a change event. Should not trigger if
// the selected index has not yet been initialized.
if (this._selectedIndex != indexToSelect && this._selectedIndex != null) {
this.selectChange.emit(this._createChangeEvent(indexToSelect));
// Emitting this value after change detection has run
// since the checked content may contain this variable
this._zone.run(() => this.selectedIndexChange.emit(indexToSelect));
Copy link
Member

Choose a reason for hiding this comment

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

It's odd to use zone.run like this; does it work if you use Promise.resolve().then(() => ...) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does work, nifty trick.

@amcdnl
Copy link
Contributor Author

amcdnl commented Aug 30, 2017

@jelbourn Ready for another review.

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

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

LGTM

@jelbourn jelbourn added pr: lgtm action: merge The PR is ready for merge by the caretaker labels Aug 31, 2017
@jelbourn jelbourn merged commit 67e02b0 into angular:master Sep 1, 2017
@amcdnl amcdnl deleted the tab-infinite-loop-4639 branch September 2, 2017 15:24
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants