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, closes ng-bootstrap#698 cl…
Browse files Browse the repository at this point in the history
  • Loading branch information
ymeine committed Oct 8, 2017
1 parent 6c7a31b commit 25b2375
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 8 deletions.
14 changes: 14 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,14 @@
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"/>
<hr>
<pre>Model: {{ model | json }}</pre>
31 changes: 31 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,31 @@
import {Component} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
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;

search = (text$: Observable<string>, focus$: Observable<string>) =>
text$
.debounceTime(200)
.distinctUntilChanged()
.merge(focus$)
.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
53 changes: 52 additions & 1 deletion src/typeahead/typeahead.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {By} from '@angular/platform-browser';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/merge';

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

it('should not be closed on input click', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="find"/>`);
const compiled = fixture.nativeElement;

changeInput(compiled, 'one');
fixture.detectChanges();
expect(getWindow(compiled)).not.toBeNull();

getNativeInput(compiled).click();
expect(getWindow(compiled)).not.toBeNull();
});

it('should open on focus', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="find"/>`);
const compiled = fixture.nativeElement;

// getNativeInput(compiled).blur();
getNativeInput(compiled).focus();
expect(getWindow(compiled)).not.toBeNull();
});

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 @@ -911,7 +960,9 @@ class TestComponent {

@ViewChild(NgbTypeahead) typeahead: NgbTypeahead;

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

findAnywhere =
(text$: Observable<string>) => { return text$.map(text => this._strings.filter(v => v.indexOf(text) > -1)); };
Expand Down
40 changes: 34 additions & 6 deletions src/typeahead/typeahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Observable} from 'rxjs/Observable';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {Subscription} from 'rxjs/Subscription';
import {letProto} from 'rxjs/operator/let';
import {_do} from 'rxjs/operator/do';
import {switchMap} from 'rxjs/operator/switchMap';
import {fromEvent} from 'rxjs/observable/fromEvent';
Expand Down Expand Up @@ -68,7 +67,7 @@ let nextWindowId = 0;
host: {
'(blur)': 'handleBlur()',
'[class.open]': 'isPopupOpen()',
'(document:click)': 'dismissPopup()',
'(document:click)': 'onDocumentClick($event)',
'(keydown)': 'handleKeyDown($event)',
'autocomplete': 'off',
'autocapitalize': 'off',
Expand All @@ -88,6 +87,7 @@ export class NgbTypeahead implements ControlValueAccessor,
private _subscription: Subscription;
private _userInput: string;
private _valueChanges: Observable<string>;
private _focus: Observable<string>;
private _resubscribeTypeahead: BehaviorSubject<any>;
private _windowRef: ComponentRef<NgbTypeaheadWindow>;
private _zoneSubscription: any;
Expand Down Expand Up @@ -117,8 +117,11 @@ export class NgbTypeahead implements ControlValueAccessor,
/**
* A function to transform the provided observable text into the array of results. Note that the "this" argument
* is undefined so you need to explicitly bind it to a desired "this" target.
* The function also receives an optional second argument holding the
* observable on focus events, sending the current input value and allowing
* you to provide a list of results on focus.
*/
@Input() ngbTypeahead: (text: Observable<string>) => Observable<any[]>;
@Input() ngbTypeahead: (text: Observable<string>, focus?: Observable<string>) => Observable<any[]>;

/**
* A function to format a given result before display. This function should return a formatted string without any
Expand Down Expand Up @@ -165,6 +168,7 @@ export class NgbTypeahead implements ControlValueAccessor,
this.placement = config.placement;

this._valueChanges = fromEvent(_elementRef.nativeElement, 'input', ($event) => $event.target.value);
this._focus = fromEvent(_elementRef.nativeElement, 'focus', ($event) => $event.target.value);

this._resubscribeTypeahead = new BehaviorSubject(null);

Expand All @@ -187,7 +191,7 @@ export class NgbTypeahead implements ControlValueAccessor,
this._onChange(value);
}
});
const results$ = letProto.call(inputValues$, this.ngbTypeahead);
const results$ = this.ngbTypeahead(inputValues$, this._focus);
const processedResults$ = _do.call(results$, () => {
if (!this.editable) {
this._onChange(undefined);
Expand All @@ -213,6 +217,29 @@ export class NgbTypeahead implements ControlValueAccessor,
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}

onDocumentClick(event) {
if (this.isPopupOpen()) {
if (!this._isClickInWidget(event)) {
this.dismissPopup();
}
}
}

_isClickInWidget(event) {
const target = event.target;

const input = this._viewContainerRef.element.nativeElement;
return this._isElementOrAncestorOf(input, target);
}

_isElementOrAncestorOf(possibleAncestor, reference) {
let currentNode = reference;
while (currentNode != null && currentNode !== possibleAncestor) {
currentNode = currentNode.parentNode;
}
return currentNode === possibleAncestor;
}

dismissPopup() {
if (this.isPopupOpen()) {
this._closePopup();
Expand Down Expand Up @@ -289,6 +316,7 @@ export class NgbTypeahead implements ControlValueAccessor,

if (!defaultPrevented) {
this.writeValue(result);
this._userInput = result;
this._onChange(result);
}
}
Expand All @@ -299,7 +327,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 @@ -318,7 +346,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', value == null ? '' : value);
}

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

0 comments on commit 25b2375

Please sign in to comment.