Skip to content

Commit af3cf62

Browse files
committed
fix(autocomplete): fix key manager instantiation
1 parent 5ef3084 commit af3cf62

File tree

5 files changed

+76
-21
lines changed

5 files changed

+76
-21
lines changed

src/demo-app/autocomplete/autocomplete-demo.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
</md-card>
2222

2323
<md-card>
24+
2425
<div>Template-driven value (currentState): {{ currentState }}</div>
25-
<div>Template-driven dirty: {{ modelDir.dirty }}</div>
26+
<div>Template-driven dirty: {{ modelDir?.dirty }}</div>
2627

27-
<md-input-container>
28-
<input mdInput placeholder="State" [mdAutocomplete]="tdAuto" [(ngModel)]="currentState" #modelDir="ngModel"
28+
<!-- Added an ngIf below to test that autocomplete works with ngIf -->
29+
<md-input-container *ngIf="true">
30+
<input mdInput placeholder="State" [mdAutocomplete]="tdAuto" [(ngModel)]="currentState"
2931
(ngModelChange)="tdStates = filterStates(currentState)" [disabled]="tdDisabled">
3032
</md-input-container>
3133

src/demo-app/autocomplete/autocomplete-demo.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {Component, ViewEncapsulation} from '@angular/core';
2-
import {FormControl} from '@angular/forms';
1+
import {Component, ViewChild, ViewEncapsulation} from '@angular/core';
2+
import {FormControl, NgModel} from '@angular/forms';
33
import 'rxjs/add/operator/startWith';
44

