diff --git a/src/demo-app/autocomplete/autocomplete-demo.html b/src/demo-app/autocomplete/autocomplete-demo.html index 1124b22a5e2f..f6597d451427 100644 --- a/src/demo-app/autocomplete/autocomplete-demo.html +++ b/src/demo-app/autocomplete/autocomplete-demo.html @@ -21,11 +21,12 @@ +
Template-driven value (currentState): {{ currentState }}
-
Template-driven dirty: {{ modelDir.dirty }}
+
Template-driven dirty: {{ modelDir?.dirty }}
- - + diff --git a/src/demo-app/autocomplete/autocomplete-demo.ts b/src/demo-app/autocomplete/autocomplete-demo.ts index d6caba770ff8..7f9ed2de234b 100644 --- a/src/demo-app/autocomplete/autocomplete-demo.ts +++ b/src/demo-app/autocomplete/autocomplete-demo.ts @@ -1,5 +1,5 @@ -import {Component, ViewEncapsulation} from '@angular/core'; -import {FormControl} from '@angular/forms'; +import {Component, ViewChild, ViewEncapsulation} from '@angular/core'; +import {FormControl, NgModel} from '@angular/forms'; import 'rxjs/add/operator/startWith'; @Component({ @@ -19,6 +19,8 @@ export class AutocompleteDemo { tdDisabled = false; + @ViewChild(NgModel) modelDir: NgModel; + states = [ {code: 'AL', name: 'Alabama'}, {code: 'AK', name: 'Alaska'}, diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 2368cdcd1e01..978e6a8019b5 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -1,5 +1,4 @@ import { - AfterContentInit, Directive, ElementRef, forwardRef, @@ -17,7 +16,6 @@ import {PositionStrategy} from '../core/overlay/position/position-strategy'; import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy'; import {Observable} from 'rxjs/Observable'; import {MdOptionSelectEvent, MdOption} from '../core/option/option'; -import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager'; import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes'; import {Dir} from '../core/rtl/dir'; import {Subscription} from 'rxjs/Subscription'; @@ -66,7 +64,7 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = { }, providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR] }) -export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAccessor, OnDestroy { +export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { private _overlayRef: OverlayRef; private _portal: TemplatePortal; private _panelOpen: boolean = false; @@ -74,8 +72,6 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce /** The subscription to positioning changes in the autocomplete panel. */ private _panelPositionSubscription: Subscription; - /** Manages active item in option list based on key events. */ - private _keyManager: ActiveDescendantKeyManager; private _positionStrategy: ConnectedPositionStrategy; /** Stream of blur events that should close the panel. */ @@ -105,10 +101,6 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce @Optional() private _dir: Dir, private _zone: NgZone, @Optional() @Host() private _inputContainer: MdInputContainer) {} - ngAfterContentInit() { - this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap(); - } - ngOnDestroy() { if (this._panelPositionSubscription) { this._panelPositionSubscription.unsubscribe(); @@ -155,7 +147,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce return Observable.merge( this.optionSelections, this._blurStream.asObservable(), - this._keyManager.tabOut + this.autocomplete._keyManager.tabOut ); } @@ -166,7 +158,9 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce /** The currently active option, coerced to MdOption type. */ get activeOption(): MdOption { - return this._keyManager.activeItem as MdOption; + if (this.autocomplete._keyManager) { + return this.autocomplete._keyManager.activeItem as MdOption; + } } /** @@ -205,7 +199,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce if (this.activeOption && event.keyCode === ENTER) { this.activeOption._selectViaInteraction(); } else { - this._keyManager.onKeydown(event); + this.autocomplete._keyManager.onKeydown(event); if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) { this.openPanel(); this._scrollToOption(); @@ -250,7 +244,8 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce * height, so the active option will be just visible at the bottom of the panel. */ private _scrollToOption(): void { - const optionOffset = this._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT; + const optionOffset = + this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT; const newScrollTop = Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT); this.autocomplete._setScrollTop(newScrollTop); @@ -344,7 +339,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce /** Reset active item to null so arrow events will activate the correct options.*/ private _resetActiveItem(): void { - this._keyManager.setActiveItem(null); + this.autocomplete._keyManager.setActiveItem(null); } /** diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 5baee70ad18e..e6095b72f493 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -13,6 +13,8 @@ import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; import {MdAutocomplete} from './autocomplete'; import {MdInputContainer} from '../input/input-container'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/map'; describe('MdAutocomplete', () => { let overlayContainerElement: HTMLElement; @@ -24,7 +26,7 @@ describe('MdAutocomplete', () => { imports: [ MdAutocompleteModule.forRoot(), MdInputModule.forRoot(), ReactiveFormsModule ], - declarations: [SimpleAutocomplete, AutocompleteWithoutForms], + declarations: [SimpleAutocomplete, AutocompleteWithoutForms, NgIfAutocomplete], providers: [ {provide: OverlayContainer, useFactory: () => { overlayContainerElement = document.createElement('div'); @@ -817,6 +819,22 @@ describe('MdAutocomplete', () => { }).not.toThrowError(); }); + it('should work when input is wrapped in ngIf', () => { + const fixture = TestBed.createComponent(NgIfAutocomplete); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + dispatchEvent('focus', input); + fixture.detectChanges(); + + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(true, `Expected panel state to read open when input is focused.`); + expect(overlayContainerElement.textContent) + .toContain('One', `Expected panel to display when input is focused.`); + expect(overlayContainerElement.textContent) + .toContain('Two', `Expected panel to display when input is focused.`); + }); + }); }); @@ -876,6 +894,35 @@ class SimpleAutocomplete implements OnDestroy { } +@Component({ + template: ` + + + + + + + {{option}} + + + ` +}) +class NgIfAutocomplete { + optionCtrl = new FormControl(); + filteredOptions: Observable; + isVisible = true; + + @ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger; + options = ['One', 'Two', 'Three']; + + constructor() { + this.filteredOptions = this.optionCtrl.valueChanges.startWith(null).map((val) => { + return val ? this.options.filter(option => new RegExp(val, 'gi').test(option)) + : this.options.slice(); + }); + } +} + @Component({ template: ` diff --git a/src/lib/autocomplete/autocomplete.ts b/src/lib/autocomplete/autocomplete.ts index bbed800589cc..f516c80c532c 100644 --- a/src/lib/autocomplete/autocomplete.ts +++ b/src/lib/autocomplete/autocomplete.ts @@ -1,4 +1,5 @@ import { + AfterContentInit, Component, ContentChildren, ElementRef, @@ -9,6 +10,7 @@ import { ViewEncapsulation } from '@angular/core'; import {MdOption} from '../core'; +import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager'; /** * Autocomplete IDs need to be unique across components, so this counter exists outside of @@ -29,7 +31,10 @@ export type AutocompletePositionY = 'above' | 'below'; '[class.mat-autocomplete]': 'true' } }) -export class MdAutocomplete { +export class MdAutocomplete implements AfterContentInit { + + /** Manages active item in option list based on key events. */ + _keyManager: ActiveDescendantKeyManager; /** Whether the autocomplete panel displays above or below its trigger. */ positionY: AutocompletePositionY = 'below'; @@ -47,6 +52,10 @@ export class MdAutocomplete { /** Unique ID to be used by autocomplete trigger's "aria-owns" property. */ id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`; + ngAfterContentInit() { + this._keyManager = new ActiveDescendantKeyManager(this.options).withWrap(); + } + /** * Sets the panel scrollTop. This allows us to manually scroll to display * options below the fold, as they are not actually being focused when active.