Skip to content

Commit

Permalink
fix(typeahead): resubscribe for value changes on blur, esc, enter
Browse files Browse the repository at this point in the history
Fixes #723
Closes #1244
  • Loading branch information
dmytroyarmak authored and pkozlowski-opensource committed Aug 7, 2017
1 parent bc3fd61 commit 47797d3
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/merge';

@Injectable()
export class WikipediaService {
Expand Down Expand Up @@ -41,6 +42,7 @@ export class NgbdTypeaheadHttp {
model: any;
searching = false;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.searching = false);

constructor(private _service: WikipediaService) {}

Expand All @@ -56,5 +58,6 @@ export class NgbdTypeaheadHttp {
this.searchFailed = true;
return Observable.of([]);
}))
.do(() => this.searching = false);
.do(() => this.searching = false)
.merge(this.hideSearchingWhenUnsubscribed);
}
111 changes: 110 additions & 1 deletion src/typeahead/typeahead.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const createTestComponent = (html: string) =>
const createOnPushTestComponent = (html: string) =>
createGenericTestComponent(html, TestOnPushComponent) as ComponentFixture<TestOnPushComponent>;

const createAsyncTestComponent = (html: string) =>
createGenericTestComponent(html, TestAsyncComponent) as ComponentFixture<TestAsyncComponent>;

enum Key {
Tab = 9,
Enter = 13,
Expand Down Expand Up @@ -54,6 +57,13 @@ function changeInput(element: any, value: string) {
input.dispatchEvent(evt);
}

function blurInput(element: any) {
const input = getNativeInput(element);
const evt = document.createEvent('HTMLEvents');
evt.initEvent('blur', false, false);
input.dispatchEvent(evt);
}

function expectInputValue(element: HTMLElement, value: string) {
expect(getNativeInput(element).value).toBe(value);
}
Expand All @@ -68,7 +78,7 @@ describe('ngb-typeahead', () => {

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, TestOnPushComponent],
declarations: [TestComponent, TestOnPushComponent, TestAsyncComponent],
imports: [NgbTypeaheadModule.forRoot(), FormsModule, ReactiveFormsModule]
});
});
Expand Down Expand Up @@ -387,6 +397,96 @@ describe('ngb-typeahead', () => {
}));
});

describe('with async typeahead function', () => {
it('should not display results when input is "blured"', fakeAsync(() => {
const fixture = createAsyncTestComponent(`<input type="text" [ngbTypeahead]="find"/>`);
const compiled = fixture.nativeElement;

changeInput(compiled, 'one');
fixture.detectChanges();

tick(50);

blurInput(compiled);
fixture.detectChanges();

tick(250);
expect(getWindow(compiled)).toBeNull();

// Make sure that it is resubscribed again
changeInput(compiled, 'two');
fixture.detectChanges();
tick(250);
expect(getWindow(compiled)).not.toBeNull();
}));

it('should not display results when "Escape" is pressed', fakeAsync(() => {
const fixture = createAsyncTestComponent(`<input type="text" [ngbTypeahead]="find"/>`);
const compiled = fixture.nativeElement;

// Change input first time
changeInput(compiled, 'one');
fixture.detectChanges();

// Results for first input are loaded
tick(250);
expect(getWindow(compiled)).not.toBeNull();

// Change input second time
changeInput(compiled, 'two');
fixture.detectChanges();
tick(50);

// Press Escape while second is still in proggress
const event = createKeyDownEvent(Key.Escape);
getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event);
fixture.detectChanges();

// Results for second input are loaded (window shouldn't be opened in this case)
tick(250);
expect(getWindow(compiled)).toBeNull();

// Make sure that it is resubscribed again
changeInput(compiled, 'three');
fixture.detectChanges();
tick(250);
expect(getWindow(compiled)).not.toBeNull();
}));

it('should not display results when value selected while new results are been loading', fakeAsync(() => {
const fixture = createAsyncTestComponent(`<input type="text" [ngbTypeahead]="find"/>`);
const compiled = fixture.nativeElement;

// Change input first time
changeInput(compiled, 'one');
fixture.detectChanges();

// Results for first input are loaded
tick(250);
expect(getWindow(compiled)).not.toBeNull();

// Change input second time
changeInput(compiled, 'two');
fixture.detectChanges();
tick(50);

// Select a value from first results list while second is still in proggress
getWindowLinks(fixture.debugElement)[0].triggerEventHandler('click', {});
fixture.detectChanges();
expect(getWindow(compiled)).toBeNull();

// Results for second input are loaded (window shouldn't be opened in this case)
tick(250);
expect(getWindow(compiled)).toBeNull();

// Make sure that it is resubscribed again
changeInput(compiled, 'three');
fixture.detectChanges();
tick(250);
expect(getWindow(compiled)).not.toBeNull();
}));
});

