Skip to content

Commit

Permalink
feat(typeahead): add support for the showHint option
Browse files Browse the repository at this point in the history
Closes #639

Closes #640
  • Loading branch information
pkozlowski-opensource committed Sep 7, 2016
1 parent 79f4597 commit 5a0226d
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 29 deletions.
102 changes: 101 additions & 1 deletion src/typeahead/typeahead.spec.ts
@@ -1,5 +1,5 @@
import {TestBed, ComponentFixture, async} from '@angular/core/testing';
import {createGenericTestComponent} from '../test/common';
import {createGenericTestComponent, isBrowser} from '../test/common';
import {expectResults, getWindowLinks} from '../test/typeahead/common';

import {Component, DebugElement} from '@angular/core';
Expand Down Expand Up @@ -395,6 +395,103 @@ describe('ngb-typeahead', () => {
});
});

if (!isBrowser(['ie', 'edge'])) {
describe('hint', () => {

it('should show hint when an item starts with user input', async(() => {
const fixture = createTestComponent(
`<input type="text" [(ngModel)]="model" [ngbTypeahead]="findAnywhere" [showHint]="true"/>`);
const compiled = fixture.nativeElement;
const inputEl = getNativeInput(compiled);

fixture.whenStable().then(() => {
changeInput(compiled, 'on');
fixture.detectChanges();
expectWindowResults(compiled, ['+one', 'one more']);
expect(inputEl.value).toBe('one');
expect(inputEl.selectionStart).toBe(2);
expect(inputEl.selectionEnd).toBe(3);

const event = createKeyDownEvent(Key.ArrowDown);
getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event);
fixture.detectChanges();
expect(inputEl.value).toBe('one more');
expect(inputEl.selectionStart).toBe(2);
expect(inputEl.selectionEnd).toBe(8);
});
}));

it('should show hint with no selection when an item does not starts with user input', async(() => {
const fixture = createTestComponent(
`<input type="text" [(ngModel)]="model" [ngbTypeahead]="findAnywhere" [showHint]="true"/>`);
const compiled = fixture.nativeElement;
const inputEl = getNativeInput(compiled);

fixture.whenStable().then(() => {
changeInput(compiled, 'ne');
fixture.detectChanges();
expectWindowResults(compiled, ['+one', 'one more']);
expect(inputEl.value).toBe('one');
expect(inputEl.selectionStart).toBe(inputEl.selectionEnd);

const event = createKeyDownEvent(Key.ArrowDown);
getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event);
fixture.detectChanges();
expect(inputEl.value).toBe('one more');
expect(inputEl.selectionStart).toBe(inputEl.selectionEnd);
});
}));

it('should take input formatter into account when displaying hints', async(() => {
const fixture = createTestComponent(`<input type="text" [(ngModel)]="model"
[ngbTypeahead]="findAnywhere"
[inputFormatter]="uppercaseFormatter"
[showHint]="true"/>`);
const compiled = fixture.nativeElement;
const inputEl = getNativeInput(compiled);

fixture.whenStable().then(() => {
changeInput(compiled, 'on');
fixture.detectChanges();
expectWindowResults(compiled, ['+one', 'one more']);
expect(inputEl.value).toBe('onE');
expect(inputEl.selectionStart).toBe(2);
expect(inputEl.selectionEnd).toBe(3);

const event = createKeyDownEvent(Key.ArrowDown);
getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event);
fixture.detectChanges();
expect(inputEl.value).toBe('onE MORE');
expect(inputEl.selectionStart).toBe(2);
expect(inputEl.selectionEnd).toBe(8);
});
}));

it('should restore hint when results window is dismissed', async(() => {
const fixture = createTestComponent(
`<input type="text" [(ngModel)]="model" [ngbTypeahead]="findAnywhere" [showHint]="true"/>`);
const compiled = fixture.nativeElement;
const inputEl = getNativeInput(compiled);

fixture.whenStable().then(() => {
changeInput(compiled, 'on');
fixture.detectChanges();
expectWindowResults(compiled, ['+one', 'one more']);
expect(inputEl.value).toBe('one');
expect(inputEl.selectionStart).toBe(2);
expect(inputEl.selectionEnd).toBe(3);

const event = createKeyDownEvent(Key.Escape);
getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event);
fixture.detectChanges();
expect(inputEl.value).toBe('on');
expect(inputEl.selectionStart).toBe(2);
expect(inputEl.selectionEnd).toBe(2);
});
}));
});
}

});

@Component({selector: 'test-cmp', template: ''})
Expand All @@ -410,6 +507,9 @@ class TestComponent {

find = (text$: Observable<string>) => { return text$.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)); };

findNothing = (text$: Observable<string>) => { return text$.map(text => []); };

