Skip to content
This repository has been archived by the owner on Oct 1, 2022. It is now read-only.

Commit

Permalink
feat(typeahead): allowing search on focus and click
Browse files Browse the repository at this point in the history
  • Loading branch information
ymeine committed Nov 24, 2017
1 parent 7eec92b commit dcb463e
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 4 deletions.
23 changes: 23 additions & 0 deletions demo/src/app/components/typeahead/demos/focus/typeahead-focus.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
It is possible to get the focus events with the current input value to emit results on focus with a great flexibility.

In this simple example, a search is done no matter the content of the input:

<ul>
<li>on empty input all options will be taken</li>
<li>otherwise options will be filtered against the search term</li>
<li>it will limit the display to 10 results in all cases</li>
</ul>

<label for="typeahead-focus">Search for a state:</label>
<input
id="typeahead-focus"
type="text"
class="form-control"
[(ngModel)]="model"
[ngbTypeahead]="search"
(focus)="focus$.next($event.target.value)"
(click)="click$.next($event.target.value)"
#instance="ngbTypeahead"
/>
<hr>
<pre>Model: {{ model | json }}</pre>
38 changes: 38 additions & 0 deletions demo/src/app/components/typeahead/demos/focus/typeahead-focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {Component, ViewChild} from '@angular/core';
import {NgbTypeahead} from '@ng-bootstrap/ng-bootstrap';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'District Of Columbia', 'Federated States Of Micronesia', 'Florida', 'Georgia',
'Guam', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine',
'Marshall Islands', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota',
'Northern Mariana Islands', 'Ohio', 'Oklahoma', 'Oregon', 'Palau', 'Pennsylvania', 'Puerto Rico', 'Rhode Island',
'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virgin Islands', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'];

@Component({
selector: 'ngbd-typeahead-focus',
templateUrl: './typeahead-focus.html',
styles: [`.form-control { width: 300px; }`]
})
export class NgbdTypeaheadFocus {
public model: any;

@ViewChild('instance') instance: NgbTypeahead;
public focus$ = new Subject<string>();
public click$ = new Subject<string>();

search = (text$: Observable<string>) =>
text$
.debounceTime(200).distinctUntilChanged()
.merge(this.focus$)
.merge(this.click$.filter(() => !this.instance.isPopupOpen()))
.map(term => (term === '' ? states : states.filter(v => v.toLowerCase().indexOf(term.toLowerCase()) > -1)).slice(0, 10));
}
7 changes: 6 additions & 1 deletion demo/src/app/components/typeahead/demos/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {NgbdTypeaheadFormat} from './format/typeahead-format';
import {NgbdTypeaheadHttp} from './http/typeahead-http';
import {NgbdTypeaheadBasic} from './basic/typeahead-basic';
import {NgbdTypeaheadFocus} from './focus/typeahead-focus';
import {NgbdTypeaheadTemplate} from './template/typeahead-template';
import {NgbdTypeaheadConfig} from './config/typeahead-config';

export const DEMO_DIRECTIVES =
[NgbdTypeaheadFormat, NgbdTypeaheadHttp, NgbdTypeaheadBasic, NgbdTypeaheadTemplate, NgbdTypeaheadConfig];
[NgbdTypeaheadFormat, NgbdTypeaheadHttp, NgbdTypeaheadBasic, NgbdTypeaheadFocus, NgbdTypeaheadTemplate, NgbdTypeaheadConfig];

