Skip to content

Commit

Permalink
fix(typeahead): improve 'ngbTypeahead' typings (#4038)
Browse files Browse the repository at this point in the history
Improves "search" function typings by using standard rxjs `OperatorFunction` type and making search function optional

```ts
// before
@input() ngbTypeahead: (text: Observable<string>) => Observable<readonly any[]>;

// after
@input() ngbTypeahead: OperatorFunction<string, readonly any[]>| null | undefined;
```

Fixes #3907
  • Loading branch information
maxokorokov committed Mar 18, 2021
1 parent 67be4ab commit 81f2c59
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {Observable} from 'rxjs';
import {Observable, OperatorFunction} from 'rxjs';
import {debounceTime, distinctUntilChanged, map} from 'rxjs/operators';

const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado',
Expand All @@ -19,7 +19,7 @@ const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'C
export class NgbdTypeaheadBasic {
public model: any;

search = (text$: Observable<string>) =>
search: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) =>
text$.pipe(
debounceTime(200),
distinctUntilChanged(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Component, ViewChild} from '@angular/core';
import {NgbTypeahead} from '@ng-bootstrap/ng-bootstrap';
import {Observable, Subject, merge} from 'rxjs';
import {Observable, Subject, merge, OperatorFunction} from 'rxjs';
import {debounceTime, distinctUntilChanged, filter, map} from 'rxjs/operators';

const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado',
Expand All @@ -24,7 +24,7 @@ export class NgbdTypeaheadFocus {
focus$ = new Subject<string>();
click$ = new Subject<string>();

search = (text$: Observable<string>) => {
search: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) => {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const inputFocus$ = this.focus$;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {Observable} from 'rxjs';
import {Observable, OperatorFunction} from 'rxjs';
import {debounceTime, distinctUntilChanged, map} from 'rxjs/operators';

const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado',
Expand All @@ -21,7 +21,7 @@ export class NgbdTypeaheadFormat {

formatter = (result: string) => result.toUpperCase();

search = (text$: Observable<string>) =>
search: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) =>
text$.pipe(
debounceTime(200),
distinctUntilChanged(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Component, Injectable} from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import {Observable, of} from 'rxjs';
import {Observable, of, OperatorFunction} from 'rxjs';
import {catchError, debounceTime, distinctUntilChanged, map, tap, switchMap} from 'rxjs/operators';

const WIKI_URL = 'https://en.wikipedia.org/w/api.php';
Expand All @@ -22,7 +22,7 @@ export class WikipediaService {
}

return this.http
.get(WIKI_URL, {params: PARAMS.set('search', term)}).pipe(
.get<[any, string[]]>(WIKI_URL, {params: PARAMS.set('search', term)}).pipe(
map(response => response[1])
);
}
Expand All @@ -41,7 +41,7 @@ export class NgbdTypeaheadHttp {

constructor(private _service: WikipediaService) {}

search = (text$: Observable<string>) =>
search: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) =>
text$.pipe(
debounceTime(300),
distinctUntilChanged(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {Observable} from 'rxjs';
import {Observable, OperatorFunction} from 'rxjs';
import {debounceTime, distinctUntilChanged, map, filter} from 'rxjs/operators';

type State = {id: number, name: string};
Expand Down Expand Up @@ -76,7 +76,7 @@ export class NgbdTypeaheadPreventManualEntry {

formatter = (state: State) => state.name;

search = (text$: Observable<string>) => text$.pipe(
search: OperatorFunction<string, readonly {id, name}[]> = (text$: Observable<string>) => text$.pipe(
debounceTime(200),
distinctUntilChanged(),
filter(term => term.length >= 2),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {Observable} from 'rxjs';
import {Observable, OperatorFunction} from 'rxjs';
import {debounceTime, map} from 'rxjs/operators';

const statesWithFlags: {name: string, flag: string}[] = [
Expand Down Expand Up @@ -66,7 +66,7 @@ const statesWithFlags: {name: string, flag: string}[] = [
export class NgbdTypeaheadTemplate {
public model: any;

search = (text$: Observable<string>) =>
search: OperatorFunction<string, readonly {name, flag}[]> = (text$: Observable<string>) =>
text$.pipe(
debounceTime(200),
map(term => term === '' ? []
Expand Down
40 changes: 37 additions & 3 deletions src/typeahead/typeahead.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, Component, DebugElement, ViewChild} from '@angu
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {merge, Observable, Subject} from 'rxjs';
import {merge, Observable, of, OperatorFunction, Subject} from 'rxjs';
import {debounceTime, filter, map} from 'rxjs/operators';

import {createGenericTestComponent, isBrowser} from '../test/common';
Expand Down Expand Up @@ -181,6 +181,40 @@ describe('ngb-typeahead', () => {
expect(getWindow(compiled)).toBeNull();
});

it('should accept "null" as ngbTypeahead value', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="null"/>`);
const compiled = fixture.nativeElement;
expect(getWindow(compiled)).toBeNull();
});

it('should accept "undefined" as ngbTypeahead value', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="undefined"/>`);
const compiled = fixture.nativeElement;
expect(getWindow(compiled)).toBeNull();
});

it('should allow changing ngbTypeahead value', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="findRef"/>`);
const compiled = fixture.nativeElement;

// null initially
expect(getWindow(compiled)).toBeNull();

// real value
fixture.componentInstance.findRef = (_: Observable<string>) => of(['one', 'one more']);
fixture.detectChanges();

changeInput(compiled, 'one');
fixture.detectChanges();
expectWindowResults(compiled, ['+one', 'one more']);

// back to null
fixture.componentInstance.findRef = undefined;
fixture.detectChanges();

expect(getWindow(compiled)).toBeNull();
});

it('should work when returning null as results', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="findNull"/>`);
const compiled = fixture.nativeElement;
Expand Down Expand Up @@ -328,7 +362,6 @@ describe('ngb-typeahead', () => {
expectWindowResults(compiled, ['+two', 'three']);
});


it('should properly make previous/next results active with down arrow keys when focusFirst is false', () => {
const fixture = createTestComponent(`<input type="text" [ngbTypeahead]="find" [focusFirst]="false"/>`);
const compiled = fixture.nativeElement;
Expand Down Expand Up @@ -939,6 +972,8 @@ class TestComponent {
focus$ = new Subject<string>();
click$ = new Subject<string>();

findRef: OperatorFunction<string, readonly any[]>| null | undefined = null;

find =
(text$: Observable<string>) => {
const clicks$ = this.click$.pipe(filter(() => !this.typeahead.isPopupOpen()));
Expand Down Expand Up @@ -973,7 +1008,6 @@ class TestComponent {

uppercaseObjFormatter = (obj: {value: string}) => { return `${obj.value}`.toUpperCase(); };


onSelect($event) { this.selectEventValue = $event; }
}

Expand Down
37 changes: 23 additions & 14 deletions src/typeahead/typeahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import {
Renderer2,
TemplateRef,
ViewContainerRef,
ApplicationRef
ApplicationRef,
OnChanges,
SimpleChanges
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {DOCUMENT} from '@angular/common';
import {BehaviorSubject, fromEvent, Observable, Subject, Subscription} from 'rxjs';
import {BehaviorSubject, fromEvent, Observable, of, OperatorFunction, Subject, Subscription} from 'rxjs';
import {map, switchMap, tap} from 'rxjs/operators';

import {Live} from '../util/accessibility/live';
Expand Down Expand Up @@ -73,7 +75,7 @@ let nextWindowId = 0;
providers: [{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgbTypeahead), multi: true}]
})
export class NgbTypeahead implements ControlValueAccessor,
OnInit, OnDestroy {
OnInit, OnChanges, OnDestroy {
private _popupService: PopupService<NgbTypeaheadWindow>;
private _subscription: Subscription | null = null;
private _closed$ = new Subject();
Expand Down Expand Up @@ -128,7 +130,7 @@ export class NgbTypeahead implements ControlValueAccessor,
*
* Note that the `this` argument is `undefined` so you need to explicitly bind it to a desired "this" target.
*/
@Input() ngbTypeahead: (text: Observable<string>) => Observable<readonly any[]>;
@Input() ngbTypeahead: OperatorFunction<string, readonly any[]>| null | undefined;

/**
* The function that converts an item from the result list to a `string` to display in the popup.
Expand Down Expand Up @@ -210,14 +212,13 @@ export class NgbTypeahead implements ControlValueAccessor,
});
}

ngOnInit(): void {
const inputValues$ = this._valueChanges.pipe(tap(value => {
this._inputValueBackup = this.showHint ? value : null;
this._onChange(this.editable ? value : undefined);
}));
const results$ = inputValues$.pipe(this.ngbTypeahead);
const userInput$ = this._resubscribeTypeahead.pipe(switchMap(() => results$));
this._subscription = this._subscribeToUserInput(userInput$);
ngOnInit(): void { this._subscribeToUserInput(); }

ngOnChanges({ngbTypeahead}: SimpleChanges): void {
if (ngbTypeahead && !ngbTypeahead.firstChange) {
this._unsubscribeFromUserInput();
this._subscribeToUserInput();
}
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -363,12 +364,20 @@ export class NgbTypeahead implements ControlValueAccessor,
this._renderer.setProperty(this._elementRef.nativeElement, 'value', toString(value));
}

private _subscribeToUserInput(userInput$: Observable<readonly any[]>): Subscription {
return userInput$.subscribe((results) => {
private _subscribeToUserInput(): void {
const results$ = this._valueChanges.pipe(
tap(value => {
this._inputValueBackup = this.showHint ? value : null;
this._onChange(this.editable ? value : undefined);
}),
this.ngbTypeahead ? this.ngbTypeahead : () => of([]));

this._subscription = this._resubscribeTypeahead.pipe(switchMap(() => results$)).subscribe(results => {
if (!results || results.length === 0) {
this._closePopup();
} else {
this._openPopup();

this._windowRef !.instance.focusFirst = this.focusFirst;
this._windowRef !.instance.results = results;
this._windowRef !.instance.term = this._elementRef.nativeElement.value;
Expand Down

0 comments on commit 81f2c59

Please sign in to comment.