Skip to content

Commit

Permalink
fix(typeahead): fix focus after leaving control (#4622) (#5593)
Browse files Browse the repository at this point in the history
* fix(typeahead): fix focus after leaving control (#4622)

Co-authored-by: Dmitriy Danilov <daniloff200@gmail.com>

* fix(typeahead): fix wrong imports, update descrtiption, move property to config

* fix(typeahead): rename property, rename all demos

Co-authored-by: Ilya Tarusin <tarusin.iliya@gmail.com>
Co-authored-by: dmitry-zhemchugov <44227371+dmitry-zhemchugov@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 5, 2020
1 parent 690f836 commit 1e43eba
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<pre class="card card-block card-header">Model: {{asyncSelected | json}}</pre>

<input [(ngModel)]="asyncSelected"
[typeahead]="dataSource"
(typeaheadLoading)="changeTypeaheadLoading($event)"
(typeaheadOnSelect)="typeaheadOnSelect($event)"
[typeaheadOptionsLimit]="7"
typeaheadOptionField="name"
placeholder="Locations loaded with timeout"
class="form-control">
<div *ngIf="typeaheadLoading">Loading</div>
Original file line number Diff line number Diff line change
@@ -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<any>;
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<any> {
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);
}
}
6 changes: 5 additions & 1 deletion demo/src/app/components/+typeahead/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -33,6 +34,9 @@ export const DEMO_COMPONENTS = [
DemoTypeaheadDelayComponent,
DemoTypeaheadDropupComponent,
DemoTypeaheadFieldComponent,
DemoTypeaheadAsyncComponent,
DemoTypeaheadCancelRequestOnFocusLostComponent,
DemoTypeaheadReactiveFormComponent,
DemoTypeaheadFirstItemActiveComponent,
DemoTypeaheadFormComponent,
DemoTypeaheadGroupingComponent,
Expand Down
15 changes: 12 additions & 3 deletions demo/src/app/components/+typeahead/typeahead-section.list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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: `<p>Set config property <code>cancelRequestOnFocusLost</code> to <code>true</code> if you want to cancel async request on focus lost event</p>`,
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',
Expand Down Expand Up @@ -233,7 +242,7 @@ export const demoComponentContent: ContentSection[] = [
title: 'Show results on blur',
anchor: 'show-on-blur',
description: `
<p>Use input property <code>typeaheadHideResultsOnBlur</code> or config property <code>hideResultsOnBlur</code>
<p>Use input property <code>typeaheadHideResultsOnBlur</code> or config property <code>hideResultsOnBlur</code>
to prevent hiding typeahead's results until a user doesn't choose an item</p>
`,
component: require('!!raw-loader!./demos/show-on-blur/show-on-blur.ts'),
Expand Down
10 changes: 8 additions & 2 deletions demo/src/ng-api-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3780,6 +3780,12 @@ export const ngdoc: any = {
"type": "boolean",
"description": "<p>used to hide results on blur</p>\n"
},
{
"name": "cancelRequestOnFocusLost",
"defaultValue": "false",
"type": "boolean",
"description": "<p>if true, typeahead will cancel async request after focus was lost</p>\n"
},
{
"name": "isAnimated",
"defaultValue": "false",
Expand Down Expand Up @@ -3920,7 +3926,7 @@ export const ngdoc: any = {
"name": "typeaheadSingleWords",
"defaultValue": "true",
"type": "boolean",
"description": "<p>Can be use to search words by inserting a single white space between each characters\nfor example &#39;C a l i f o r n i a&#39; will match &#39;California&#39;.</p>\n"
"description": "<p>can be use to search words by inserting a single white space between each characters\nfor example &#39;C a l i f o r n i a&#39; will match &#39;California&#39;.</p>\n"
},
{
"name": "typeaheadWaitMs",
Expand All @@ -3932,7 +3938,7 @@ export const ngdoc: any = {
"defaultValue": " ",
"type": "string",
"description": "<p>should be used only in case typeaheadSingleWords attribute is true.\nSets the word delimiter to break words. Defaults to space.</p>\n"
}
},
],
"outputs": [
{
Expand Down
2 changes: 2 additions & 0 deletions src/typeahead/typeahead.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
9 changes: 9 additions & 0 deletions src/typeahead/typeahead.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> = new EventEmitter();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 || '');
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 1e43eba

Please sign in to comment.