From 1e43eba455b3500d570e4908557e0a3b1e3bf8dc Mon Sep 17 00:00:00 2001 From: Dmitriy Danilov Date: Wed, 5 Feb 2020 17:06:25 +0200 Subject: [PATCH] fix(typeahead): fix focus after leaving control (#4622) (#5593) * fix(typeahead): fix focus after leaving control (#4622) Co-authored-by: Dmitriy Danilov * fix(typeahead): fix wrong imports, update descrtiption, move property to config * fix(typeahead): rename property, rename all demos Co-authored-by: Ilya Tarusin Co-authored-by: dmitry-zhemchugov <44227371+dmitry-zhemchugov@users.noreply.github.com> --- .../cancel-on-focus-lost.html | 11 ++ .../cancel-on-focus-lost.ts | 102 ++++++++++++++++++ .../app/components/+typeahead/demos/index.ts | 6 +- .../+typeahead/typeahead-section.list.ts | 15 ++- demo/src/ng-api-doc.ts | 10 +- src/typeahead/typeahead.config.ts | 2 + src/typeahead/typeahead.directive.ts | 9 ++ 7 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.html create mode 100644 demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.ts diff --git a/demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.html b/demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.html new file mode 100644 index 0000000000..15ece2e68e --- /dev/null +++ b/demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.html @@ -0,0 +1,11 @@ +
Model: {{asyncSelected | json}}
+ + +
Loading
diff --git a/demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.ts b/demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.ts new file mode 100644 index 0000000000..29c299e162 --- /dev/null +++ b/demo/src/app/components/+typeahead/demos/cancel-on-focus-lost/cancel-on-focus-lost.ts @@ -0,0 +1,102 @@ +import { Component } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { TypeaheadMatch } from 'ngx-bootstrap/typeahead'; +import { mergeMap, delay } from 'rxjs/operators'; +import { TypeaheadConfig } from 'ngx-bootstrap/typeahead'; + +export function getTypeaheadConfig(): TypeaheadConfig { + return Object.assign(new TypeaheadConfig(), { cancelRequestOnFocusLost: true }); +} + +@Component({ + selector: 'demo-typeahead-cancel-on-focus-lost', + templateUrl: './cancel-on-focus-lost.html', + providers: [{ provide: TypeaheadConfig, useFactory: getTypeaheadConfig }] +}) +export class DemoTypeaheadCancelRequestOnFocusLostComponent { + asyncSelected: string; + typeaheadLoading: boolean; + typeaheadNoResults: boolean; + dataSource: Observable; + statesComplex: any[] = [ + { id: 1, name: 'Alabama', region: 'South' }, + { id: 2, name: 'Alaska', region: 'West' }, + { id: 3, name: 'Arizona', region: 'West' }, + { id: 4, name: 'Arkansas', region: 'South' }, + { id: 5, name: 'California', region: 'West' }, + { id: 6, name: 'Colorado', region: 'West' }, + { id: 7, name: 'Connecticut', region: 'Northeast' }, + { id: 8, name: 'Delaware', region: 'South' }, + { id: 9, name: 'Florida', region: 'South' }, + { id: 10, name: 'Georgia', region: 'South' }, + { id: 11, name: 'Hawaii', region: 'West' }, + { id: 12, name: 'Idaho', region: 'West' }, + { id: 13, name: 'Illinois', region: 'Midwest' }, + { id: 14, name: 'Indiana', region: 'Midwest' }, + { id: 15, name: 'Iowa', region: 'Midwest' }, + { id: 16, name: 'Kansas', region: 'Midwest' }, + { id: 17, name: 'Kentucky', region: 'South' }, + { id: 18, name: 'Louisiana', region: 'South' }, + { id: 19, name: 'Maine', region: 'Northeast' }, + { id: 21, name: 'Maryland', region: 'South' }, + { id: 22, name: 'Massachusetts', region: 'Northeast' }, + { id: 23, name: 'Michigan', region: 'Midwest' }, + { id: 24, name: 'Minnesota', region: 'Midwest' }, + { id: 25, name: 'Mississippi', region: 'South' }, + { id: 26, name: 'Missouri', region: 'Midwest' }, + { id: 27, name: 'Montana', region: 'West' }, + { id: 28, name: 'Nebraska', region: 'Midwest' }, + { id: 29, name: 'Nevada', region: 'West' }, + { id: 30, name: 'New Hampshire', region: 'Northeast' }, + { id: 31, name: 'New Jersey', region: 'Northeast' }, + { id: 32, name: 'New Mexico', region: 'West' }, + { id: 33, name: 'New York', region: 'Northeast' }, + { id: 34, name: 'North Dakota', region: 'Midwest' }, + { id: 35, name: 'North Carolina', region: 'South' }, + { id: 36, name: 'Ohio', region: 'Midwest' }, + { id: 37, name: 'Oklahoma', region: 'South' }, + { id: 38, name: 'Oregon', region: 'West' }, + { id: 39, name: 'Pennsylvania', region: 'Northeast' }, + { id: 40, name: 'Rhode Island', region: 'Northeast' }, + { id: 41, name: 'South Carolina', region: 'South' }, + { id: 42, name: 'South Dakota', region: 'Midwest' }, + { id: 43, name: 'Tennessee', region: 'South' }, + { id: 44, name: 'Texas', region: 'South' }, + { id: 45, name: 'Utah', region: 'West' }, + { id: 46, name: 'Vermont', region: 'Northeast' }, + { id: 47, name: 'Virginia', region: 'South' }, + { id: 48, name: 'Washington', region: 'South' }, + { id: 49, name: 'West Virginia', region: 'South' }, + { id: 50, name: 'Wisconsin', region: 'Midwest' }, + { id: 51, name: 'Wyoming', region: 'West' } + ]; + + constructor() { + this.dataSource = Observable.create((observer: any) => { + // Runs on every search + observer.next(this.asyncSelected); + }) + .pipe( + mergeMap((token: string) => this.getStatesAsObservable(token)), + delay(1000) + ); + } + + getStatesAsObservable(token: string): Observable { + const query = new RegExp(token, 'i'); + + return of( + this.statesComplex.filter((state: any) => { + return query.test(state.name); + }) + ); + } + + changeTypeaheadLoading(e: boolean): void { + this.typeaheadLoading = e; + } + + typeaheadOnSelect(e: TypeaheadMatch): void { + console.log('Selected value: ', e.value); + } +} diff --git a/demo/src/app/components/+typeahead/demos/index.ts b/demo/src/app/components/+typeahead/demos/index.ts index 2ee1143dc1..93e6ab08b4 100644 --- a/demo/src/app/components/+typeahead/demos/index.ts +++ b/demo/src/app/components/+typeahead/demos/index.ts @@ -7,6 +7,8 @@ import { DemoTypeaheadContainerComponent } from './container/container'; import { DemoTypeaheadDelayComponent } from './delay/delay'; import { DemoTypeaheadDropupComponent } from './dropup/dropup'; import { DemoTypeaheadFieldComponent } from './field/field'; +import { DemoTypeaheadCancelRequestOnFocusLostComponent } from './cancel-on-focus-lost/cancel-on-focus-lost'; +import { DemoTypeaheadReactiveFormComponent } from './reactive-form/reactive-form'; import { DemoTypeaheadFirstItemActiveComponent } from './first-item-active/first-item-active'; import { DemoTypeaheadFormComponent } from './form/form'; import { DemoTypeaheadGroupingComponent } from './grouping/grouping'; @@ -17,7 +19,6 @@ import { DemoTypeaheadNoResultComponent } from './no-result/no-result'; import { DemoTypeaheadOnBlurComponent } from './on-blur/on-blur'; import { DemoTypeaheadOnSelectComponent } from './on-select/on-select'; import { DemoTypeaheadPhraseDelimitersComponent } from './phrase-delimiters/phrase-delimiters'; -import { DemoTypeaheadReactiveFormComponent } from './reactive-form/reactive-form'; import { DemoTypeaheadScrollableComponent } from './scrollable/scrollable'; import { DemotypeaheadSelectFirstItemComponent } from './selected-first-item/selected-first-item'; import { DemoTypeaheadShowOnBlurComponent } from './show-on-blur/show-on-blur'; @@ -33,6 +34,9 @@ export const DEMO_COMPONENTS = [ DemoTypeaheadDelayComponent, DemoTypeaheadDropupComponent, DemoTypeaheadFieldComponent, + DemoTypeaheadAsyncComponent, + DemoTypeaheadCancelRequestOnFocusLostComponent, + DemoTypeaheadReactiveFormComponent, DemoTypeaheadFirstItemActiveComponent, DemoTypeaheadFormComponent, DemoTypeaheadGroupingComponent, diff --git a/demo/src/app/components/+typeahead/typeahead-section.list.ts b/demo/src/app/components/+typeahead/typeahead-section.list.ts index 9db1f41efd..f0e9de4939 100644 --- a/demo/src/app/components/+typeahead/typeahead-section.list.ts +++ b/demo/src/app/components/+typeahead/typeahead-section.list.ts @@ -3,13 +3,15 @@ import { ContentSection } from '../../docs/models/content-section.model'; import { DemoTopSectionComponent } from '../../docs/demo-section-components/demo-top-section'; import { DemoTypeaheadAdaptivePositionComponent } from './demos/adaptive-position/adaptive-position'; import { DemoTypeaheadAnimatedComponent } from './demos/animated/animated'; -import { DemoTypeaheadAsyncComponent } from './demos/async/async'; import { DemoTypeaheadBasicComponent } from './demos/basic/basic'; +import { DemoTypeaheadCancelRequestOnFocusLostComponent } from './demos/cancel-on-focus-lost/cancel-on-focus-lost'; import { DemoTypeaheadConfigComponent } from './demos/config/config'; import { DemoTypeaheadContainerComponent } from './demos/container/container'; import { DemoTypeaheadDelayComponent } from './demos/delay/delay'; import { DemoTypeaheadDropupComponent } from './demos/dropup/dropup'; import { DemoTypeaheadFieldComponent } from './demos/field/field'; +import { DemoTypeaheadAsyncComponent } from './demos/async/async'; +import { DemoTypeaheadReactiveFormComponent } from './demos/reactive-form/reactive-form'; import { DemoTypeaheadFormComponent } from './demos/form/form'; import { DemoTypeaheadGroupingComponent } from './demos/grouping/grouping'; import { DemoTypeaheadItemTemplateComponent } from './demos/item-template/item-template'; @@ -19,7 +21,6 @@ import { DemoTypeaheadNoResultComponent } from './demos/no-result/no-result'; import { DemoTypeaheadOnBlurComponent } from './demos/on-blur/on-blur'; import { DemoTypeaheadOnSelectComponent } from './demos/on-select/on-select'; import { DemoTypeaheadPhraseDelimitersComponent } from './demos/phrase-delimiters/phrase-delimiters'; -import { DemoTypeaheadReactiveFormComponent } from './demos/reactive-form/reactive-form'; import { DemoTypeaheadScrollableComponent } from './demos/scrollable/scrollable'; import { DemotypeaheadSelectFirstItemComponent } from './demos/selected-first-item/selected-first-item'; import { DemoTypeaheadShowOnBlurComponent } from './demos/show-on-blur/show-on-blur'; @@ -89,6 +90,14 @@ export const demoComponentContent: ContentSection[] = [ html: require('!!raw-loader!./demos/async/async.html'), outlet: DemoTypeaheadAsyncComponent }, + { + title: 'Cancel on focus lost', + anchor: 'cancel-on-focus-lost', + description: `

Set config property cancelRequestOnFocusLost to true if you want to cancel async request on focus lost event

`, + component: require('!!raw-loader!./demos/cancel-on-focus-lost/cancel-on-focus-lost.ts'), + html: require('!!raw-loader!./demos/cancel-on-focus-lost/cancel-on-focus-lost.html'), + outlet: DemoTypeaheadCancelRequestOnFocusLostComponent + }, { title: 'With delay', anchor: 'delay', @@ -233,7 +242,7 @@ export const demoComponentContent: ContentSection[] = [ title: 'Show results on blur', anchor: 'show-on-blur', description: ` -

Use input property typeaheadHideResultsOnBlur or config property hideResultsOnBlur +

Use input property typeaheadHideResultsOnBlur or config property hideResultsOnBlur to prevent hiding typeahead's results until a user doesn't choose an item

`, component: require('!!raw-loader!./demos/show-on-blur/show-on-blur.ts'), diff --git a/demo/src/ng-api-doc.ts b/demo/src/ng-api-doc.ts index f1aec0425f..3b9adee514 100644 --- a/demo/src/ng-api-doc.ts +++ b/demo/src/ng-api-doc.ts @@ -3780,6 +3780,12 @@ export const ngdoc: any = { "type": "boolean", "description": "

used to hide results on blur

\n" }, + { + "name": "cancelRequestOnFocusLost", + "defaultValue": "false", + "type": "boolean", + "description": "

if true, typeahead will cancel async request after focus was lost

\n" + }, { "name": "isAnimated", "defaultValue": "false", @@ -3920,7 +3926,7 @@ export const ngdoc: any = { "name": "typeaheadSingleWords", "defaultValue": "true", "type": "boolean", - "description": "

Can be use to search words by inserting a single white space between each characters\nfor example 'C a l i f o r n i a' will match 'California'.

\n" + "description": "

can be use to search words by inserting a single white space between each characters\nfor example 'C a l i f o r n i a' will match 'California'.

\n" }, { "name": "typeaheadWaitMs", @@ -3932,7 +3938,7 @@ export const ngdoc: any = { "defaultValue": " ", "type": "string", "description": "

should be used only in case typeaheadSingleWords attribute is true.\nSets the word delimiter to break words. Defaults to space.

\n" - } + }, ], "outputs": [ { diff --git a/src/typeahead/typeahead.config.ts b/src/typeahead/typeahead.config.ts index 568f5cd33b..05e0a0ccbe 100644 --- a/src/typeahead/typeahead.config.ts +++ b/src/typeahead/typeahead.config.ts @@ -9,6 +9,8 @@ export class TypeaheadConfig { isAnimated = false; /** used to hide results on blur */ hideResultsOnBlur = true; + /** if true, typeahead will cancel async request on blur */ + cancelRequestOnFocusLost = false; /** used to choose the first item in typeahead container */ selectFirstItem = true; /** used to active/inactive the first item in typeahead container */ diff --git a/src/typeahead/typeahead.directive.ts b/src/typeahead/typeahead.directive.ts index 44a0c28584..1feaab2695 100644 --- a/src/typeahead/typeahead.directive.ts +++ b/src/typeahead/typeahead.directive.ts @@ -137,6 +137,8 @@ export class TypeaheadDirective implements OnInit, OnDestroy { _container: TypeaheadContainerComponent; isActiveItemChanged = false; isTypeaheadOptionsListActive = false; + isFocused = false; + cancelRequestOnFocusLost = false; // tslint:disable-next-line:no-any protected keyUpEventEmitter: EventEmitter = new EventEmitter(); @@ -168,6 +170,7 @@ export class TypeaheadDirective implements OnInit, OnDestroy { Object.assign(this, { typeaheadHideResultsOnBlur: config.hideResultsOnBlur, + typeaheadCancelRequestOnFocusLost: config.cancelRequestOnFocusLost, typeaheadSelectFirstItem: config.selectFirstItem, typeaheadIsFirstItemActive: config.isFirstItemActive, typeaheadMinLength: config.minLength, @@ -269,6 +272,7 @@ export class TypeaheadDirective implements OnInit, OnDestroy { @HostListener('click') @HostListener('focus') onFocus(): void { + this.isFocused = true; if (this.typeaheadMinLength === 0) { this.typeaheadLoading.emit(true); this.keyUpEventEmitter.emit(this.element.nativeElement.value || ''); @@ -277,6 +281,7 @@ export class TypeaheadDirective implements OnInit, OnDestroy { @HostListener('blur') onBlur(): void { + this.isFocused = false; if (this._container && !this._container.isFocused) { this.typeaheadOnBlur.emit(this._container.active); } @@ -492,6 +497,10 @@ export class TypeaheadDirective implements OnInit, OnDestroy { return; } + if (!this.isFocused && this.cancelRequestOnFocusLost) { + return; + } + if (this._container) { // fix: remove usage of ngControl internals const _controlValue = (this.typeaheadLatinize