diff --git a/package.json b/package.json
index 36b3bdfd4..e6559231a 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"highlight.js": "^9.9.0",
"jasmine-core": "2.4.1",
"jasmine-spec-reporter": "2.5.0",
- "karma": "1.2.0",
+ "karma": "1.6.0",
"karma-browserstack-launcher": "^1.2.0",
"karma-chrome-launcher": "^2.0.0",
"karma-jasmine": "^1.1.0",
diff --git a/src/app/shared/navbar/index.ts b/src/app/shared/navbar/index.ts
index f5899d036..a44923a77 100644
--- a/src/app/shared/navbar/index.ts
+++ b/src/app/shared/navbar/index.ts
@@ -1 +1,2 @@
export * from './navbar';
+export * from './searchbar';
diff --git a/src/app/shared/navbar/navbar.html b/src/app/shared/navbar/navbar.html
index 5582045af..590e37f12 100644
--- a/src/app/shared/navbar/navbar.html
+++ b/src/app/shared/navbar/navbar.html
@@ -8,6 +8,7 @@
Components
Guides
+
.mat-button {
- &:last-child {
- margin-left: auto;
- }
- }
}
.docs-angular-logo {
diff --git a/src/app/shared/navbar/searchbar/index.ts b/src/app/shared/navbar/searchbar/index.ts
new file mode 100644
index 000000000..2520d4d6f
--- /dev/null
+++ b/src/app/shared/navbar/searchbar/index.ts
@@ -0,0 +1 @@
+export * from './searchbar';
diff --git a/src/app/shared/navbar/searchbar/searchbar.html b/src/app/shared/navbar/searchbar/searchbar.html
new file mode 100644
index 000000000..20156607e
--- /dev/null
+++ b/src/app/shared/navbar/searchbar/searchbar.html
@@ -0,0 +1,17 @@
+
+
+ search
+
+
+
+
+ {{item.name}}
+
+
diff --git a/src/app/shared/navbar/searchbar/searchbar.scss b/src/app/shared/navbar/searchbar/searchbar.scss
new file mode 100644
index 000000000..3d0ff9407
--- /dev/null
+++ b/src/app/shared/navbar/searchbar/searchbar.scss
@@ -0,0 +1,60 @@
+$input-bg: #85D9E2;
+
+@mixin color-placeholder() {
+ -webkit-font-smoothing: antialiased;
+ color: white;
+}
+
+:host {
+ position: relative;
+ flex: 2;
+
+ * {
+ box-sizing: border-box;
+ }
+
+ &.docs-expanded .docs-search-input-container {
+ width: 100%;
+ }
+
+ .docs-search-input-container {
+ display: block;
+ position: relative;
+ margin-left: auto;
+ height: 100%;
+ width: 200px;
+ transition: width .2s ease;
+ i.material-icons {
+ position: absolute;
+ left: 15px; top: 50%;
+ transform: translateY(-50%);
+ height: 28px;
+ width: 28px;
+ }
+ }
+
+ input {
+ background: $input-bg;
+ border: none;
+ border-radius: 2px;
+ color: white;
+ font-size: 18px;
+ height: 95%;
+ line-height: 95%;
+ padding-left: 50px;
+ position: relative;
+ transition: width .2s ease;
+ width: 100%;
+
+ /* Set placeholder text to be white */
+ &::-webkit-input-placeholder { @include color-placeholder(); } /* Chrome/Opera/Safari */
+ &::-moz-placeholder { @include color-placeholder(); } /* Firefox 19+ */
+ &:-moz-placeholder { @include color-placeholder(); } /* Firefox 18- */
+ &:ms-input-placeholder { @include color-placeholder(); } /* IE 10+ */
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+}
diff --git a/src/app/shared/navbar/searchbar/searchbar.spec.ts b/src/app/shared/navbar/searchbar/searchbar.spec.ts
new file mode 100644
index 000000000..970bc2178
--- /dev/null
+++ b/src/app/shared/navbar/searchbar/searchbar.spec.ts
@@ -0,0 +1,99 @@
+import {Injectable} from '@angular/core';
+import {TestBed, inject, async, ComponentFixture} from '@angular/core/testing';
+import {Router, RouterModule} from '@angular/router';
+import {MaterialModule} from '@angular/material';
+import {ReactiveFormsModule, FormControl} from '@angular/forms';
+
+import {DocumentationItems, DocItem} from '../../documentation-items/documentation-items';
+import {SearchBar} './searchbar';
+
+const mockRouter = {
+ navigate: jasmine.createSpy('navigate'),
+ navigateByUrl: jasmine.createSpy('navigateByUrl');
+};
+
+const testDocItem = {
+ id: 'test-doc-item',
+ name: 'TestingExample',
+ examples: ['test-examples']
+};
+
+
+describe('SearchBar', () => {
+ let fixture: ComponentFixture;
+ let component: SearchBar;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [RouterModule, ReactiveFormsModule, MaterialModule],
+ declarations: [SearchBar],
+ providers: [
+ {provide: DocumentationItems, useClass: MockDocumentationItems},
+ {provide: Router, useValue: mockRouter},
+ ],
+ });
+
+ TestBed.compileComponents();
+ fixture = TestBed.createComponent(SearchBar);
+ component = fixture.componentInstance;
+ component.searchControl = new FormControl('');
+ fixture.detectChanges();
+ }));
+
+ afterEach(() => {
+ component._router.navigateByUrl.calls.reset();
+ });
+
+ it('should toggle isExpanded', () => {
+ expect(component._isExpanded).toBe(false);
+ component.toggleIsExpanded();
+ expect(component._isExpanded).toBe(true);
+ });
+
+ describe('Filter Search Suggestions', () => {
+ it('should return all items matching search query', () => {
+ const query = 'testing';
+ const result = component.filterSearchSuggestions(query);
+ expect(result).toEqual([testDocItem]);
+ });
+
+ it('should return empty list if no items match', () => {
+ const query = 'does not exist';
+ const result = component.filterSearchSuggestions(query);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('Navigate', () => {
+
+ it('should take an id and navigate to the given route', () => {
+ component.navigate('button-toggle');
+ expect(component._router.navigateByUrl).toHaveBeenCalled();
+ });
+
+ it('should not navigate if no id is given', () => {
+ component.navigate('');
+ expect(component._router.navigateByUrl).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should show a snackbar error', () => {
+ spyOn(component._snackBar, 'open');
+ component._showError();
+ expect(component._snackBar.open).toHaveBeenCalled();
+ expect(component._snackBar.open).toHaveBeenCalledWith(
+ 'No search results found.',
+ null, {duration: 3000});
+ });
+
+ it('should return the proper display value for form control', () => {
+ const result = component.displayFn(testDocItem);
+ expect(result).toEqual(testDocItem.name);
+ });
+});
+
+
+class MockDocumentationItems extends DocumentationItems {
+ getAllItems(): DocItem[] { return [testDocItem]; }
+}
+
diff --git a/src/app/shared/navbar/searchbar/searchbar.ts b/src/app/shared/navbar/searchbar/searchbar.ts
new file mode 100644
index 000000000..fb913fd88
--- /dev/null
+++ b/src/app/shared/navbar/searchbar/searchbar.ts
@@ -0,0 +1,80 @@
+import {Component, ViewChild} from '@angular/core';
+import {MdAutocompleteTrigger, MdSnackBar} from '@angular/material';
+import {Router} from '@angular/router';
+import {FormControl} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/operator/mergeMap';
+
+import {DocumentationItems, DocItem} from '../../documentation-items/documentation-items';
+
+
+@Component({
+ selector: 'search-bar-component',
+ templateUrl: './searchbar.html',
+ styleUrls: ['./searchbar.scss'],
+ host: {
+ '[class.docs-expanded]': '_isExpanded'
+ }
+})
+
+export class SearchBar {
+
+ @ViewChild(MdAutocompleteTrigger)
+ private _autocompleteTrigger: MdAutocompleteTrigger;
+
+ public allDocItems: DocItem[];
+ public filteredSuggestions: Observable;
+ public searchControl: FormControl = new FormControl('');
+ public sub;
+
+ private _isExpanded: boolean = false;
+
+ constructor(
+ private _docItems: DocumentationItems,
+ private _router: Router,
+ private _snackBar: MdSnackBar
+ ) {
+ this.allDocItems = _docItems.getAllItems();
+ this.filteredSuggestions = this.searchControl.valueChanges
+ .startWith(null)
+ .map(item => item ? this.filterSearchSuggestions(item) : this.allDocItems.slice());
+ }
+
+ // This handles the user interacting with the autocomplete panel clicks or keyboard.
+ public ngAfterViewInit() {
+ // We listen to the changes on `filteredSuggestions in order to
+ // listen to the latest _autocompleteTrigger.optionSelections
+ this.sub = this.filteredSuggestions
+ .flatMap(_ => Observable.merge(...this._autocompleteTrigger.optionSelections))
+ .subscribe(evt => this.navigate(evt.source.value.id));
+ }
+
+ public ngOnDestroy() {
+ if (this.sub) { this.sub.unsubscribe(); }
+ }
+
+ public toggleIsExpanded() {
+ this._isExpanded = !this._isExpanded;
+ }
+
+ public displayFn(item: DocItem) {
+ return item.name;
+ }
+
+ public filterSearchSuggestions(searchTerm): DocItem[] {
+ return this.allDocItems.filter(item => new RegExp(`^${searchTerm}`, 'gi').test(item.name));
+ }
+
+ public handlePlainSearch(searchTerm) {
+ const item = this.allDocItems.find(item => item.name.toLowerCase() === searchTerm);
+ item ? this.navigate(item.id) : this._showError();
+ }
+
+ public navigate(id) {
+ return id ? this._router.navigateByUrl(`/components/component/${id}`) : null;
+ }
+
+ private _showError() {
+ this._snackBar.open('No search results found.', null, {duration: 3000});
+ }
+}
diff --git a/src/app/shared/shared-module.ts b/src/app/shared/shared-module.ts
index 77eeec4fc..b9c299542 100644
--- a/src/app/shared/shared-module.ts
+++ b/src/app/shared/shared-module.ts
@@ -1,9 +1,11 @@
import {NgModule} from '@angular/core';
import {HttpModule} from '@angular/http';
+import {ReactiveFormsModule} from '@angular/forms';
import {DocViewer} from './doc-viewer/doc-viewer';
import {ExampleViewer} from './example-viewer/example-viewer';
import {DocumentationItems} from './documentation-items/documentation-items';
import {NavBar} from './navbar/navbar';
+import {SearchBar} from './navbar/searchbar/searchbar';
import {MaterialModule} from '@angular/material';
import {BrowserModule} from '@angular/platform-browser';
import {RouterModule} from '@angular/router';
@@ -16,9 +18,10 @@ import {GuideItems} from './guide-items/guide-items';
HttpModule,
RouterModule,
BrowserModule,
+ ReactiveFormsModule,
MaterialModule,
],
- declarations: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
+ declarations: [DocViewer, ExampleViewer, NavBar, SearchBar, PlunkerButton],
exports: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
providers: [DocumentationItems, GuideItems],
entryComponents: [