Skip to content

Commit bf80fa2

Browse files
feat(typeahead): add window for results display
Closes ng-bootstrap#432
1 parent b61ad8f commit bf80fa2

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed

.travis.yml

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ language: node_js
22
node_js:
33
- "5"
44

5+
addons:
6+
firefox: "latest"
7+
58
before_install:
69
- export DISPLAY=:99.0
710
- sh -e /etc/init.d/xvfb start
+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import {inject, async} from '@angular/core/testing';
2+
import {TestComponentBuilder} from '@angular/compiler/testing';
3+
import {By} from '@angular/platform-browser';
4+
5+
import {Component} from '@angular/core';
6+
7+
import {NgbTypeaheadWindow} from './typeahead-window';
8+
9+
function normalizeText(txt: string): string {
10+
return txt.trim().replace(/\s+/g, ' ');
11+
}
12+
13+
function expectResults(nativeEl: HTMLElement, resultsDef: string[]): void {
14+
const pages = nativeEl.querySelectorAll('a');
15+
16+
expect(pages.length).toEqual(resultsDef.length);
17+
18+
for (let i = 0; i < resultsDef.length; i++) {
19+
let resultDef = resultsDef[i];
20+
let classIndicator = resultDef.charAt(0);
21+
22+
expect(pages[i]).toHaveCssClass('dropdown-item');
23+
if (classIndicator === '+') {
24+
expect(pages[i]).toHaveCssClass('active');
25+
expect(normalizeText(pages[i].textContent)).toEqual(resultDef.substr(1));
26+
} else {
27+
expect(pages[i]).not.toHaveCssClass('active');
28+
expect(normalizeText(pages[i].textContent)).toEqual(resultDef);
29+
}
30+
}
31+
}
32+
33+
describe('ngb-typeahead-window', () => {
34+
35+
describe('display', () => {
36+
37+
it('should display results with the first row active', async(inject([TestComponentBuilder], (tcb) => {
38+
const html = '<ngb-typeahead-window [results]="results" [term]="term"></ngb-typeahead-window>';
39+
40+
tcb.overrideTemplate(TestComponent, html).createAsync(TestComponent).then((fixture) => {
41+
fixture.detectChanges();
42+
expectResults(fixture.nativeElement, ['+bar', 'baz']);
43+
});
44+
})));
45+
46+
it('should use a formatting function to display results', async(inject([TestComponentBuilder], (tcb) => {
47+
const html =
48+
'<ngb-typeahead-window [results]="results" [term]="term" [formatter]="formatterFn"></ngb-typeahead-window>';
49+
50+
tcb.overrideTemplate(TestComponent, html).createAsync(TestComponent).then((fixture) => {
51+
fixture.detectChanges();
52+
expectResults(fixture.nativeElement, ['+BAR', 'BAZ']);
53+
});
54+
})));
55+
});
56+
57+
describe('active row', () => {
58+
59+
it('should change active row on prev / next method call', async(inject([TestComponentBuilder], (tcb) => {
60+
const html = `
61+
<button (click)="w.next()">+</button>
62+
<button (click)="w.prev()">-</button>
63+
<ngb-typeahead-window [results]="results" [term]="term" #w="ngbTypeaheadWindow"></ngb-typeahead-window>`;
64+
65+
tcb.overrideTemplate(TestComponent, html).createAsync(TestComponent).then((fixture) => {
66+
fixture.detectChanges();
67+
const buttons = fixture.nativeElement.querySelectorAll('button');
68+
69+
expectResults(fixture.nativeElement, ['+bar', 'baz']);
70+
71+
buttons[0].click();
72+
fixture.detectChanges();
73+
expectResults(fixture.nativeElement, ['bar', '+baz']);
74+
75+
buttons[1].click();
76+
fixture.detectChanges();
77+
expectResults(fixture.nativeElement, ['+bar', 'baz']);
78+
});
79+
})));
80+
81+
it('should wrap active row on prev / next method call', async(inject([TestComponentBuilder], (tcb) => {
82+
const html = `
83+
<button (click)="w.next()">+</button>
84+
<button (click)="w.prev()">-</button>
85+
<ngb-typeahead-window [results]="results" [term]="term" #w="ngbTypeaheadWindow"></ngb-typeahead-window>`;
86+
87+
tcb.overrideTemplate(TestComponent, html).createAsync(TestComponent).then((fixture) => {
88+
fixture.detectChanges();
89+
const buttons = fixture.nativeElement.querySelectorAll('button');
90+
91+
expectResults(fixture.nativeElement, ['+bar', 'baz']);
92+
93+
buttons[1].click();
94+
fixture.detectChanges();
95+
expectResults(fixture.nativeElement, ['bar', '+baz']);
96+
97+
buttons[0].click();
98+
fixture.detectChanges();
99+
expectResults(fixture.nativeElement, ['+bar', 'baz']);
100+
});
101+
})));
102+
103+
it('should change active row on mouseenter', async(inject([TestComponentBuilder], (tcb) => {
104+
const html = `<ngb-typeahead-window [results]="results" [term]="term"></ngb-typeahead-window>`;
105+
106+
tcb.overrideTemplate(TestComponent, html).createAsync(TestComponent).then((fixture) => {
107+
fixture.detectChanges();
108+
const rowDebugEls = fixture.debugElement.queryAll(By.css('a'));
109+
110+
expectResults(fixture.nativeElement, ['+bar', 'baz']);
111+
112+
rowDebugEls[1].triggerEventHandler('mouseenter', {});
113+
fixture.detectChanges();
114+
expectResults(fixture.nativeElement, ['bar', '+baz']);
115+
});
116+
})));
117+
});
118+
119+
describe('result selection', () => {
120+
it('should select a given row on click', async(inject([TestComponentBuilder], (tcb) => {
121+
const html =
122+
'<ngb-typeahead-window [results]="results" [term]="term" (select)="selected = $event"></ngb-typeahead-window>';
123+
124+
tcb.overrideTemplate(TestComponent, html).createAsync(TestComponent).then((fixture) => {
125+
fixture.detectChanges();
126+
const rowDebugEls = fixture.debugElement.queryAll(By.css('a'));
127+
128+
expectResults(fixture.nativeElement, ['+bar', 'baz']);
129+
130+
rowDebugEls[1].triggerEventHandler('click', {});
131+
fixture.detectChanges();
132+
expect(fixture.componentInstance.selected).toBe('baz');
133+
});
134+
})));
135+
});
136+
137+
});
138+
139+
@Component({selector: 'test-cmp', directives: [NgbTypeaheadWindow], template: ''})
140+
class TestComponent {
141+
results = ['bar', 'baz'];
142+
term = 'ba';
143+
selected: string;
144+
145+
formatterFn = (result) => { return result.toUpperCase(); };
146+
}