55
@Component({
@@ -19,6 +19,8 @@ export class AutocompleteDemo {
1919

2020
tdDisabled = false;
2121

22+
@ViewChild(NgModel) modelDir: NgModel;
23+
2224
states = [
2325
{code: 'AL', name: 'Alabama'},
2426
{code: 'AK', name: 'Alaska'},

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
AfterContentInit,
32
Directive,
43
ElementRef,
54
forwardRef,
@@ -17,7 +16,6 @@ import {PositionStrategy} from '../core/overlay/position/position-strategy';
1716
import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy';
1817
import {Observable} from 'rxjs/Observable';
1918
import {MdOptionSelectEvent, MdOption} from '../core/option/option';
20-
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';
2119
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
2220
import {Dir} from '../core/rtl/dir';
2321
import {Subscription} from 'rxjs/Subscription';
@@ -66,16 +64,14 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
6664
},
6765
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
6866
})
69-
export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAccessor, OnDestroy {
67+
export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
7068
private _overlayRef: OverlayRef;
7169
private _portal: TemplatePortal;
7270
private _panelOpen: boolean = false;
7371

7472
/** The subscription to positioning changes in the autocomplete panel. */
7573
private _panelPositionSubscription: Subscription;
7674

77-
/** Manages active item in option list based on key events. */
78-
private _keyManager: ActiveDescendantKeyManager;
7975
private _positionStrategy: ConnectedPositionStrategy;
8076

8177
/** Stream of blur events that should close the panel. */
@@ -108,10 +104,6 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
108104
@Optional() private _dir: Dir, private _zone: NgZone,
109105
@Optional() @Host() private _inputContainer: MdInputContainer) {}
110106

111-
ngAfterContentInit() {
112-
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap();
113-
}
114-
115107
ngOnDestroy() {
116108
if (this._panelPositionSubscription) {
117109
this._panelPositionSubscription.unsubscribe();
@@ -158,7 +150,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
158150
return Observable.merge(
159151
this.optionSelections,
160152
this._blurStream.asObservable(),
161-
this._keyManager.tabOut
153+
this.autocomplete._keyManager.tabOut
162154
);
163155
}
164156

@@ -169,7 +161,9 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
169161

170162
/** The currently active option, coerced to MdOption type. */
171163
get activeOption(): MdOption {
172-
return this._keyManager.activeItem as MdOption;
164+
if (this.autocomplete._keyManager) {
165+
return this.autocomplete._keyManager.activeItem as MdOption;
166+
}
173167
}
174168

175169
/**
@@ -208,7 +202,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
208202
if (this.activeOption && event.keyCode === ENTER) {
209203
this.activeOption._selectViaInteraction();
210204
} else {
211-
this._keyManager.onKeydown(event);
205+
this.autocomplete._keyManager.onKeydown(event);
212206
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
213207
this.openPanel();
214208
this._scrollToOption();
@@ -262,7 +256,8 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
262256
* height, so the active option will be just visible at the bottom of the panel.
263257
*/
264258
private _scrollToOption(): void {
265-
const optionOffset = this._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
259+
const optionOffset =
260+
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
266261
const newScrollTop =
267262
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
268263
this.autocomplete._setScrollTop(newScrollTop);
@@ -356,7 +351,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
356351

357352
/** Reset active item to null so arrow events will activate the correct options.*/
358353
private _resetActiveItem(): void {
359-
this._keyManager.setActiveItem(null);
354+
this.autocomplete._keyManager.setActiveItem(null);
360355
}
361356

362357
/**

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
1313
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
1414
import {MdAutocomplete} from './autocomplete';
1515
import {MdInputContainer} from '../input/input-container';
16+
import {Observable} from 'rxjs/Observable';
17+
import 'rxjs/add/operator/map';
1618

1719
describe('MdAutocomplete', () => {
1820
let overlayContainerElement: HTMLElement;
@@ -24,7 +26,7 @@ describe('MdAutocomplete', () => {
2426
imports: [
2527
MdAutocompleteModule.forRoot(), MdInputModule.forRoot(), ReactiveFormsModule
2628
],
27-
declarations: [SimpleAutocomplete, AutocompleteWithoutForms],
29+
declarations: [SimpleAutocomplete, AutocompleteWithoutForms, NgIfAutocomplete],
2830
providers: [
2931
{provide: OverlayContainer, useFactory: () => {
3032
overlayContainerElement = document.createElement('div');
@@ -859,6 +861,22 @@ describe('MdAutocomplete', () => {
859861
}).not.toThrowError();
860862
});
861863

864+
it('should work when input is wrapped in ngIf', () => {
865+
const fixture = TestBed.createComponent(NgIfAutocomplete);
866+
fixture.detectChanges();
867+
868+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
869+
dispatchEvent('focus', input);
870+
fixture.detectChanges();
871+
872+
expect(fixture.componentInstance.trigger.panelOpen)
873+
.toBe(true, `Expected panel state to read open when input is focused.`);
874+
expect(overlayContainerElement.textContent)
875+
.toContain('One', `Expected panel to display when input is focused.`);
876+
expect(overlayContainerElement.textContent)
877+
.toContain('Two', `Expected panel to display when input is focused.`);
878+
});
879+
862880
});
863881
});
864882

@@ -919,6 +937,35 @@ class SimpleAutocomplete implements OnDestroy {
919937

920938
}
921939

940+
@Component({
941+
template: `
942+
<md-input-container *ngIf="isVisible">
943+
<input mdInput placeholder="Choose" [mdAutocomplete]="auto" [formControl]="optionCtrl">
944+
</md-input-container>
945+
946+
<md-autocomplete #auto="mdAutocomplete">
947+
<md-option *ngFor="let option of filteredOptions | async" [value]="option">
948+
{{option}}
949+
</md-option>
950+
</md-autocomplete>
951+
`
952+
})
953+
class NgIfAutocomplete {
954+
optionCtrl = new FormControl();
955+
filteredOptions: Observable<any>;
956+
isVisible = true;
957+
958+
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
959+
options = ['One', 'Two', 'Three'];
960+
961+
constructor() {
962+
this.filteredOptions = this.optionCtrl.valueChanges.startWith(null).map((val) => {
963+
return val ? this.options.filter(option => new RegExp(val, 'gi').test(option))
964+
: this.options.slice();
965+
});
966+
}
967+
}
968+
922969

923970
@Component({
924971
template: `

src/lib/autocomplete/autocomplete.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AfterContentInit,
23
Component,
34
ContentChildren,
45
ElementRef,
@@ -9,6 +10,7 @@ import {
910
ViewEncapsulation
1011
} from '@angular/core';
1112
import {MdOption} from '../core';
13+
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';
1214

1315
/**
1416
* Autocomplete IDs need to be unique across components, so this counter exists outside of
@@ -29,7 +31,10 @@ export type AutocompletePositionY = 'above' | 'below';
2931
'[class.mat-autocomplete]': 'true'
3032
}
3133
})
32-
export class MdAutocomplete {
34+
export class MdAutocomplete implements AfterContentInit {
35+
36+
/** Manages active item in option list based on key events. */
37+
_keyManager: ActiveDescendantKeyManager;
3338

3439
/** Whether the autocomplete panel displays above or below its trigger. */
3540
positionY: AutocompletePositionY = 'below';
@@ -47,6 +52,10 @@ export class MdAutocomplete {
4752
/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
4853
id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`;
4954

55+
ngAfterContentInit() {
56+
this._keyManager = new ActiveDescendantKeyManager(this.options).withWrap();
57+
}
58+
5059
/**
5160
* Sets the panel scrollTop. This allows us to manually scroll to display
5261
* options below the fold, as they are not actually being focused when active.

0 commit comments

Comments
 (0)