export const DEMO_SNIPPETS = {
'basic': {
'code': require('!!prismjs-loader?lang=typescript!./basic/typeahead-basic'),
'markup': require('!!prismjs-loader?lang=markup!./basic/typeahead-basic.html')
},
'focus': {
'code': require('!!prismjs-loader?lang=typescript!./focus/typeahead-focus'),
'markup': require('!!prismjs-loader?lang=markup!./focus/typeahead-focus.html')
},
'format': {
'code': require('!!prismjs-loader?lang=typescript!./format/typeahead-format'),
'markup': require('!!prismjs-loader?lang=markup!./format/typeahead-format.html')
Expand Down
3 changes: 3 additions & 0 deletions demo/src/app/components/typeahead/typeahead.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {DEMO_SNIPPETS} from './demos';
<ngbd-example-box demoTitle="Simple Typeahead" [snippets]="snippets" component="typeahead" demo="basic">
<ngbd-typeahead-basic></ngbd-typeahead-basic>
</ngbd-example-box>
<ngbd-example-box demoTitle="Open on focus" [snippets]="snippets" component="typeahead" demo="focus">
<ngbd-typeahead-focus></ngbd-typeahead-focus>
</ngbd-example-box>
<ngbd-example-box demoTitle="Formatted results" [snippets]="snippets" component="typeahead" demo="format">
<ngbd-typeahead-format></ngbd-typeahead-format>
</ngbd-example-box>
Expand Down
101 changes: 100 additions & 1 deletion src/typeahead/typeahead.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {Component, DebugElement, ViewChild, ChangeDetectionStrategy} from '@angu
import {Validators, FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/merge';
import 'rxjs/add/operator/filter';

import {NgbTypeahead} from './typeahead';
import {NgbTypeaheadModule} from './typeahead.module';
Expand Down Expand Up @@ -194,6 +197,93 @@ describe('ngb-typeahead', () => {
expect(getWindow(compiled)).not.toBeNull();
});

it('should open on focus or click', async(async() => {
const fixture = createTestComponent(`<input
type="text"
[ngbTypeahead]="find"
(focus)="focus$.next($event.target.value)"
(click)="click$.next($event.target.value)"
/>`);
const compiled = fixture.nativeElement;

let searchCount = 0;
fixture.componentInstance.findOutput$.subscribe(() => searchCount++);
const checkSearchCount = (expected, context) => {
expect(searchCount).toBe(expected, `Search count is not correct: ${context}`);
searchCount = 0;
};

const checkWindowIsClosed = () => {
expect(getWindow(compiled)).toBeNull();
expect(fixture.componentInstance.model).toBe(undefined);
expect(getNativeInput(compiled).value).toBe('');
};

const checkWindowIsOpen = () => { expect(getWindow(compiled)).not.toBeNull(); };

// focusing the input triggers a search and opens the dropdown
getNativeInput(compiled).focus();
// on IE the focus can be asynchronous, so we need to wait a tick before continuing
await new Promise(resolve => setTimeout(resolve, 0));
checkSearchCount(1, 'on first focus');
checkWindowIsOpen();

// clicking again in the input while the dropdown is open doesn't trigger a new search and keeps the dropdown
// open
getNativeInput(compiled).click();
checkSearchCount(0, 'on input click when dropdown already open');
checkWindowIsOpen();

// closing the dropdown but keeping focus
const event = createKeyDownEvent(Key.Escape);
getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event);
fixture.detectChanges();
checkWindowIsClosed();

// clicking again in the input while already focused but dropdown closed triggers a search and opens the
// dropdown
getNativeInput(compiled).click();
checkSearchCount(1, 'on input click when input is already focused but dropdown is closed');
checkWindowIsOpen();

// closing the dropdown and losing focus
fixture.nativeElement.click();
getNativeInput(compiled).blur();
checkWindowIsClosed();

// Clicking directly, putting focus at the same time, triggers only one search and opens the dropdown
getNativeInput(compiled).click();
checkSearchCount(1, 'on input focus specifically with a click');
checkWindowIsOpen();
}));

it('should preserve value previously selected with mouse when reopening with focus then closing without selection',
() => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="find"/>`);
const compiled = fixture.nativeElement;

fixture.whenStable().then(() => {
// open with partial input
changeInput(compiled, 'o');
fixture.detectChanges();

// select with click
getWindowLinks(fixture.debugElement)[0].triggerEventHandler('click', {});
fixture.detectChanges();
expectInputValue(compiled, 'one');

// open again but with focus
getNativeInput(compiled).blur();
getNativeInput(compiled).focus();

// close without selecting a new value
const event = createKeyDownEvent(Key.Escape);
getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event);
fixture.detectChanges();
expectInputValue(compiled, 'one');
});
});

it('should be closed when ESC is pressed', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="find"/>`);
const compiled = fixture.nativeElement;
Expand Down Expand Up @@ -921,9 +1011,18 @@ class TestComponent {

form = new FormGroup({control: new FormControl('', Validators.required)});

findOutput$: Observable<any[]>;

@ViewChild(NgbTypeahead) typeahead: NgbTypeahead;
public focus$ = new Subject<string>();
public click$ = new Subject<string>();

find = (text$: Observable<string>) => { return text$.map(text => this._strings.filter(v => v.startsWith(text))); };
find = (text$: Observable<string>) => {
this.findOutput$ = text$.merge(this.focus$)
.merge(this.click$.filter(() => !this.typeahead.isPopupOpen()))
.map(text => this._strings.filter(v => v.startsWith(text)));
return this.findOutput$;
};

findAnywhere =
(text$: Observable<string>) => { return text$.map(text => this._strings.filter(v => v.indexOf(text) > -1)); };
Expand Down
5 changes: 3 additions & 2 deletions src/typeahead/typeahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export class NgbTypeahead implements ControlValueAccessor,

if (!defaultPrevented) {
this.writeValue(result);
this._userInput = result;
this._onChange(result);
}
}
Expand All @@ -312,7 +313,7 @@ export class NgbTypeahead implements ControlValueAccessor,
}

private _showHint() {
if (this.showHint) {
if (this.showHint && this._userInput != null) {
const userInputLowerCase = this._userInput.toLowerCase();
const formattedVal = this._formatItemForInput(this._windowRef.instance.getActive());

Expand All @@ -331,7 +332,7 @@ export class NgbTypeahead implements ControlValueAccessor,
}

private _writeInputValue(value: string): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'value', value);
this._renderer.setProperty(this._elementRef.nativeElement, 'value', toString(value));
}

private _subscribeToUserInput(userInput$: Observable<any[]>): Subscription {
Expand Down

0 comments on commit dcb463e

Please sign in to comment.