src/typeahead/typeahead-window.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {Component, Input, Output, EventEmitter} from '@angular/core';
2+
import {NgbHighlight} from './highlight';
3+
4+
@Component({
5+
selector: 'ngb-typeahead-window',
6+
exportAs: 'ngbTypeaheadWindow',
7+
template: `
8+
<div class="dropdown-menu" aria-labelledby="dropdownMenu1">
9+
<template ngFor [ngForOf]="results" let-result let-idx="index">
10+
<a class="dropdown-item" [class.active]="idx === activeIdx"
11+
(mouseenter)="markActive(idx)"
12+
(click)="select(result)"><ngb-highlight [result]="formatter(result)" [term]="term"></ngb-highlight></a>
13+
</template>
14+
</div>
15+
`,
16+
directives: [NgbHighlight]
17+
})
18+
export class NgbTypeaheadWindow {
19+
private activeIdx = 0;
20+
21+
/**
22+
* Event raised when users selects a particular result row.
23+
*/
24+
@Output('select') selectEvent = new EventEmitter();
25+
26+
/**
27+
* Typeahead match results to be displayed
28+
*/
29+
@Input() results;
30+
31+
/**
32+
* Search term used to get current results
33+
*/
34+
@Input() term;
35+
36+
/**
37+
* A function used to format a given result before display. This function should return a formated string without any
38+
* HTML markup.
39+
*/
40+
@Input() formatter = (result) => { return `${result}`; };
41+
42+
43+
markActive(activeIdx: number) { this.activeIdx = activeIdx; }
44+
45+
next() { this.activeIdx = (this.activeIdx + 1) % this.results.length; }
46+
47+
prev() { this.activeIdx = (this.activeIdx === 0 ? this.results.length - 1 : this.activeIdx - 1); }
48+
49+
select(item) { this.selectEvent.emit(item); }
50+
}

0 commit comments

Comments
 (0)