findObjects =
Expand Down
94 changes: 66 additions & 28 deletions src/typeahead/typeahead.ts
Expand Up @@ -17,6 +17,7 @@ import {
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Observable, Subject, Subscription} from 'rxjs/Rx';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/let';
import {Positioning} from '../util/positioning';
import {NgbTypeaheadWindow, ResultTemplateContext} from './typeahead-window';
Expand Down Expand Up @@ -45,7 +46,7 @@ const NGB_TYPEAHEAD_VALUE_ACCESSOR = {
host: {
'(blur)': 'onTouched()',
'[class.open]': 'isPopupOpen()',
'(document:click)': 'closePopup()',
'(document:click)': 'dismissPopup()',
'(input)': 'onChange($event.target.value)',
'(keydown)': 'handleKeyDown($event)',
'autocomplete': 'off',
Expand All @@ -60,6 +61,7 @@ export class NgbTypeahead implements OnInit,
private _popupService: PopupService<NgbTypeaheadWindow>;
private _positioning = new Positioning();
private _subscription: Subscription;
private _userInput: string;
private _valueChanges = new Subject<string>();
private _windowRef: ComponentRef<NgbTypeaheadWindow>;

Expand All @@ -85,7 +87,12 @@ export class NgbTypeahead implements OnInit,
@Input() resultTemplate: TemplateRef<ResultTemplateContext>;

/**
* An event emitted when a match is selected. Event payload is equal to the selected item
* Show hint when an option in the result list matches.
*/
@Input() showHint = false;

/**
* An event emitted when a match is selected. Event payload is equal to the selected item.
*/
@Output() selectItem = new EventEmitter();

Expand Down Expand Up @@ -118,44 +125,45 @@ export class NgbTypeahead implements OnInit,
ngOnDestroy() { this._subscription.unsubscribe(); }

ngOnInit() {
this._subscription = this._valueChanges.let (this.ngbTypeahead).subscribe((results) => {
if (!results || results.length === 0) {
this.closePopup();
} else {
this._openPopup();
this._windowRef.instance.results = results;
this._windowRef.instance.term = this._elementRef.nativeElement.value;
if (this.resultFormatter) {
this._windowRef.instance.formatter = this.resultFormatter;
}
if (this.resultTemplate) {
this._windowRef.instance.resultTemplate = this.resultTemplate;
}
}
});
this._subscription =
this._valueChanges.do(value => this._userInput = value).let (this.ngbTypeahead).subscribe((results) => {
if (!results || results.length === 0) {
this._closePopup();
} else {
this._openPopup();
this._windowRef.instance.results = results;
this._windowRef.instance.term = this._elementRef.nativeElement.value;
if (this.resultFormatter) {
this._windowRef.instance.formatter = this.resultFormatter;
}
if (this.resultTemplate) {
this._windowRef.instance.resultTemplate = this.resultTemplate;
}
this._showHint();
}
});
}

registerOnChange(fn: (value: any) => any): void { this._onChangeNoEmit = fn; }

registerOnTouched(fn: () => any): void { this.onTouched = fn; }

writeValue(value) {
const formattedValue = value && this.inputFormatter ? this.inputFormatter(value) : toString(value);
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', formattedValue);
}
writeValue(value) { this._writeInputValue(this._formatItemForInput(value)); }

/**
* @internal
*/
isPopupOpen() { return this._windowRef != null; }
dismissPopup() {
if (this.isPopupOpen()) {
this._closePopup();
this._writeInputValue(this._userInput);
}
}

/**
* @internal
*/
closePopup() {
this._popupService.close();
this._windowRef = null;
}
isPopupOpen() { return this._windowRef != null; }

/**
* @internal
Expand All @@ -171,17 +179,19 @@ export class NgbTypeahead implements OnInit,
switch (event.which) {
case Key.ArrowDown:
this._windowRef.instance.next();
this._showHint();
break;
case Key.ArrowUp:
this._windowRef.instance.prev();
this._showHint();
break;
case Key.Enter:
case Key.Tab:
const result = this._windowRef.instance.getActive();
this._selectResult(result);
break;
case Key.Escape:
this.closePopup();
this.dismissPopup();
break;
}
}
Expand All @@ -194,11 +204,39 @@ export class NgbTypeahead implements OnInit,
}
}

private _closePopup() {
this._popupService.close();
this._windowRef = null;
}

private _selectResult(result: any) {
this.writeValue(result);
this._onChangeNoEmit(result);
this.selectItem.emit(result);
this.closePopup();
this._closePopup();
}

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

if (userInputLowerCase === formattedVal.substr(0, this._userInput.length).toLowerCase()) {
this._writeInputValue(this._userInput + formattedVal.substr(this._userInput.length));
this._renderer.invokeElementMethod(
this._elementRef.nativeElement, 'setSelectionRange', [this._userInput.length, formattedVal.length]);
} else {
this.writeValue(this._windowRef.instance.getActive());
}
}
}

private _formatItemForInput(item: any): string {
return item && this.inputFormatter ? this.inputFormatter(item) : toString(item);
}

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

Expand Down

0 comments on commit 5a0226d

Please sign in to comment.