Skip to content

Commit d7348d7

Browse files
committed
fix(autocomplete): fix key manager instantiation
1 parent c203589 commit d7348d7

File tree

5 files changed

+75
-21
lines changed

5 files changed

+75
-21
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
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+
<md-input-container *ngIf="true">
29+
<input mdInput placeholder="State" [mdAutocomplete]="tdAuto" [(ngModel)]="currentState"
2930
(ngModelChange)="this.tdStates = filterStates(currentState)" [disabled]="tdDisabled">
3031
</md-input-container>
3132

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. */
@@ -105,10 +101,6 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
105101
@Optional() private _dir: Dir, private _zone: NgZone,
106102
@Optional() @Host() private _inputContainer: MdInputContainer) {}
107103

108-
ngAfterContentInit() {
109-
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap();
110-
}
111-
112104
ngOnDestroy() {
113105
if (this._panelPositionSubscription) {
114106
this._panelPositionSubscription.unsubscribe();
@@ -155,7 +147,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
155147
return Observable.merge(
156148
this.optionSelections,
157149
this._blurStream.asObservable(),
158-
this._keyManager.tabOut
150+
this.autocomplete._keyManager.tabOut
159151
);
160152
}
161153

@@ -166,7 +158,9 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
166158

167159
/** The currently active option, coerced to MdOption type. */
168160
get activeOption(): MdOption {
169-
return this._keyManager.activeItem as MdOption;
161+
if (this.autocomplete._keyManager) {
162+
return this.autocomplete._keyManager.activeItem as MdOption;
163+
}
170164
}
171165

172166
/**
@@ -205,7 +199,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
205199
if (this.activeOption && event.keyCode === ENTER) {
206200
this.activeOption._selectViaInteraction();
207201
} else {
208-
this._keyManager.onKeydown(event);
202+
this.autocomplete._keyManager.onKeydown(event);
209203
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
210204
this.openPanel();
211205
this._scrollToOption();
@@ -250,7 +244,8 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
250244
* height, so the active option will be just visible at the bottom of the panel.
251245
*/
252246
private _scrollToOption(): void {
253-
const optionOffset = this._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
247+
const optionOffset =
248+
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
254249
const newScrollTop =
255250
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
256251
this.autocomplete._setScrollTop(newScrollTop);
@@ -344,7 +339,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
344339

345340
/** Reset active item to null so arrow events will activate the correct options.*/
346341
private _resetActiveItem(): void {
347-
this._keyManager.setActiveItem(null);
342+
this.autocomplete._keyManager.setActiveItem(null);
348343
}
349344

350345
/**

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');
@@ -817,6 +819,22 @@ describe('MdAutocomplete', () => {
817819
}).not.toThrowError();
818820
});
819821

822+
it('should work when input is wrapped in ngIf', () => {
823+
const fixture = TestBed.createComponent(NgIfAutocomplete);
824+
fixture.detectChanges();
825+
826+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
827+
dispatchEvent('focus', input);
828+
fixture.detectChanges();
829+
830+
expect(fixture.componentInstance.trigger.panelOpen)
831+
.toBe(true, `Expected panel state to read open when input is focused.`);
832+
expect(overlayContainerElement.textContent)
833+
.toContain('One', `Expected panel to display when input is focused.`);
834+
expect(overlayContainerElement.textContent)
835+
.toContain('Two', `Expected panel to display when input is focused.`);
836+
});
837+
820838
});
821839
});
822840

@@ -876,6 +894,35 @@ class SimpleAutocomplete implements OnDestroy {
876894

877895
}
878896

897+
@Component({
898+
template: `
899+
<md-input-container *ngIf="isVisible">
900+
<input mdInput placeholder="Choose" [mdAutocomplete]="auto" [formControl]="optionCtrl">
901+
</md-input-container>
902+
903+
<md-autocomplete #auto="mdAutocomplete">
904+
<md-option *ngFor="let option of filteredOptions | async" [value]="option">
905+
{{option}}
906+
</md-option>
907+
</md-autocomplete>
908+
`
909+
})
910+
class NgIfAutocomplete {
911+
optionCtrl = new FormControl();
912+
filteredOptions: Observable<any>;
913+
isVisible = true;
914+
915+
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
916+
options = ['One', 'Two', 'Three'];
917+
918+
constructor() {
919+
this.filteredOptions = this.optionCtrl.valueChanges.startWith(null).map((val) => {
920+
return val ? this.options.filter(option => new RegExp(val, 'gi').test(option))
921+
: this.options.slice();
922+
});
923+
}
924+
}
925+
879926

880927
@Component({
881928
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)