Skip to content

Commit

Permalink
feat(autocomplete): add value support (angular#2516)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara committed Jan 20, 2017
1 parent 4a84cb4 commit 343e7e3
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 40 deletions.
55 changes: 49 additions & 6 deletions src/demo-app/autocomplete/autocomplete-demo.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
<div class="demo-autocomplete">
<md-input-container>
<input mdInput placeholder="State" [mdAutocomplete]="auto">
</md-input-container>
<md-card>
<div>Reactive value: {{ stateCtrl.value }}</div>
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>

<md-autocomplete #auto="mdAutocomplete">
<md-option *ngFor="let state of states" [value]="state.code"> {{ state.name }} </md-option>
</md-autocomplete>
<md-input-container>
<input mdInput placeholder="State" [mdAutocomplete]="reactiveAuto" [formControl]="stateCtrl">
</md-input-container>

<md-card-actions>
<button md-button (click)="stateCtrl.reset()">RESET</button>
<button md-button (click)="stateCtrl.setValue('California')">SET VALUE</button>
<button md-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
TOGGLE DISABLED
</button>
</md-card-actions>

</md-card>

<md-card>
<div>Template-driven value (currentState): {{ currentState }}</div>
<div>Template-driven dirty: {{ modelDir.dirty }}</div>

<md-input-container>
<input mdInput placeholder="State" [mdAutocomplete]="tdAuto" [(ngModel)]="currentState" #modelDir="ngModel"
(ngModelChange)="this.tdStates = filterStates(currentState)" [disabled]="tdDisabled">
</md-input-container>

<md-card-actions>
<button md-button (click)="modelDir.reset()">RESET</button>
<button md-button (click)="currentState='California'">SET VALUE</button>
<button md-button (click)="tdDisabled=!tdDisabled">
TOGGLE DISABLED
</button>
</md-card-actions>

</md-card>
</div>

<md-autocomplete #reactiveAuto="mdAutocomplete">
<md-option *ngFor="let state of reactiveStates" [value]="state.name">
<span>{{ state.name }}</span>
<span class="demo-secondary-text"> ({{state.code}}) </span>
</md-option>
</md-autocomplete>

<md-autocomplete #tdAuto="mdAutocomplete">
<md-option *ngFor="let state of tdStates" [value]="state.name">
<span>{{ state.name }}</span>
<span class="demo-secondary-text"> ({{state.code}}) </span>
</md-option>
</md-autocomplete>
19 changes: 18 additions & 1 deletion src/demo-app/autocomplete/autocomplete-demo.scss
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
.demo-autocomplete {}
.demo-autocomplete {
display: flex;
flex-flow: row wrap;

md-card {
width: 350px;
margin: 24px;
}

md-input-container {
margin-top: 16px;
}
}

.demo-secondary-text {
color: rgba(0, 0, 0, 0.54);
margin-left: 8px;
}
33 changes: 31 additions & 2 deletions src/demo-app/autocomplete/autocomplete-demo.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import {Component} from '@angular/core';
import {Component, OnDestroy, ViewEncapsulation} from '@angular/core';
import {FormControl} from '@angular/forms';
import {Subscription} from 'rxjs/Subscription';

@Component({
moduleId: module.id,
selector: 'autocomplete-demo',
templateUrl: 'autocomplete-demo.html',
styleUrls: ['autocomplete-demo.css'],
encapsulation: ViewEncapsulation.None
})
export class AutocompleteDemo {
export class AutocompleteDemo implements OnDestroy {
stateCtrl = new FormControl();
currentState = '';

reactiveStates: any[];
tdStates: any[];

reactiveValueSub: Subscription;
tdDisabled = false;

states = [
{code: 'AL', name: 'Alabama'},
{code: 'AZ', name: 'Arizona'},
Expand Down Expand Up @@ -35,4 +47,21 @@ export class AutocompleteDemo {
{code: 'WI', name: 'Wisconsin'},
{code: 'WY', name: 'Wyoming'},
];

constructor() {
this.reactiveStates = this.states;
this.tdStates = this.states;
this.reactiveValueSub =
this.stateCtrl.valueChanges.subscribe(val => this.reactiveStates = this.filterStates(val));

}

filterStates(val: string) {
return val ? this.states.filter((s) => s.name.match(new RegExp(val, 'gi'))) : this.states;
}

ngOnDestroy() {
this.reactiveValueSub.unsubscribe();
}

}
51 changes: 43 additions & 8 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import {
Directive, ElementRef, Input, ViewContainerRef, Optional, OnDestroy
} from '@angular/core';
import {NgControl} from '@angular/forms';
import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core';
import {MdAutocomplete} from './autocomplete';
import {PositionStrategy} from '../core/overlay/position/position-strategy';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
import {MdOptionSelectEvent} from '../core/option/option';
import 'rxjs/add/observable/merge';
import {Dir} from '../core/rtl/dir';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/switchMap';


/** The panel needs a slight y-offset to ensure the input underline displays. */
export const MD_AUTOCOMPLETE_PANEL_OFFSET = 6;
Expand All @@ -23,14 +27,12 @@ export class MdAutocompleteTrigger implements OnDestroy {
private _portal: TemplatePortal;
private _panelOpen: boolean = false;

/** The subscription to events that close the autocomplete panel. */
private _closingActionsSubscription: Subscription;

/* The autocomplete panel to be attached to this trigger. */
@Input('mdAutocomplete') autocomplete: MdAutocomplete;

constructor(private _element: ElementRef, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir) {}
private _viewContainerRef: ViewContainerRef,
@Optional() private _controlDir: NgControl, @Optional() private _dir: Dir) {}

ngOnDestroy() { this._destroyPanel(); }

Expand All @@ -47,8 +49,7 @@ export class MdAutocompleteTrigger implements OnDestroy {

if (!this._overlayRef.hasAttached()) {
this._overlayRef.attach(this._portal);
this._closingActionsSubscription =
this.panelClosingActions.subscribe(() => this.closePanel());
this._subscribeToClosingActions();
}

this._panelOpen = true;
Expand All @@ -60,7 +61,6 @@ export class MdAutocompleteTrigger implements OnDestroy {
this._overlayRef.detach();
}

this._closingActionsSubscription.unsubscribe();
this._panelOpen = false;
}

Expand All @@ -78,6 +78,25 @@ export class MdAutocompleteTrigger implements OnDestroy {
return this.autocomplete.options.map(option => option.onSelect);
}


/**
* This method listens to a stream of panel closing actions and resets the
* stream every time the option list changes.
*/
private _subscribeToClosingActions(): void {
// Every time the option list changes...
this.autocomplete.options.changes
// and also at initialization, before there are any option changes...
.startWith(null)
// create a new stream of panelClosingActions, replacing any previous streams
// that were created, and flatten it so our stream only emits closing events...
.switchMap(() => this.panelClosingActions)
// when the first closing event occurs...
.first()
// set the value, close the panel, and complete.
.subscribe(event => this._setValueAndClose(event));
}

/** Destroys the autocomplete suggestion panel. */
private _destroyPanel(): void {
if (this._overlayRef) {
Expand All @@ -87,6 +106,22 @@ export class MdAutocompleteTrigger implements OnDestroy {
}
}

/**
* This method closes the panel, and if a value is specified, also sets the associated
* control to that value. It will also mark the control as dirty if this interaction
* stemmed from the user.
*/
private _setValueAndClose(event: MdOptionSelectEvent | null): void {
if (event) {
this._controlDir.control.setValue(event.source.value);
if (event.isUserInput) {
this._controlDir.control.markAsDirty();
}
}

this.closePanel();
}

private _createOverlay(): void {
this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef);
this._overlayRef = this._overlay.create(this._getOverlayConfig());
Expand Down
Loading

0 comments on commit 343e7e3

Please sign in to comment.