Skip to content

fix(select): avoid going into infinite loop under certain conditions #2955

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

Merged
merged 5 commits into from
Feb 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ describe('MdDialog', () => {

expect(document.activeElement.id)
.toBe('dialog-trigger', 'Expected that the trigger was refocused after dialog close');

document.body.removeChild(button);
}));
});

Expand Down
34 changes: 33 additions & 1 deletion src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ViewChild,
ViewChildren,
ChangeDetectionStrategy,
OnInit,
} from '@angular/core';
import {MdSelectModule} from './index';
import {OverlayContainer} from '../core/overlay/overlay-container';
Expand Down Expand Up @@ -34,6 +35,8 @@ describe('MdSelect', () => {
SelectWithChangeEvent,
CustomSelectAccessor,
CompWithCustomSelect,
SelectWithErrorSibling,
ThrowsErrorOnInit,
BasicSelectOnPush
],
providers: [
Expand Down Expand Up @@ -1239,6 +1242,14 @@ describe('MdSelect', () => {
});
}));

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

});

describe('change event', () => {
Expand Down Expand Up @@ -1281,7 +1292,7 @@ describe('MdSelect', () => {
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelectOnPush);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
});

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

@Component({
selector: 'select-infinite-loop',
template: `
<md-select [(ngModel)]="value"></md-select>
<throws-error-on-init></throws-error-on-init>
`
})
class SelectWithErrorSibling {
value: string;
}

@Component({
selector: 'throws-error-on-init',
template: ''
})
export class ThrowsErrorOnInit implements OnInit {
ngOnInit() {
throw new Error('Oh no!');
}
}

@Component({
selector: 'basic-select-on-push',
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
29 changes: 15 additions & 14 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {ControlValueAccessor, NgControl} from '@angular/forms';
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
import {ConnectedOverlayDirective} from '../core/overlay/overlay-directives';
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
import 'rxjs/add/operator/startWith';


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

ngAfterContentInit() {
this._initKeyManager();
this._resetOptions();
this._changeSubscription = this.options.changes.subscribe(() => {

this._changeSubscription = this.options.changes.startWith(null).subscribe(() => {
this._resetOptions();

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

ngOnDestroy() {
this._dropSubscriptions();
this._changeSubscription.unsubscribe();
this._tabSubscription.unsubscribe();

if (this._changeSubscription) {
this._changeSubscription.unsubscribe();
}

if (this._tabSubscription) {
this._tabSubscription.unsubscribe();
}
}

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

this._setSelectionByValue(value);
this._changeDetectorRef.markForCheck();
}

/**
Expand Down