Skip to content

Commit 8449ef4

Browse files
authored
fix(autocomplete): close panel when options list is empty (#2834)
1 parent 88b54ae commit 8449ef4

File tree

2 files changed

+294
-266
lines changed

2 files changed

+294
-266
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {
44
ElementRef,
55
forwardRef,
66
Input,
7+
NgZone,
78
Optional,
89
OnDestroy,
10+
QueryList,
911
ViewContainerRef,
1012
} from '@angular/core';
1113
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
@@ -18,6 +20,7 @@ import {MdOptionSelectEvent, MdOption} from '../core/option/option';
1820
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';
1921
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
2022
import {Subscription} from 'rxjs/Subscription';
23+
import 'rxjs/add/observable/of';
2124
import 'rxjs/add/observable/merge';
2225
import {Dir} from '../core/rtl/dir';
2326
import 'rxjs/add/operator/startWith';
@@ -57,7 +60,7 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
5760
'[attr.aria-owns]': 'autocomplete?.id',
5861
'(focus)': 'openPanel()',
5962
'(blur)': '_onTouched()',
60-
'(input)': '_onChange($event.target.value)',
63+
'(input)': '_handleInput($event.target.value)',
6164
'(keydown)': '_handleKeydown($event)',
6265
},
6366
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
@@ -85,7 +88,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
8588

8689
constructor(private _element: ElementRef, private _overlay: Overlay,
8790
private _viewContainerRef: ViewContainerRef,
88-
@Optional() private _dir: Dir) {}
91+
@Optional() private _dir: Dir, private _zone: NgZone) {}
8992

9093
ngAfterContentInit() {
9194
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap();
@@ -131,7 +134,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
131134
* A stream of actions that should close the autocomplete panel, including
132135
* when an option is selected and when the backdrop is clicked.
133136
*/
134-
get panelClosingActions(): Observable<any> {
137+
get panelClosingActions(): Observable<MdOptionSelectEvent> {
135138
return Observable.merge(
136139
...this.optionSelections,
137140
this._overlayRef.backdropClick(),
@@ -140,7 +143,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
140143
}
141144

142145
/** Stream of autocomplete option selections. */
143-
get optionSelections(): Observable<any>[] {
146+
get optionSelections(): Observable<MdOptionSelectEvent>[] {
144147
return this.autocomplete.options.map(option => option.onSelect);
145148
}
146149

@@ -185,14 +188,19 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
185188
if (this.activeOption && event.keyCode === ENTER) {
186189
this.activeOption._selectViaInteraction();
187190
} else {
188-
this.openPanel();
189191
this._keyManager.onKeydown(event);
190192
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
193+
this.openPanel();
191194
this._scrollToOption();
192195
}
193196
}
194197
}
195198

199+
_handleInput(value: string): void {
200+
this._onChange(value);
201+
this.openPanel();
202+
}
203+
196204
/**
197205
* Given that we are not actually focusing active options, we must manually adjust scroll
198206
* to reveal options below the fold. First, we find the offset of the option from the top
@@ -211,22 +219,33 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
211219
* stream every time the option list changes.
212220
*/
213221
private _subscribeToClosingActions(): void {
214-
// Every time the option list changes...
215-
this.autocomplete.options.changes
216-
// and also at initialization, before there are any option changes...
217-
.startWith(null)
222+
const initialOptions = this._getStableOptions();
223+
224+
// When the zone is stable initially, and when the option list changes...
225+
Observable.merge(initialOptions, this.autocomplete.options.changes)
218226
// create a new stream of panelClosingActions, replacing any previous streams
219227
// that were created, and flatten it so our stream only emits closing events...
220-
.switchMap(() => {
228+
.switchMap(options => {
221229
this._resetPanel();
222-
return this.panelClosingActions;
230+
// If the options list is empty, emit close event immediately.
231+
// Otherwise, listen for panel closing actions...
232+
return options.length ? this.panelClosingActions : Observable.of(null);
223233
})
224234
// when the first closing event occurs...
225235
.first()
226236
// set the value, close the panel, and complete.
227237
.subscribe(event => this._setValueAndClose(event));
228238
}
229239

240+
/**
241+
* Retrieves the option list once the zone stabilizes. It's important to wait until
242+
* stable so that change detection can run first and update the query list
243+
* with the options available under the current filter.
244+
*/
245+
private _getStableOptions(): Observable<QueryList<MdOption>> {
246+
return this._zone.onStable.first().map(() => this.autocomplete.options);
247+
}
248+
230249
/** Destroys the autocomplete suggestion panel. */
231250
private _destroyPanel(): void {
232251
if (this._overlayRef) {

0 commit comments

Comments
 (0)