Skip to content

Commit 7c84490

Browse files
crisbetoandrewseguin
authored andcommitted
fix(drawer): infinite loop when two-way opened binding is toggled mid-animation (#8872)
As of 004e0fe the drawer supports a two-way binding on the `opened` property, however because the `openedChange` emits at the end of the animation and two-way bindings invoke their setter which then trigger an animation which in turn invokes `openedChange`, there is the potential for an infinite loop if the user changes the `opened` state mid-animation. These changes avoid the issue by turning the `openedChanged` into an async stream and emitting the current value at the end of the animation, rather than determining the value based on the animation state. Fixes #8869.
1 parent 50967b6 commit 7c84490

File tree

2 files changed

+40
-6
lines changed

2 files changed

+40
-6
lines changed

src/lib/sidenav/drawer.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '@angular/core/testing';
99
import {Component, ElementRef, ViewChild} from '@angular/core';
1010
import {By} from '@angular/platform-browser';
11-
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
11+
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
1212
import {MatDrawer, MatSidenavModule, MatDrawerContainer} from './index';
1313
import {A11yModule} from '@angular/cdk/a11y';
1414
import {PlatformModule} from '@angular/cdk/platform';
@@ -139,6 +139,7 @@ describe('MatDrawer', () => {
139139
expect(testComponent.openCount).toBe(0);
140140
expect(testComponent.closeCount).toBe(0);
141141

142+
tick();
142143
fixture.debugElement.query(By.css('.close')).nativeElement.click();
143144
fixture.detectChanges();
144145

@@ -368,6 +369,37 @@ describe('MatDrawer', () => {
368369

369370
expect(fixture.componentInstance.isOpen).toBe(true);
370371
}));
372+
373+
it('should not throw when a two-way binding is toggled quickly while animating',
374+
fakeAsync(() => {
375+
TestBed
376+
.resetTestingModule()
377+
.configureTestingModule({
378+
imports: [MatSidenavModule, BrowserAnimationsModule],
379+
declarations: [DrawerOpenBinding],
380+
})
381+
.compileComponents();
382+
383+
const fixture = TestBed.createComponent(DrawerOpenBinding);
384+
fixture.detectChanges();
385+
386+
// Note that we need actual timeouts and the `BrowserAnimationsModule`
387+
// in order to test it correctly.
388+
setTimeout(() => {
389+
fixture.componentInstance.isOpen = !fixture.componentInstance.isOpen;
390+
expect(() => fixture.detectChanges()).not.toThrow();
391+
392+
setTimeout(() => {
393+
fixture.componentInstance.isOpen = !fixture.componentInstance.isOpen;
394+
expect(() => fixture.detectChanges()).not.toThrow();
395+
}, 1);
396+
397+
tick(1);
398+
}, 1);
399+
400+
tick(1);
401+
}));
402+
371403
});
372404

373405
describe('focus trapping behavior', () => {
@@ -512,6 +544,7 @@ describe('MatDrawerContainer', () => {
512544

513545
fixture.componentInstance.renderDrawer = false;
514546
fixture.detectChanges();
547+
tick();
515548

516549
expect(parseInt(contentElement.style.marginLeft)).toBeLessThan(initialMargin);
517550
}));

src/lib/sidenav/drawer.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
195195
_animationState: 'open-instant' | 'open' | 'void' = 'void';
196196

197197
/** Event emitted when the drawer open state is changed. */
198-
@Output() openedChange: EventEmitter<boolean> = new EventEmitter<boolean>();
198+
@Output() openedChange: EventEmitter<boolean> =
199+
// Note this has to be async in order to avoid some issues with two-bindings (see #8872).
200+
new EventEmitter<boolean>(/* isAsync */true);
199201

200202
/** Event emitted when the drawer has been opened. */
201203
@Output('opened')
@@ -402,10 +404,9 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
402404
_onAnimationEnd(event: AnimationEvent) {
403405
const {fromState, toState} = event;
404406

405-
if (toState.indexOf('open') === 0 && fromState === 'void') {
406-
this.openedChange.emit(true);
407-
} else if (toState === 'void' && fromState.indexOf('open') === 0) {
408-
this.openedChange.emit(false);
407+
if ((toState.indexOf('open') === 0 && fromState === 'void') ||
408+
(toState === 'void' && fromState.indexOf('open') === 0)) {
409+
this.openedChange.emit(this._opened);
409410
}
410411
}
411412

0 commit comments

Comments
 (0)