Skip to content

Commit 2f70a0e

Browse files
committed
feat(autocomplete): allow use of obj values
1 parent a94d135 commit 2f70a0e

File tree

6 files changed

+269
-33
lines changed

6 files changed

+269
-33
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<div [style.height.px]="topHeightCtrl.value"></div>
33
<div class="demo-autocomplete">
44
<md-card>
5-
<div>Reactive value: {{ stateCtrl.value }}</div>
5+
Reactive length: {{ reactiveStates.length }}
6+
<div>Reactive value: {{ stateCtrl.value | json }}</div>
67
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>
78

89
<md-input-container>
@@ -11,7 +12,7 @@
1112

1213
<md-card-actions>
1314
<button md-button (click)="stateCtrl.reset()">RESET</button>
14-
<button md-button (click)="stateCtrl.setValue('California')">SET VALUE</button>
15+
<button md-button (click)="stateCtrl.setValue(states[10])">SET VALUE</button>
1516
<button md-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
1617
TOGGLE DISABLED
1718
</button>
@@ -39,8 +40,8 @@
3940
</md-card>
4041
</div>
4142

42-
<md-autocomplete #reactiveAuto="mdAutocomplete">
43-
<md-option *ngFor="let state of reactiveStates" [value]="state.name">
43+
<md-autocomplete #reactiveAuto="mdAutocomplete" [displayWith]="displayFn">
44+
<md-option *ngFor="let state of reactiveStates" [value]="state">
4445
<span>{{ state.name }}</span>
4546
<span class="demo-secondary-text"> ({{state.code}}) </span>
4647
</md-option>
@@ -49,6 +50,5 @@
4950
<md-autocomplete #tdAuto="mdAutocomplete">
5051
<md-option *ngFor="let state of tdStates" [value]="state.name">
5152
<span>{{ state.name }}</span>
52-
<span class="demo-secondary-text"> ({{state.code}}) </span>
5353
</md-option>
5454
</md-autocomplete>

src/demo-app/autocomplete/autocomplete-demo.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
flex-flow: row wrap;
44

55
md-card {
6-
width: 350px;
6+
width: 400px;
77
margin: 24px;
88
}
99

src/demo-app/autocomplete/autocomplete-demo.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Component, OnDestroy, ViewEncapsulation} from '@angular/core';
22
import {FormControl} from '@angular/forms';
33
import {Subscription} from 'rxjs/Subscription';
4+
import 'rxjs/add/operator/startWith';
45