describe('objects', () => {

it('should work with custom objects as values', async(() => {
Expand Down Expand Up @@ -839,3 +939,12 @@ class TestOnPushComponent {
return text$.debounceTime(200).map(text => this._strings.filter(v => v.startsWith(text)));
};
}

@Component({selector: 'test-async-cmp', template: ''})
class TestAsyncComponent {
private _strings = ['one', 'one more', 'two', 'three'];

find = (text$: Observable<string>) => {
return text$.debounceTime(200).map(text => this._strings.filter(v => v.startsWith(text)));
};
}
15 changes: 13 additions & 2 deletions src/typeahead/typeahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import {
} from '@angular/core';
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';
import {positionElements} from '../util/positioning';
import {NgbTypeaheadWindow, ResultTemplateContext} from './typeahead-window';
Expand Down Expand Up @@ -86,6 +88,7 @@ export class NgbTypeahead implements ControlValueAccessor,
private _subscription: Subscription;
private _userInput: string;
private _valueChanges: Observable<string>;
private _resubscribeTypeahead: BehaviorSubject<any>;
private _windowRef: ComponentRef<NgbTypeaheadWindow>;
private _zoneSubscription: any;

Expand Down Expand Up @@ -155,6 +158,8 @@ export class NgbTypeahead implements ControlValueAccessor,

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

this._resubscribeTypeahead = new BehaviorSubject(null);

this._popupService = new PopupService<NgbTypeaheadWindow>(
NgbTypeaheadWindow, _injector, _viewContainerRef, _renderer, componentFactoryResolver);

Expand All @@ -175,11 +180,12 @@ export class NgbTypeahead implements ControlValueAccessor,
}
});
const results$ = letProto.call(inputValues$, this.ngbTypeahead);
const userInput$ = _do.call(results$, () => {
const processedResults$ = _do.call(results$, () => {
if (!this.editable) {
this._onChange(undefined);
}
});
const userInput$ = switchMap.call(this._resubscribeTypeahead, () => processedResults$);
this._subscription = this._subscribeToUserInput(userInput$);
}

Expand Down Expand Up @@ -208,7 +214,10 @@ export class NgbTypeahead implements ControlValueAccessor,

isPopupOpen() { return this._windowRef != null; }

handleBlur() { this._onTouched(); }
handleBlur() {
this._resubscribeTypeahead.next(null);
this._onTouched();
}

handleKeyDown(event: KeyboardEvent) {
if (!this.isPopupOpen()) {
Expand Down Expand Up @@ -239,6 +248,7 @@ export class NgbTypeahead implements ControlValueAccessor,
break;
case Key.Escape:
event.preventDefault();
this._resubscribeTypeahead.next(null);
this.dismissPopup();
break;
}
Expand Down Expand Up @@ -267,6 +277,7 @@ export class NgbTypeahead implements ControlValueAccessor,
private _selectResult(result: any) {
let defaultPrevented = false;
this.selectItem.emit({item: result, preventDefault: () => { defaultPrevented = true; }});
this._resubscribeTypeahead.next(null);

if (!defaultPrevented) {
this.writeValue(result);
Expand Down

0 comments on commit 47797d3

Please sign in to comment.