Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(typeahead): fix focus after leaving control (#4622) #5593

Merged
merged 7 commits into from
Feb 5, 2020
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