Skip to content

Commit 278e191

Browse files
committed
fix(drawer): infinite loop when two-way opened binding is toggled mid-animation
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 0c39219 commit 278e191

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';
@@ -138,6 +138,7 @@ describe('MatDrawer', () => {
138138
expect(testComponent.openCount).toBe(0);
139139
expect(testComponent.closeCount).toBe(0);
140140

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

@@ -367,6 +368,37 @@ describe('MatDrawer', () => {
367368

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

372404
describe('focus trapping behavior', () => {
@@ -496,6 +528,7 @@ describe('MatDrawerContainer', () => {
496528

497529
fixture.componentInstance.renderDrawer = false;
498530
fixture.detectChanges();
531+
tick();
499532

500533
expect(parseInt(contentElement.style.marginLeft)).toBeLessThan(initialMargin);
501534
}));

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')
@@ -390,10 +392,9 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
390392
_onAnimationEnd(event: AnimationEvent) {
391393
const {fromState, toState} = event;
392394

393-
if (toState.indexOf('open') === 0 && fromState === 'void') {
394-
this.openedChange.emit(true);
395-
} else if (toState === 'void' && fromState.indexOf('open') === 0) {
396-
this.openedChange.emit(false);
395+
if ((toState.indexOf('open') === 0 && fromState === 'void') ||
396+
(toState === 'void' && fromState.indexOf('open') === 0)) {
397+
this.openedChange.emit(this._opened);
397398
}
398399
}
399400

0 commit comments

Comments
 (0)