Skip to content

Commit

Permalink
feat(autocomplete): allow use of obj values (#2792)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara committed Feb 3, 2017
1 parent fd5e4d9 commit 55e1847
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 44 deletions.
10 changes: 5 additions & 5 deletions src/demo-app/autocomplete/autocomplete-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<div [style.height.px]="topHeightCtrl.value"></div>
<div class="demo-autocomplete">
<md-card>
<div>Reactive value: {{ stateCtrl.value }}</div>
Reactive length: {{ reactiveStates.length }}
<div>Reactive value: {{ stateCtrl.value | json }}</div>
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>

<md-input-container>
Expand All @@ -11,7 +12,7 @@

<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.setValue(states[10])">SET VALUE</button>
<button md-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
TOGGLE DISABLED
</button>
Expand Down Expand Up @@ -39,8 +40,8 @@
</md-card>
</div>

<md-autocomplete #reactiveAuto="mdAutocomplete">
<md-option *ngFor="let state of reactiveStates" [value]="state.name">
<md-autocomplete #reactiveAuto="mdAutocomplete" [displayWith]="displayFn">
<md-option *ngFor="let state of reactiveStates | async" [value]="state">
<span>{{ state.name }}</span>
<span class="demo-secondary-text"> ({{state.code}}) </span>
</md-option>
Expand All @@ -49,6 +50,5 @@
<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>
2 changes: 1 addition & 1 deletion src/demo-app/autocomplete/autocomplete-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
flex-flow: row wrap;

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

Expand Down
27 changes: 14 additions & 13 deletions src/demo-app/autocomplete/autocomplete-demo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Component, OnDestroy, ViewEncapsulation} from '@angular/core';
import {Component, ViewEncapsulation} from '@angular/core';
import {FormControl} from '@angular/forms';
import {Subscription} from 'rxjs/Subscription';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/startWith';

@Component({
moduleId: module.id,
Expand All @@ -9,15 +10,14 @@ import {Subscription} from 'rxjs/Subscription';
styleUrls: ['autocomplete-demo.css'],
encapsulation: ViewEncapsulation.None
})
export class AutocompleteDemo implements OnDestroy {
stateCtrl = new FormControl();
export class AutocompleteDemo {
stateCtrl: FormControl;
currentState = '';
topHeightCtrl = new FormControl(0);

reactiveStates: any[];
reactiveStates: Observable<any>;
tdStates: any[];

reactiveValueSub: Subscription;
tdDisabled = false;

states = [
Expand Down Expand Up @@ -74,19 +74,20 @@ export class AutocompleteDemo implements OnDestroy {
];

constructor() {
this.reactiveStates = this.states;
this.tdStates = this.states;
this.reactiveValueSub =
this.stateCtrl.valueChanges.subscribe(val => this.reactiveStates = this.filterStates(val));
this.stateCtrl = new FormControl({code: 'CA', name: 'California'});
this.reactiveStates = this.stateCtrl.valueChanges
.startWith(this.stateCtrl.value)
.map(val => this.displayFn(val))
.map(name => this.filterStates(name));
}

displayFn(value: any): string {
return value && typeof value === 'object' ? value.name : value;
}

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

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

}
79 changes: 70 additions & 9 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import {
AfterContentInit, Directive, ElementRef, Input, ViewContainerRef, Optional, OnDestroy
AfterContentInit,
Directive,
ElementRef,
forwardRef,
Input,
Optional,
OnDestroy,
ViewContainerRef,
} from '@angular/core';
import {NgControl} from '@angular/forms';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core';
import {MdAutocomplete} from './autocomplete';
import {PositionStrategy} from '../core/overlay/position/position-strategy';
Expand All @@ -28,6 +35,16 @@ export const AUTOCOMPLETE_OPTION_HEIGHT = 48;
/** The total height of the autocomplete panel. */
export const AUTOCOMPLETE_PANEL_HEIGHT = 256;

/**
* Provider that allows the autocomplete to register as a ControlValueAccessor.
* @docs-private
*/
export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MdAutocompleteTrigger),
multi: true
};

@Directive({
selector: 'input[mdAutocomplete], input[matAutocomplete]',
host: {
Expand All @@ -39,10 +56,13 @@ export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
'[attr.aria-expanded]': 'panelOpen.toString()',
'[attr.aria-owns]': 'autocomplete?.id',
'(focus)': 'openPanel()',
'(blur)': '_onTouched()',
'(input)': '_onChange($event.target.value)',
'(keydown)': '_handleKeydown($event)',
}
},
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
})
export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAccessor, OnDestroy {
private _overlayRef: OverlayRef;
private _portal: TemplatePortal;
private _panelOpen: boolean = false;
Expand All @@ -54,12 +74,18 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
private _keyManager: ActiveDescendantKeyManager;
private _positionStrategy: ConnectedPositionStrategy;

/** View -> model callback called when value changes */
_onChange: (value: any) => {};

/** View -> model callback called when autocomplete has been touched */
_onTouched = () => {};

/* 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 _controlDir: NgControl, @Optional() private _dir: Dir) {}
@Optional() private _dir: Dir) {}

ngAfterContentInit() {
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap();
Expand Down Expand Up @@ -123,6 +149,38 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
return this._keyManager.activeItem as MdOption;
}

/**
* Sets the autocomplete's value. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
*
* @param value New value to be written to the model.
*/
writeValue(value: any): void {
Promise.resolve(null).then(() => this._setTriggerValue(value));
}

/**
* Saves a callback function to be invoked when the autocomplete's value
* changes from user input. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
*
* @param fn Callback to be triggered when the value changes.
*/
registerOnChange(fn: (value: any) => {}): void {
this._onChange = fn;
}

/**
* Saves a callback function to be invoked when the autocomplete is blurred
* by the user. Part of the ControlValueAccessor interface required
* to integrate with Angular's core forms API.
*
* @param fn Callback to be triggered when the component has been touched.
*/
registerOnTouched(fn: () => {}) {
this._onTouched = fn;
}

_handleKeydown(event: KeyboardEvent): void {
if (this.activeOption && event.keyCode === ENTER) {
this.activeOption._selectViaInteraction();
Expand Down Expand Up @@ -178,17 +236,20 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
}
}

private _setTriggerValue(value: any): void {
this._element.nativeElement.value =
this.autocomplete.displayWith ? this.autocomplete.displayWith(value) : value;
}

/**
* 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._setTriggerValue(event.source.value);
this._onChange(event.source.value);
}

this.closePanel();
Expand Down
Loading

0 comments on commit 55e1847

Please sign in to comment.