Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Commit

Permalink
feat(navbar): Add basic searchbar component to site.
Browse files Browse the repository at this point in the history
  • Loading branch information
riavalon committed Jun 9, 2017
1 parent 14c0587 commit 0c6c522
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 8 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/navbar/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './navbar';
export * from './searchbar';
1 change: 1 addition & 0 deletions src/app/shared/navbar/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</a>
<a md-button class="docs-button" routerLink="components">Components</a>
<a md-button class="docs-button" routerLink="guides">Guides</a>
<search-bar-component></search-bar-component>
<a md-button class="docs-button" href="https://github.com/angular/material2" aria-label="GitHub Repository">
<img class="docs-github-logo"
src="../../../assets/img/homepage/github-circle-white-transparent.svg"
Expand Down
6 changes: 0 additions & 6 deletions src/app/shared/navbar/navbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
display: flex;
flex-wrap: wrap;
padding: 8px 16px;

> .mat-button {
&:last-child {
margin-left: auto;
}
}
}

.docs-angular-logo {
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/navbar/searchbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './searchbar';
18 changes: 18 additions & 0 deletions src/app/shared/navbar/searchbar/searchbar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="docs-search-input-container">
<input
placeholder="Search"
type="text"
class="docs-search-input"
(focus)="toggleIsExpanded()"
(blur)="toggleIsExpanded()"
(keyup.enter)="handlePlainSearch($event.target.value.toLowerCase())"
[mdAutocomplete]="auto"
[formControl]="searchControl">
<md-icon class="docs-search-icon">search</md-icon>
</div>

<md-autocomplete #auto="mdAutocomplete" [displayWith]="displayFn">
<md-option *ngFor="let item of filteredSuggestions | async" [value]="item">
{{item.name}}
</md-option>
</md-autocomplete>
59 changes: 59 additions & 0 deletions src/app/shared/navbar/searchbar/searchbar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@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;

.docs-search-icon {
position: absolute;
left: 15px; top: 50%;
transform: translateY(-50%);
height: 28px;
width: 28px;
}
}

.docs-search-input {
background: rgba(255, 255, 255, 0.4);
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;
}
}

}
99 changes: 99 additions & 0 deletions src/app/shared/navbar/searchbar/searchbar.spec.ts
Original file line number Diff line number Diff line change
@@ -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} from './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<SearchBar>;
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(() => {
(<any>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]; }
}

107 changes: 107 additions & 0 deletions src/app/shared/navbar/searchbar/searchbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 {Subscription} from 'rxjs/Subscription';
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;

allDocItems: DocItem[];
filteredSuggestions: Observable<DocItem[]>;
searchControl: FormControl = new FormControl('');
subscription: Subscription;

_isExpanded: boolean = false;

constructor(
public _docItems: DocumentationItems,
public _router: Router,
public _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.
ngAfterViewInit() {
// We listen to the changes on `filteredSuggestions in order to
// listen to the latest _autocompleteTrigger.optionSelections
this.subscription = this.filteredSuggestions
.flatMap(_ => this._autocompleteTrigger.optionSelections)
.subscribe(evt => this._navigate(evt.source.value.id));
}

ngOnDestroy() {
if (this.subscription) { this.subscription.unsubscribe(); }
}

toggleIsExpanded() {
if (this._autocompleteTrigger) {
this._delayDropdown(this._isExpanded);
}
this._isExpanded = !this._isExpanded;
}

displayFn(item: DocItem) {
return item.name;
}

filterSearchSuggestions(searchTerm): DocItem[] {
return this.allDocItems.filter(item => new RegExp(`^${searchTerm}`, 'gi').test(item.name));
}

handlePlainSearch(searchTerm) {
const item = this.allDocItems.find(item => item.name.toLowerCase() === searchTerm);
return item ?
this._navigate(item.id) :
this.navigateToClosestMatch(searchTerm);
}

navigateToClosestMatch(term) {
this._resetSearch();
this._navigate(this.filterSearchSuggestions(term)[0].id);
}

_navigate(id) {
this._resetSearch();
return id ? this._router.navigateByUrl(`/components/component/${id}`) : null;
}

_resetSearch() {
this.searchControl.reset();
this.searchControl.setValue('');
}

_showError() {
this._snackBar.open('No search results found.', null, {duration: 3000});
}

_delayDropdown(isExpanded: boolean) {
if (isExpanded) {
this._autocompleteTrigger.closePanel();
} else {
this._autocompleteTrigger.closePanel();
setTimeout(() => this._autocompleteTrigger.openPanel(), 210);
}
}
}
5 changes: 4 additions & 1 deletion src/app/shared/shared-module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: [
Expand Down

0 comments on commit 0c6c522

Please sign in to comment.