diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html
index e5444b1b66e7..280494cc935f 100644
--- a/src/demo-app/select/select-demo.html
+++ b/src/demo-app/select/select-demo.html
@@ -129,5 +129,32 @@
+
+
+ compareWith
+
+
+
+ {{ drink.viewValue }}
+
+
+ Value: {{ currentDrinkObject | json }}
+ Touched: {{ drinkObjectControl.touched }}
+ Dirty: {{ drinkObjectControl.dirty }}
+ Status: {{ drinkObjectControl.control?.status }}
+ Comparison Mode: {{ compareByValue ? 'VALUE' : 'REFERENCE' }}
+
+
+
+
+
+
+
+
This div is for testing scrolled selects.
diff --git a/src/demo-app/select/select-demo.ts b/src/demo-app/select/select-demo.ts
index 9a35b612e1c2..fe91f63bd961 100644
--- a/src/demo-app/select/select-demo.ts
+++ b/src/demo-app/select/select-demo.ts
@@ -10,11 +10,13 @@ import {MdSelectChange} from '@angular/material';
})
export class SelectDemo {
drinksRequired = false;
+ drinkObjectRequired = false;
pokemonRequired = false;
drinksDisabled = false;
pokemonDisabled = false;
showSelect = false;
currentDrink: string;
+ currentDrinkObject: {} = {value: 'tea-5', viewValue: 'Tea'};
currentPokemon: string[];
currentPokemonFromGroup: string;
currentDigimon: string;
@@ -24,6 +26,7 @@ export class SelectDemo {
topHeightCtrl = new FormControl(0);
drinksTheme = 'primary';
pokemonTheme = 'primary';
+ compareByValue = true;
foods = [
{value: null, viewValue: 'None'},
@@ -111,4 +114,20 @@ export class SelectDemo {
setPokemonValue() {
this.currentPokemon = ['eevee-4', 'psyduck-6'];
}
+
+ setDrinkObjectByCopy(selectedDrink: {}) {
+ if (selectedDrink) {
+ this.currentDrinkObject = Object.assign({}, selectedDrink);
+ } else {
+ this.currentDrinkObject = undefined;
+ }
+ }
+
+ compareDrinkObjectsByValue(d1: {value: string}, d2: {value: string}) {
+ return d1 && d2 && d1.value === d2.value;
+ }
+
+ compareByReference(o1: any, o2: any) {
+ return o1 === o2;
+ }
}
diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts
index ef8bd567ad59..0824f26650c5 100644
--- a/src/lib/select/select.spec.ts
+++ b/src/lib/select/select.spec.ts
@@ -66,7 +66,9 @@ describe('MdSelect', () => {
InvalidSelectInForm,
BasicSelectWithoutForms,
BasicSelectWithoutFormsPreselected,
- BasicSelectWithoutFormsMultiple
+ BasicSelectWithoutFormsMultiple,
+ FalsyValueSelect,
+ NgModelCompareWithSelect,
],
providers: [
{provide: OverlayContainer, useFactory: () => {
@@ -2571,8 +2573,75 @@ describe('MdSelect', () => {
});
-});
+ describe('compareWith behavior', () => {
+ let fixture: ComponentFixture;
+ let instance: NgModelCompareWithSelect;
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(NgModelCompareWithSelect);
+ instance = fixture.componentInstance;
+ spyOn(instance, 'compareByReference').and.callThrough();
+ fixture.detectChanges();
+ }));
+
+ const testCompareByReferenceBehavior = () => {
+ it('should initialize with no selection despite having a value', () => {
+ expect(instance.selectedFood.value).toBe('pizza-1');
+ expect(instance.select.selected).toBeUndefined();
+ });
+
+ it('should not update the selection when changing the value', async(() => {
+ instance.options.first._selectViaInteraction();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(instance.selectedFood.value).toEqual('steak-0');
+ expect(instance.select.selected).toBeUndefined();
+ });
+ }));
+ };
+
+ it('should not use the comparator', () => {
+ expect(instance.compareByReference).not.toHaveBeenCalled();
+ });
+ testCompareByReferenceBehavior();
+
+ describe('when comparing by reference', () => {
+ beforeEach(async(() => {
+ instance.useCompareByReference();
+ fixture.detectChanges();
+ }));
+
+ it('should use the comparator', () => {
+ expect(instance.compareByReference).toHaveBeenCalled();
+ });
+
+ testCompareByReferenceBehavior();
+ });
+
+ describe('when comparing by value', () => {
+ beforeEach(async(() => {
+ instance.useCompareByValue();
+ fixture.detectChanges();
+ }));
+
+ it('should have a selection', () => {
+ const selectedOption = instance.select.selected as MdOption;
+ expect(selectedOption.value.value).toEqual('pizza-1');
+ });
+
+ it('should update when making a new selection', async(() => {
+ instance.options.last._selectViaInteraction();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ const selectedOption = instance.select.selected as MdOption;
+ expect(instance.selectedFood.value).toEqual('tacos-2');
+ expect(selectedOption.value.value).toEqual('tacos-2');
+ });
+ }));
+ });
+ });
+});
@Component({
selector: 'basic-select',
@@ -3019,7 +3088,6 @@ class SelectWithGroups {
@ViewChildren(MdOption) options: QueryList;
}
-
@Component({
template: ``
})
@@ -3027,7 +3095,6 @@ class InvalidSelectInForm {
value: any;
}
-
@Component({
template: `
@@ -3086,3 +3153,37 @@ class BasicSelectWithoutFormsMultiple {
@ViewChild(MdSelect) select: MdSelect;
}
+
+@Component({
+ selector: 'ng-model-compare-with',
+ template: `
+
+ {{ food.viewValue }}
+
+ `
+})
+class NgModelCompareWithSelect {
+ foods: ({value: string, viewValue: string})[] = [
+ { value: 'steak-0', viewValue: 'Steak' },
+ { value: 'pizza-1', viewValue: 'Pizza' },
+ { value: 'tacos-2', viewValue: 'Tacos' },
+ ];
+ selectedFood: {value: string, viewValue: string} = { value: 'pizza-1', viewValue: 'Pizza' };
+ comparator: (f1: any, f2: any) => boolean;
+
+ @ViewChild(MdSelect) select: MdSelect;
+ @ViewChildren(MdOption) options: QueryList;
+
+ useCompareByValue() { this.comparator = this.compareByValue; }
+
+ useCompareByReference() { this.comparator = this.compareByReference; }
+
+ compareByValue(f1: any, f2: any) { return f1 && f2 && f1.value === f2.value; }
+
+ compareByReference(f1: any, f2: any) { return f1 === f2; }
+
+ setFoodByCopy(newValue: {value: string, viewValue: string}) {
+ this.selectedFood = Object.assign({}, newValue);
+ }
+}
diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts
index 9bd7ff685fed..c3e858bd8300 100644
--- a/src/lib/select/select.ts
+++ b/src/lib/select/select.ts
@@ -27,6 +27,7 @@ import {
Inject,
ChangeDetectionStrategy,
InjectionToken,
+ isDevMode,
} from '@angular/core';
import {MdOption, MdOptionSelectionChange, MdOptgroup} from '../core/option/index';
import {ENTER, SPACE, UP_ARROW, DOWN_ARROW, HOME, END} from '../core/keyboard/keycodes';
@@ -194,6 +195,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Whether the component is in multiple selection mode. */
private _multiple: boolean = false;
+ /** Comparison function to specify which option is displayed. Defaults to object equality. */
+ private _compareWith = (o1: any, o2: any) => o1 === o2;
+
/** Deals with the selection logic. */
_selectionModel: SelectionModel;
@@ -308,6 +312,16 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this._multiple = coerceBooleanProperty(value);
}
+ @Input()
+ get compareWith() { return this._compareWith; }
+ set compareWith(fn: (o1: any, o2: any) => boolean) {
+ this._compareWith = fn;
+ if (this._selectionModel) {
+ // A different comparator means the selection could change.
+ this._initializeSelection();
+ }
+ }
+
/** Whether to float the placeholder text. */
@Input()
get floatPlaceholder(): FloatPlaceholderType { return this._floatPlaceholder; }
@@ -391,13 +405,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this._initKeyManager();
this._changeSubscription = startWith.call(this.options.changes, null).subscribe(() => {
- this._resetOptions();
-
- // Defer setting the value in order to avoid the "Expression
- // has changed after it was checked" errors from Angular.
- Promise.resolve().then(() => {
- this._setSelectionByValue(this._control ? this._control.value : this._value);
- });
+ this._resetOptions();
+ this._initializeSelection();
});
}
@@ -616,6 +625,14 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
scrollContainer!.scrollTop = this._scrollTop;
}
+ private _initializeSelection(): void {
+ // Defer setting the value in order to avoid the "Expression
+ // has changed after it was checked" errors from Angular.
+ Promise.resolve().then(() => {
+ this._setSelectionByValue(this._control ? this._control.value : this._value);
+ });
+ }
+
/**
* Sets the selected option based on a value. If no option can be
* found with the designated value, the select trigger is cleared.
@@ -649,11 +666,21 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
* Finds and selects and option based on its value.
* @returns Option that has the corresponding value.
*/
- private _selectValue(value: any, isUserInput = false): MdOption | undefined {
- let optionsArray = this.options.toArray();
- let correspondingOption = optionsArray.find(option => {
- return option.value != null && option.value === value;
- });
+ private _selectValue(value: any, isUserInput = false): MdOption|undefined {
+ const optionsArray = this.options.toArray();
+
+ const findOption = (option: MdOption) => {
+ try {
+ return this._compareWith(option.value, value);
+ } catch (error) {
+ if (isDevMode()) {
+ // Notify developers of errors in their comparator.
+ console.warn(error);
+ }
+ return false;
+ }
+ };
+ const correspondingOption = optionsArray.find(findOption);
if (correspondingOption) {
isUserInput ? correspondingOption._selectViaInteraction() : correspondingOption.select();