Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(autocomplete): allow use of obj values #2792

Merged
merged 1 commit into from
Jan 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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