Skip to content

Commit 998a583

Browse files
crisbetotinayuangao
authored andcommitted
fix(select): avoid going into infinite loop under certain conditions (#2955)
* fix(select): avoid going into infinite loop under certain conditions Currently `md-select` calls `writeValue` recursively until the `QueryList` with all of the options is initialized. This usually means 1-2 iterations max, however in certain conditions (e.g. a sibling component throws an error on init) this may not happen and the browser would get thrown into an infinite loop. This change switches to saving the attempted value assignments in a property and applying it after initialization. Fixes #2950. * fix: more elegant approach to initial value and add unit test * fix: button not being removed after test * chore: remove unnecessary expression
1 parent 93937e6 commit 998a583

File tree

3 files changed

+50
-15
lines changed

3 files changed

+50
-15
lines changed

src/lib/dialog/dialog.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ describe('MdDialog', () => {
381381

382382
expect(document.activeElement.id)
383383
.toBe('dialog-trigger', 'Expected that the trigger was refocused after dialog close');
384+
385+
document.body.removeChild(button);
384386
}));
385387
});
386388

src/lib/select/select.spec.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ViewChild,
88
ViewChildren,
99
ChangeDetectionStrategy,
10+
OnInit,
1011
} from '@angular/core';
1112
import {MdSelectModule} from './index';
1213
import {OverlayContainer} from '../core/overlay/overlay-container';
@@ -34,6 +35,8 @@ describe('MdSelect', () => {
3435
SelectWithChangeEvent,
3536
CustomSelectAccessor,
3637
CompWithCustomSelect,
38+
SelectWithErrorSibling,
39+
ThrowsErrorOnInit,
3740
BasicSelectOnPush
3841
],
3942
providers: [
@@ -1239,6 +1242,14 @@ describe('MdSelect', () => {
12391242
});
12401243
}));
12411244

1245+
it('should not crash the browser when a sibling throws an error on init', async(() => {
1246+
// Note that this test can be considered successful if the error being thrown didn't
1247+
// end up crashing the testing setup altogether.
1248+
expect(() => {
1249+
TestBed.createComponent(SelectWithErrorSibling).detectChanges();
1250+
}).toThrowError(new RegExp('Oh no!', 'g'));
1251+
}));
1252+
12421253
});
12431254

12441255
describe('change event', () => {
@@ -1281,7 +1292,7 @@ describe('MdSelect', () => {
12811292
beforeEach(() => {
12821293
fixture = TestBed.createComponent(BasicSelectOnPush);
12831294
fixture.detectChanges();
1284-
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
1295+
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
12851296
});
12861297

12871298
it('should update the trigger based on the value', () => {
@@ -1474,6 +1485,27 @@ class CompWithCustomSelect {
14741485
@ViewChild(CustomSelectAccessor) customAccessor: CustomSelectAccessor;
14751486
}
14761487

1488+
@Component({
1489+
selector: 'select-infinite-loop',
1490+
template: `
1491+
<md-select [(ngModel)]="value"></md-select>
1492+
<throws-error-on-init></throws-error-on-init>
1493+
`
1494+
})
1495+
class SelectWithErrorSibling {
1496+
value: string;
1497+
}
1498+
1499+
@Component({
1500+
selector: 'throws-error-on-init',
1501+
template: ''
1502+
})
1503+
export class ThrowsErrorOnInit implements OnInit {
1504+
ngOnInit() {
1505+
throw new Error('Oh no!');
1506+
}
1507+
}
1508+
14771509
@Component({
14781510
selector: 'basic-select-on-push',
14791511
changeDetection: ChangeDetectionStrategy.OnPush,

src/lib/select/select.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {ControlValueAccessor, NgControl} from '@angular/forms';
2525
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
2626
import {ConnectedOverlayDirective} from '../core/overlay/overlay-directives';
2727
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
28+
import 'rxjs/add/operator/startWith';
29+
2830

2931
/**
3032
* The following style constants are necessary to save here in order
@@ -243,8 +245,8 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
243245

244246
ngAfterContentInit() {
245247
this._initKeyManager();
246-
this._resetOptions();
247-
this._changeSubscription = this.options.changes.subscribe(() => {
248+
249+
this._changeSubscription = this.options.changes.startWith(null).subscribe(() => {
248250
this._resetOptions();
249251

250252
if (this._control) {
@@ -257,8 +259,14 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
257259

258260
ngOnDestroy() {
259261
this._dropSubscriptions();
260-
this._changeSubscription.unsubscribe();
261-
this._tabSubscription.unsubscribe();
262+
263+
if (this._changeSubscription) {
264+
this._changeSubscription.unsubscribe();
265+
}
266+
267+
if (this._tabSubscription) {
268+
this._tabSubscription.unsubscribe();
269+
}
262270
}
263271

264272
/** Toggles the overlay panel open or closed. */
@@ -292,17 +300,10 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
292300
* @param value New value to be written to the model.
293301
*/
294302
writeValue(value: any): void {
295-
if (!this.options) {
296-
// In reactive forms, writeValue() will be called synchronously before
297-
// the select's child options have been created. It's necessary to call
298-
// writeValue() again after the options have been created to ensure any
299-
// initial view value is set.
300-
Promise.resolve(null).then(() => this.writeValue(value));
301-
return;
303+
if (this.options) {
304+
this._setSelectionByValue(value);
305+
this._changeDetectorRef.markForCheck();
302306
}
303-
304-
this._setSelectionByValue(value);
305-
this._changeDetectorRef.markForCheck();
306307
}
307308

308309
/**

0 commit comments

Comments
 (0)