diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html
index f1e903f4824b..e2813192aafe 100644
--- a/src/demo-app/select/select-demo.html
+++ b/src/demo-app/select/select-demo.html
@@ -33,6 +33,26 @@
+
+
+ Filter selection
+
+
+
+ {{ movie.viewValue }}
+
+ Value: {{ currentMovie }}
+ Touched: {{ movieControl.touched }}
+ Dirty: {{ movieControl.dirty }}
+ Status: {{ movieControl.control?.status }}
+
+
+
+
+
+
+
+
diff --git a/src/demo-app/select/select-demo.ts b/src/demo-app/select/select-demo.ts
index 0b369e0c137b..0994a5ac1aa8 100644
--- a/src/demo-app/select/select-demo.ts
+++ b/src/demo-app/select/select-demo.ts
@@ -10,9 +10,12 @@ import {MdSelectChange} from '@angular/material';
})
export class SelectDemo {
isRequired = false;
+ movieRequired = false;
isDisabled = false;
+ moviesDisabled = false;
showSelect = false;
currentDrink: string;
+ currentMovie: string;
latestChangeEvent: MdSelectChange;
foodControl = new FormControl('pizza-1');
@@ -40,6 +43,17 @@ export class SelectDemo {
{value: 'squirtle-2', viewValue: 'Squirtle'}
];
+ movies = [
+ {value: 'moonraker-0', viewValue: 'Moonraker'},
+ {value: 'goldfinger-1', viewValue: 'Sprite'},
+ {value: 'thunderball-2', viewValue: 'Water'},
+ {value: 'dr-no-3', viewValue: 'Dr. No'},
+ {value: 'octopussy-4', viewValue: 'Octopussy'},
+ {value: 'goldeneye-5', viewValue: 'Goldeneye'},
+ {value: 'skyfall-6', viewValue: 'Skyfall'},
+ {value: 'spectre-7', viewValue: 'Spectre'}
+ ];
+
toggleDisabled() {
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
}
diff --git a/src/lib/select/index.ts b/src/lib/select/index.ts
index 1a2e4afe6a88..339981ad75e0 100644
--- a/src/lib/select/index.ts
+++ b/src/lib/select/index.ts
@@ -6,12 +6,21 @@ import {
CompatibilityModule,
OverlayModule,
} from '../core';
+import {MdInputModule} from '../input/input';
+import {FormsModule} from '@angular/forms';
export * from './select';
export {fadeInContent, transformPanel, transformPlaceholder} from './select-animations';
@NgModule({
- imports: [CommonModule, OverlayModule, MdOptionModule, CompatibilityModule],
+ imports: [
+ CommonModule,
+ OverlayModule,
+ MdOptionModule,
+ CompatibilityModule,
+ MdInputModule,
+ FormsModule
+ ],
exports: [MdSelect, MdOptionModule, CompatibilityModule],
declarations: [MdSelect],
})
diff --git a/src/lib/select/select.html b/src/lib/select/select.html
index 61e55e51a4e9..53c51f04220a 100644
--- a/src/lib/select/select.html
+++ b/src/lib/select/select.html
@@ -6,12 +6,13 @@
+ backdropClass="cdk-overlay-transparent-backdrop" [positions]="_positions" [minWidth]="_triggerWidth"
+ [offsetY]="_offsetY" [offsetX]="_offsetX" (attach)="_setScrollTop()">
+ (keydown)="_keyManager.onKeydown($event)" [style.transformOrigin]="_transformOrigin"
+ [class.md-select-panel-done-animating]="_panelDoneAnimating">
+
diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss
index 3365d5c8caaf..3ddb7ee5f60b 100644
--- a/src/lib/select/select.scss
+++ b/src/lib/select/select.scss
@@ -108,3 +108,19 @@ md-select {
}
}
+.md-select-content md-input {
+ @include md-menu-item-base();
+ height: auto;
+
+ .md-input-wrapper {
+ width: 100%;
+ }
+
+ .md-input-underline {
+ width: calc(100% - #{$md-menu-side-padding*2});
+ }
+}
+
+md-option[hidden] {
+ display: none;
+}
\ No newline at end of file
diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts
index 5f57794bdd80..e28d277c536f 100644
--- a/src/lib/select/select.spec.ts
+++ b/src/lib/select/select.spec.ts
@@ -20,6 +20,7 @@ describe('MdSelect', () => {
imports: [MdSelectModule.forRoot(), ReactiveFormsModule, FormsModule],
declarations: [
BasicSelect,
+ SearchSelect,
NgModelSelect,
ManySelects,
NgIfSelect,
@@ -1255,6 +1256,57 @@ describe('MdSelect', () => {
expect(fixture.componentInstance.changeListener).toHaveBeenCalledTimes(1);
});
});
+
+ describe('Search input', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SearchSelect);
+ fixture.detectChanges();
+
+ let trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
+ trigger.click();
+ fixture.detectChanges();
+ });
+
+ it('should hide elements that are not in the filter', () => {
+ fixture.componentInstance.select._onSearch('steak');
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ let options =
+ overlayContainerElement.querySelectorAll('md-option') as NodeListOf;
+
+ expect(options[0].getAttribute('hidden')).toEqual(null);
+ expect(options[1].getAttribute('hidden')).toEqual('true');
+ expect(options[2].getAttribute('hidden')).toEqual('true');
+ expect(options[3].getAttribute('hidden')).toEqual('true');
+ expect(options[4].getAttribute('hidden')).toEqual('true');
+ expect(options[5].getAttribute('hidden')).toEqual('true');
+ expect(options[6].getAttribute('hidden')).toEqual('true');
+ expect(options[7].getAttribute('hidden')).toEqual('true');
+ });
+ });
+
+ it('should should not be case sensitive', () => {
+ fixture.componentInstance.select._onSearch('S-');
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ let options =
+ overlayContainerElement.querySelectorAll('md-option') as NodeListOf;
+
+ expect(options[0].getAttribute('hidden')).toEqual('true');
+ expect(options[1].getAttribute('hidden')).toEqual('true');
+ expect(options[2].getAttribute('hidden')).toEqual(null);
+ expect(options[3].getAttribute('hidden')).toEqual('true');
+ expect(options[4].getAttribute('hidden')).toEqual(null);
+ expect(options[5].getAttribute('hidden')).toEqual(null);
+ expect(options[6].getAttribute('hidden')).toEqual('true');
+ expect(options[7].getAttribute('hidden')).toEqual('true');
+ });
+ });
+ });
});
@Component({
@@ -1289,6 +1341,38 @@ class BasicSelect {
@ViewChildren(MdOption) options: QueryList;
}
+@Component({
+ selector: 'search-select',
+ template: `
+
+
+
+ {{ food.viewValue }}
+
+
+
+ `
+})
+class SearchSelect {
+ foods: any[] = [
+ { value: 'steak-0', viewValue: 'Steak' },
+ { value: 'pizza-1', viewValue: 'Pizza' },
+ { value: 'tacos-2', viewValue: 'Tacos', disabled: true },
+ { value: 'sandwich-3', viewValue: 'Sandwich' },
+ { value: 'chips-4', viewValue: 'Chips' },
+ { value: 'eggs-5', viewValue: 'Eggs' },
+ { value: 'pasta-6', viewValue: 'Pasta' },
+ { value: 'sushi-7', viewValue: 'Sushi' },
+ ];
+ control = new FormControl();
+ isRequired: boolean;
+ heightAbove = 0;
+ heightBelow = 0;
+
+ @ViewChild(MdSelect) select: MdSelect;
+ @ViewChildren(MdOption) options: QueryList;
+}
+
@Component({
selector: 'ng-model-select',
template: `
diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts
index 1a3dabfd5fc6..f6b110666588 100644
--- a/src/lib/select/select.ts
+++ b/src/lib/select/select.ts
@@ -222,6 +222,14 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
get required() { return this._required; }
set required(value: any) { this._required = coerceBooleanProperty(value); }
+ /** An optional search function. */
+ @Input()
+ get search() { return this._search; }
+ set search(search: boolean) {
+ this._search = search;
+ }
+ private _search: boolean;
+
/** Event emitted when the select has been opened. */
@Output() onOpen: EventEmitter = new EventEmitter();
@@ -272,6 +280,9 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
this._calculateOverlayPosition();
this._placeholderState = this._isRtl() ? 'floating-rtl' : 'floating-ltr';
this._panelOpen = true;
+ if (this._search) {
+ this._onSearch('');
+ }
}
/** Closes the overlay panel and focuses the host element. */
@@ -410,6 +421,18 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
scrollContainer.scrollTop = this._scrollTop;
}
+ _onSearch(query: string): void {
+ this.options.forEach((option: MdOption) => {
+ let valueIndex = option.value.toLowerCase().indexOf(query.toLowerCase());
+ let viewValueIndex = option.viewValue.toLowerCase().indexOf(query.toLowerCase());
+ if (valueIndex === -1 && viewValueIndex === -1) {
+ option._getHostElement().setAttribute('hidden', 'true');
+ } else {
+ option._getHostElement().removeAttribute('hidden');
+ }
+ });
+ }
+
/**
* Sets the selected option based on a value. If no option can be
* found with the designated value, the select trigger is cleared.
@@ -546,7 +569,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
// The farthest the panel can be scrolled before it hits the bottom
const maxScroll = scrollContainerHeight - panelHeight;
- if (this.selected) {
+ if (this.selected && !this._search) {
const selectedIndex = this._getOptionIndex(this.selected);
// We must maintain a scroll buffer so the selected option will be scrolled to the
// center of the overlay panel rather than the top.