diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html
index 52ce34ba0f..7aef453b5e 100644
--- a/aio/src/app/app.component.html
+++ b/aio/src/app/app.component.html
@@ -11,7 +11,7 @@
-
+
diff --git a/aio/src/app/search/search-box/search-box.component.spec.ts b/aio/src/app/search/search-box/search-box.component.spec.ts
index c8c9618544..075b0278a1 100644
--- a/aio/src/app/search/search-box/search-box.component.spec.ts
+++ b/aio/src/app/search/search-box/search-box.component.spec.ts
@@ -7,10 +7,10 @@ import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';
@Component({
- template: ''
+ template: ''
})
class HostComponent {
- doSearch = jasmine.createSpy('doSearch');
+ searchHandler = jasmine.createSpy('searchHandler');
}
describe('SearchBoxComponent', () => {
@@ -39,40 +39,61 @@ describe('SearchBoxComponent', () => {
location.search.and.returnValue({ search: 'initial search' });
component.ngOnInit();
expect(location.search).toHaveBeenCalled();
- expect(host.doSearch).toHaveBeenCalledWith('initial search');
+ expect(host.searchHandler).toHaveBeenCalledWith('initial search');
expect(component.searchBox.nativeElement.value).toEqual('initial search');
}));
});
describe('on input', () => {
- it('should trigger the search event', () => {
+ it('should trigger the onSearch event', () => {
const input = fixture.debugElement.query(By.css('input'));
- input.triggerEventHandler('input', { target: { value: 'some query' } });
- expect(host.doSearch).toHaveBeenCalledWith('some query');
+ input.nativeElement.value = 'some query (input)';
+ input.triggerEventHandler('input', { });
+ expect(host.searchHandler).toHaveBeenCalledWith('some query (input)');
});
});
describe('on keyup', () => {
- it('should trigger the search event', () => {
+ it('should trigger the onSearch event', () => {
const input = fixture.debugElement.query(By.css('input'));
- input.triggerEventHandler('keyup', { target: { value: 'some query' } });
- expect(host.doSearch).toHaveBeenCalledWith('some query');
+ input.nativeElement.value = 'some query (keyup)';
+ input.triggerEventHandler('keyup', { });
+ expect(host.searchHandler).toHaveBeenCalledWith('some query (keyup)');
});
});
describe('on focus', () => {
- it('should trigger the search event', () => {
+ it('should trigger the onSearch event', () => {
const input = fixture.debugElement.query(By.css('input'));
- input.triggerEventHandler('focus', { target: { value: 'some query' } });
- expect(host.doSearch).toHaveBeenCalledWith('some query');
+ input.nativeElement.value = 'some query (focus)';
+ input.triggerEventHandler('focus', { });
+ expect(host.searchHandler).toHaveBeenCalledWith('some query (focus)');
});
});
describe('on click', () => {
it('should trigger the search event', () => {
const input = fixture.debugElement.query(By.css('input'));
- input.triggerEventHandler('click', { target: { value: 'some query'}});
- expect(host.doSearch).toHaveBeenCalledWith('some query');
+ input.nativeElement.value = 'some query (click)';
+ input.triggerEventHandler('click', { });
+ expect(host.searchHandler).toHaveBeenCalledWith('some query (click)');
+ });
+ });
+
+ describe('event filtering', () => {
+ it('should only send events if the search value has changed', () => {
+ const input = fixture.debugElement.query(By.css('input'));
+
+ input.nativeElement.value = 'some query';
+ input.triggerEventHandler('input', { });
+ expect(host.searchHandler).toHaveBeenCalledTimes(1);
+
+ input.triggerEventHandler('input', { });
+ expect(host.searchHandler).toHaveBeenCalledTimes(1);
+
+ input.nativeElement.value = 'some other query';
+ input.triggerEventHandler('input', { });
+ expect(host.searchHandler).toHaveBeenCalledTimes(2);
});
});
diff --git a/aio/src/app/search/search-box/search-box.component.ts b/aio/src/app/search/search-box/search-box.component.ts
index 5073d41906..3603ebf56a 100644
--- a/aio/src/app/search/search-box/search-box.component.ts
+++ b/aio/src/app/search/search-box/search-box.component.ts
@@ -1,5 +1,8 @@
import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output } from '@angular/core';
import { LocationService } from 'app/shared/location.service';
+import { Subject } from 'rxjs/Subject';
+import 'rxjs/add/operator/filter';
+import 'rxjs/add/operator/distinctUntilChanged';
/**
* This component provides a text box to type a search query that will be sent to the SearchService.
@@ -17,15 +20,19 @@ import { LocationService } from 'app/shared/location.service';
type="search"
aria-label="search"
placeholder="Search"
- (input)="onSearch($event.target.value)"
- (keyup)="onSearch($event.target.value)"
- (focus)="onSearch($event.target.value)"
- (click)="onSearch($event.target.value)">`
+ (input)="doSearch()"
+ (keyup)="doSearch()"
+ (focus)="doSearch()"
+ (click)="doSearch()">`
})
export class SearchBoxComponent implements OnInit {
+ private searchSubject = new Subject();
+
@ViewChild('searchBox') searchBox: ElementRef;
- @Output() search = new EventEmitter();
+ @Output() onSearch = this.searchSubject
+ .filter(value => !!(value && value.trim()))
+ .distinctUntilChanged();
constructor(private locationService: LocationService) { }
@@ -36,12 +43,12 @@ export class SearchBoxComponent implements OnInit {
const query = this.locationService.search()['search'];
if (query) {
this.searchBox.nativeElement.value = query;
- this.onSearch(query);
+ this.doSearch();
}
}
- onSearch(query: string) {
- this.search.emit(query);
+ doSearch() {
+ this.searchSubject.next(this.searchBox.nativeElement.value);
}
focus() {
diff --git a/aio/src/app/search/search-worker.js b/aio/src/app/search/search-worker.js
index ae00a13e07..ed4ea4c08e 100644
--- a/aio/src/app/search/search-worker.js
+++ b/aio/src/app/search/search-worker.js
@@ -6,21 +6,22 @@
var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
// NOTE: This needs to be kept in sync with `ngsw-manifest.json`.
-importScripts('https://unpkg.com/lunr@0.7.2/lunr.min.js');
+importScripts('https://unpkg.com/lunr@2.1.0/lunr.js');
-var index = createIndex();
+var index;
var pages = {};
self.onmessage = handleMessage;
// Create the lunr index - the docs should be an array of objects, each object containing
// the path and search terms for a page
-function createIndex() {
+function createIndex(addFn) {
return lunr(/** @this */function() {
this.ref('path');
this.field('titleWords', {boost: 50});
this.field('members', {boost: 40});
this.field('keywords', {boost: 20});
+ addFn(this);
});
}
@@ -32,7 +33,7 @@ function handleMessage(message) {
switch(type) {
case 'load-index':
makeRequest(SEARCH_TERMS_URL, function(searchInfo) {
- loadIndex(searchInfo);
+ index = createIndex(loadIndex(searchInfo));
self.postMessage({type: type, id: id, payload: true});
});
break;
@@ -67,16 +68,29 @@ function makeRequest(url, callback) {
// Create the search index from the searchInfo which contains the information about each page to be indexed
function loadIndex(searchInfo) {
- // Store the pages data to be used in mapping query results back to pages
- // Add search terms from each page to the search index
- searchInfo.forEach(function(page) {
- index.add(page);
- pages[page.path] = page;
- });
+ return function(index) {
+ // Store the pages data to be used in mapping query results back to pages
+ // Add search terms from each page to the search index
+ searchInfo.forEach(function(page) {
+ index.add(page);
+ pages[page.path] = page;
+ });
+ };
}
// Query the index and return the processed results
function queryIndex(query) {
+ // The index requires the query to be lowercase
+ var terms = query.toLowerCase().split(/\s+/);
+ var results = index.query(function(qb) {
+ terms.forEach(function(term) {
+ // Only include terms that are longer than 2 characters, if there is more than one term
+ // Add trailing wildcard to each term so that it will match more results
+ if (terms.length === 1 || term.trim().length > 2) {
+ qb.term(term, { wildcard: lunr.Query.wildcard.TRAILING });
+ }
+ });
+ });
// Only return the array of paths to pages
- return index.search(query).map(function(hit) { return pages[hit.ref]; });
+ return results.map(function(hit) { return pages[hit.ref]; });
}