56
@Component({
67
moduleId: module.id,
@@ -10,7 +11,7 @@ import {Subscription} from 'rxjs/Subscription';
1011
encapsulation: ViewEncapsulation.None
1112
})
1213
export class AutocompleteDemo implements OnDestroy {
13-
stateCtrl = new FormControl();
14+
stateCtrl: FormControl;
1415
currentState = '';
1516
topHeightCtrl = new FormControl(0);
1617

@@ -76,9 +77,16 @@ export class AutocompleteDemo implements OnDestroy {
7677
constructor() {
7778
this.reactiveStates = this.states;
7879
this.tdStates = this.states;
80+
this.stateCtrl = new FormControl(this.reactiveStates[2]);
7981
this.reactiveValueSub =
80-
this.stateCtrl.valueChanges.subscribe(val => this.reactiveStates = this.filterStates(val));
82+
this.stateCtrl.valueChanges.startWith(this.reactiveStates[2]).subscribe((val) => {
83+
const name = typeof val === 'string' || val === null ? val : val.name;
84+
this.reactiveStates = this.filterStates(name);
85+
});
86+
}
8187

88+
displayFn(value: any): string {
89+
return value ? value.name : null;
8290
}
8391

8492
filterStates(val: string) {

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import {
2-
AfterContentInit, Directive, ElementRef, Input, ViewContainerRef, Optional, OnDestroy
2+
AfterContentInit,
3+
Directive,
4+
ElementRef,
5+
forwardRef,
6+
Input,
7+
Optional,
8+
OnDestroy,
9+
ViewContainerRef,
310
} from '@angular/core';
4-
import {NgControl} from '@angular/forms';
11+
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
512
import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core';
613
import {MdAutocomplete} from './autocomplete';
714
import {PositionStrategy} from '../core/overlay/position/position-strategy';
@@ -28,6 +35,16 @@ export const AUTOCOMPLETE_OPTION_HEIGHT = 48;
2835
/** The total height of the autocomplete panel. */
2936
export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
3037

38+
/**
39+
* Provider that allows the autocomplete to register as a ControlValueAccessor.
40+
* @docs-private
41+
*/
42+
export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
43+
provide: NG_VALUE_ACCESSOR,
44+
useExisting: forwardRef(() => MdAutocompleteTrigger),
45+
multi: true
46+
};
47+
3148
@Directive({
3249
selector: 'input[mdAutocomplete], input[matAutocomplete]',
3350
host: {
@@ -39,10 +56,13 @@ export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
3956
'[attr.aria-expanded]': 'panelOpen.toString()',
4057
'[attr.aria-owns]': 'autocomplete?.id',
4158
'(focus)': 'openPanel()',
59+
'(blur)': '_onTouched()',
60+
'(input)': '_onChange($event.target.value)',
4261
'(keydown)': '_handleKeydown($event)',
43-
}
62+
},
63+
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
4464
})
45-
export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
65+
export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAccessor, OnDestroy {
4666
private _overlayRef: OverlayRef;
4767
private _portal: TemplatePortal;
4868
private _panelOpen: boolean = false;
@@ -54,12 +74,18 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
5474
private _keyManager: ActiveDescendantKeyManager;
5575
private _positionStrategy: ConnectedPositionStrategy;
5676

77+
/** View -> model callback called when value changes */
78+
_onChange: (value: any) => {};
79+
80+
/** View -> model callback called when autocomplete has been touched */
81+
_onTouched = () => {};
82+
5783
/* The autocomplete panel to be attached to this trigger. */
5884
@Input('mdAutocomplete') autocomplete: MdAutocomplete;
5985

6086
constructor(private _element: ElementRef, private _overlay: Overlay,
6187
private _viewContainerRef: ViewContainerRef,
62-
@Optional() private _controlDir: NgControl, @Optional() private _dir: Dir) {}
88+
@Optional() private _dir: Dir) {}
6389

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

152+
/**
153+
* Sets the autocomplete's value. Part of the ControlValueAccessor interface
154+
* required to integrate with Angular's core forms API.
155+
*
156+
* @param value New value to be written to the model.
157+
*/
158+
writeValue(value: any): void {
159+
Promise.resolve(null).then(() => this._setTriggerValue(value));
160+
}
161+
162+
/**
163+
* Saves a callback function to be invoked when the autocomplete's value
164+
* changes from user input. Part of the ControlValueAccessor interface
165+
* required to integrate with Angular's core forms API.
166+
*
167+
* @param fn Callback to be triggered when the value changes.
168+
*/
169+
registerOnChange(fn: (value: any) => {}): void {
170+
this._onChange = fn;
171+
}
172+
173+
/**
174+
* Saves a callback function to be invoked when the autocomplete is blurred
175+
* by the user. Part of the ControlValueAccessor interface required
176+
* to integrate with Angular's core forms API.
177+
*
178+
* @param fn Callback to be triggered when the component has been touched.
179+
*/
180+
registerOnTouched(fn: () => {}) {
181+
this._onTouched = fn;
182+
}
183+
126184
_handleKeydown(event: KeyboardEvent): void {
127185
if (this.activeOption && event.keyCode === ENTER) {
128186
this.activeOption._selectViaInteraction();
@@ -178,17 +236,20 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
178236
}
179237
}
180238

239+
private _setTriggerValue(value: any): void {
240+
this._element.nativeElement.value =
241+
this.autocomplete.displayWith ? this.autocomplete.displayWith(value) : value;
242+
}
243+
181244
/**
182245
* This method closes the panel, and if a value is specified, also sets the associated
183246
* control to that value. It will also mark the control as dirty if this interaction
184247
* stemmed from the user.
185248
*/
186249
private _setValueAndClose(event: MdOptionSelectEvent | null): void {
187250
if (event) {
188-
this._controlDir.control.setValue(event.source.value);
189-
if (event.isUserInput) {
190-
this._controlDir.control.markAsDirty();
191-
}
251+
this._setTriggerValue(event.source.value);
252+
this._onChange(event.source.value);
192253
}
193254

194255
this.closePanel();

0 commit comments

Comments
 (0)