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

Commit fe77301

Browse files
committed
feat(navbar): Add basic searchbar component to site.
1 parent e704d54 commit fe77301

File tree

9 files changed

+263
-7
lines changed

9 files changed

+263
-7
lines changed

src/app/shared/navbar/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './navbar';
2+
export * from './searchbar';

src/app/shared/navbar/navbar.html

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</a>
99
<a md-button class="docs-button" routerLink="components">Components</a>
1010
<a md-button class="docs-button" routerLink="guides">Guides</a>
11+
<search-bar-component></search-bar-component>
1112
<a md-button class="docs-button" href="https://github.com/angular/material2" aria-label="GitHub Repository">
1213
<img class="docs-github-logo"
1314
src="../../../assets/img/homepage/github-circle-white-transparent.svg"

src/app/shared/navbar/navbar.scss

-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22
display: flex;
33
flex-wrap: wrap;
44
padding: 8px 16px;
5-
6-
> .mat-button {
7-
&:last-child {
8-
margin-left: auto;
9-
}
10-
}
115
}
126

137
.docs-angular-logo {
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './searchbar';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="docs-search-input-container">
2+
<input
3+
placeholder="Search"
4+
type="text"
5+
(focus)="toggleIsExpanded(true)"
6+
(blur)="toggleIsExpanded(false)"
7+
(keyup.enter)="handlePlainSearch($event.target.value.toLowerCase())"
8+
[mdAutocomplete]="auto"
9+
[formControl]="searchControl">
10+
<i class="material-icons">search</i>
11+
</div>
12+
13+
<md-autocomplete #auto="mdAutocomplete" [displayWith]="displayFn">
14+
<md-option *ngFor="let item of filteredSuggestions | async" [value]="item">
15+
{{item.name}}
16+
</md-option>
17+
</md-autocomplete>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
$input-bg: #85D9E2;
2+
3+
@mixin color-placeholder() {
4+
-webkit-font-smoothing: antialiased;
5+
color: white;
6+
}
7+
8+
:host {
9+
position: relative;
10+
flex: 2;
11+
12+
* {
13+
box-sizing: border-box;
14+
}
15+
16+
&.docs-expanded .docs-search-input-container {
17+
width: 100%;
18+
}
19+
20+
.docs-search-input-container {
21+
display: block;
22+
position: relative;
23+
margin-left: auto;
24+
height: 100%;
25+
width: 200px;
26+
transition: width .2s ease;
27+
i.material-icons {
28+
position: absolute;
29+
left: 15px; top: 50%;
30+
transform: translateY(-50%);
31+
height: 28px;
32+
width: 28px;
33+
}
34+
}
35+
36+
input {
37+
background: $input-bg;
38+
border: none;
39+
border-radius: 2px;
40+
color: white;
41+
font-size: 18px;
42+
height: 95%;
43+
line-height: 95%;
44+
padding-left: 50px;
45+
position: relative;
46+
transition: width .2s ease;
47+
width: 100%;
48+
49+
/* Set placeholder text to be white */
50+
&::-webkit-input-placeholder { @include color-placeholder(); } /* Chrome/Opera/Safari */
51+
&::-moz-placeholder { @include color-placeholder(); } /* Firefox 19+ */
52+
&:-moz-placeholder { @include color-placeholder(); } /* Firefox 18- */
53+
&:ms-input-placeholder { @include color-placeholder(); } /* IE 10+ */
54+
55+
&:focus {
56+
outline: none;
57+
}
58+
}
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {Injectable} from '@angular/core';
2+
import {TestBed, inject, async, ComponentFixture} from '@angular/core/testing';
3+
import {Router, RouterModule} from '@angular/router';
4+
import {MaterialModule} from '@angular/material';
5+
import {ReactiveFormsModule, FormControl} from '@angular/forms';
6+
7+
import {DocumentationItems, DocItem} from '../../documentation-items/documentation-items';
8+
import {SearchBar} './searchbar';
9+
10+
const mockRouter = {
11+
navigate: jasmine.createSpy('navigate'),
12+
navigateByUrl: jasmine.createSpy('navigateByUrl');
13+
};
14+
15+
const testDocItem = {
16+
id: 'test-doc-item',
17+
name: 'TestingExample',
18+
examples: ['test-examples']
19+
};
20+
21+
22+
describe('SearchBar', () => {
23+
let fixture: ComponentFixture<SearchBar>;
24+
let component: SearchBar;
25+
26+
beforeEach(async(() => {
27+
TestBed.configureTestingModule({
28+
imports: [RouterModule, ReactiveFormsModule, MaterialModule],
29+
declarations: [SearchBar],
30+
providers: [
31+
{provide: DocumentationItems, useClass: MockDocumentationItems},
32+
{provide: Router, useValue: mockRouter},
33+
],
34+
});
35+
36+
TestBed.compileComponents();
37+
fixture = TestBed.createComponent(SearchBar);
38+
component = fixture.componentInstance;
39+
component.searchControl = new FormControl('');
40+
fixture.detectChanges();
41+
}));
42+
43+
afterEach(() => {
44+
component._router.navigateByUrl.calls.reset();
45+
});
46+
47+
it('should toggle isExpanded', () => {
48+
expect(component._isExpanded).toBe(false);
49+
component.toggleIsExpanded();
50+
expect(component._isExpanded).toBe(true);
51+
});
52+
53+
describe('Filter Search Suggestions', () => {
54+
it('should return all items matching search query', () => {
55+
const query = 'testing';
56+
const result = component.filterSearchSuggestions(query);
57+
expect(result).toEqual([testDocItem]);
58+
});
59+
60+
it('should return empty list if no items match', () => {
61+
const query = 'does not exist';
62+
const result = component.filterSearchSuggestions(query);
63+
expect(result).toEqual([]);
64+
});
65+
});
66+
67+
describe('Navigate', () => {
68+
69+
it('should take an id and navigate to the given route', () => {
70+
component.navigate('button-toggle');
71+
expect(component._router.navigateByUrl).toHaveBeenCalled();
72+
});
73+
74+
it('should not navigate if no id is given', () => {
75+
component.navigate('');
76+
expect(component._router.navigateByUrl).not.toHaveBeenCalled();
77+
});
78+
});
79+
80+
it('should show a snackbar error', () => {
81+
spyOn(component._snackBar, 'open');
82+
component._showError();
83+
expect(component._snackBar.open).toHaveBeenCalled();
84+
expect(component._snackBar.open).toHaveBeenCalledWith(
85+
'No search results found.',
86+
null, {duration: 3000});
87+
});
88+
89+
it('should return the proper display value for form control', () => {
90+
const result = component.displayFn(testDocItem);
91+
expect(result).toEqual(testDocItem.name);
92+
});
93+
});
94+
95+
96+
class MockDocumentationItems extends DocumentationItems {
97+
getAllItems(): DocItem[] { return [testDocItem]; }
98+
}
99+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {Component, ViewChild} from '@angular/core';
2+
import {MdAutocompleteTrigger, MdSnackBar} from '@angular/material';
3+
import {Router} from '@angular/router';
4+
import {FormControl} from '@angular/forms';
5+
import {Observable} from 'rxjs/Observable';
6+
import 'rxjs/add/operator/mergeMap';
7+
8+
import {DocumentationItems, DocItem} from '../../documentation-items/documentation-items';
9+
10+
11+
@Component({
12+
selector: 'search-bar-component',
13+
templateUrl: './searchbar.html',
14+
styleUrls: ['./searchbar.scss'],
15+
host: {
16+
'[class.docs-expanded]': '_isExpanded'
17+
}
18+
})
19+
20+
export class SearchBar {
21+
22+
@ViewChild(MdAutocompleteTrigger)
23+
private _autocompleteTrigger: MdAutocompleteTrigger;
24+
25+
public allDocItems: DocItem[];
26+
public filteredSuggestions: Observable<DocItem[]>;
27+
public searchControl: FormControl = new FormControl('');
28+
public sub;
29+
30+
private _isExpanded: boolean = false;
31+
32+
constructor(
33+
private _docItems: DocumentationItems,
34+
private _router: Router,
35+
private _snackBar: MdSnackBar
36+
) {
37+
this.allDocItems = _docItems.getAllItems();
38+
this.filteredSuggestions = this.searchControl.valueChanges
39+
.startWith(null)
40+
.map(item => item ? this.filterSearchSuggestions(item) : this.allDocItems.slice());
41+
}
42+
43+
// This handles the user interacting with the autocomplete panel clicks or keyboard.
44+
public ngAfterViewInit() {
45+
// We listen to the changes on `filteredSuggestions in order to
46+
// listen to the latest _autocompleteTrigger.optionSelections
47+
this.sub = this.filteredSuggestions
48+
.flatMap(_ => Observable.merge(...this._autocompleteTrigger.optionSelections))
49+
.subscribe(evt => this.navigate(evt.source.value.id));
50+
}
51+
52+
public ngOnDestroy() {
53+
if (this.sub) { this.sub.unsubscribe(); }
54+
}
55+
56+
public toggleIsExpanded() {
57+
this._isExpanded = !this._isExpanded;
58+
}
59+
60+
public displayFn(item: DocItem) {
61+
return item.name;
62+
}
63+
64+
public filterSearchSuggestions(searchTerm): DocItem[] {
65+
return this.allDocItems.filter(item => new RegExp(`^${searchTerm}`, 'gi').test(item.name));
66+
}
67+
68+
public handlePlainSearch(searchTerm) {
69+
const item = this.allDocItems.find(item => item.name.toLowerCase() === searchTerm);
70+
item ? this.navigate(item.id) : this._showError();
71+
}
72+
73+
public navigate(id) {
74+
return id ? this._router.navigateByUrl(`/components/component/${id}`) : null;
75+
}
76+
77+
private _showError() {
78+
this._snackBar.open('No search results found.', null, {duration: 3000});
79+
}
80+
}

src/app/shared/shared-module.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {NgModule} from '@angular/core';
22
import {HttpModule} from '@angular/http';
3+
import {ReactiveFormsModule} from '@angular/forms';
34
import {DocViewer} from './doc-viewer/doc-viewer';
45
import {ExampleViewer} from './example-viewer/example-viewer';
56
import {DocumentationItems} from './documentation-items/documentation-items';
67
import {NavBar} from './navbar/navbar';
8+
import {SearchBar} from './navbar/searchbar/searchbar';
79
import {MaterialModule} from '@angular/material';
810
import {BrowserModule} from '@angular/platform-browser';
911
import {RouterModule} from '@angular/router';
@@ -16,9 +18,10 @@ import {GuideItems} from './guide-items/guide-items';
1618
HttpModule,
1719
RouterModule,
1820
BrowserModule,
21+
ReactiveFormsModule,
1922
MaterialModule,
2023
],
21-
declarations: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
24+
declarations: [DocViewer, ExampleViewer, NavBar, SearchBar, PlunkerButton],
2225
exports: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
2326
providers: [DocumentationItems, GuideItems],
2427
entryComponents: [

0 commit comments

Comments
 (0)