From b6e3b62953cd68c211e4d7bc31a2a42654543c57 Mon Sep 17 00:00:00 2001 From: svetoldo4444ka Date: Sun, 16 Dec 2018 13:21:59 +0200 Subject: [PATCH] feat(typeahead): optionally do not hide the results on blur (#4783) fixes #2059 --- .../+typeahead/demos/config/config.html | 3 + .../+typeahead/demos/config/config.ts | 68 +++++++++++ .../app/components/+typeahead/demos/index.ts | 111 ++++-------------- .../demos/show-on-blur/show-on-blur.html | 11 ++ .../demos/show-on-blur/show-on-blur.ts | 62 ++++++++++ .../+typeahead/typeahead-section.list.ts | 49 ++++++-- demo/src/ng-api-doc.ts | 109 +++++++++++++++-- src/spec/typeahead-match.class.spec.ts | 33 ++++++ src/spec/typeahead.directive.spec.ts | 34 ++++++ src/typeahead/public_api.ts | 1 + src/typeahead/typeahead.config.ts | 8 ++ src/typeahead/typeahead.directive.ts | 15 ++- src/typeahead/typeahead.module.ts | 3 +- 13 files changed, 398 insertions(+), 109 deletions(-) create mode 100644 demo/src/app/components/+typeahead/demos/config/config.html create mode 100644 demo/src/app/components/+typeahead/demos/config/config.ts create mode 100644 demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.html create mode 100644 demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.ts create mode 100644 src/spec/typeahead-match.class.spec.ts create mode 100644 src/typeahead/typeahead.config.ts diff --git a/demo/src/app/components/+typeahead/demos/config/config.html b/demo/src/app/components/+typeahead/demos/config/config.html new file mode 100644 index 0000000000..d97751156a --- /dev/null +++ b/demo/src/app/components/+typeahead/demos/config/config.html @@ -0,0 +1,3 @@ + diff --git a/demo/src/app/components/+typeahead/demos/config/config.ts b/demo/src/app/components/+typeahead/demos/config/config.ts new file mode 100644 index 0000000000..26626d48c7 --- /dev/null +++ b/demo/src/app/components/+typeahead/demos/config/config.ts @@ -0,0 +1,68 @@ +import { Component } from '@angular/core'; +import { TypeaheadConfig } from 'ngx-bootstrap/typeahead'; + +// such override allows to keep some initial values +export function getTypeaheadConfig(): TypeaheadConfig { + return Object.assign(new TypeaheadConfig(), { hideResultsOnBlur: false }); +} + +@Component({ + selector: 'demo-typeahead-config', + templateUrl: './config.html', + providers: [{ provide: TypeaheadConfig, useFactory: getTypeaheadConfig }] +}) +export class DemoTypeaheadConfigComponent { + selected: string; + states: string[] = [ + 'Alabama', + 'Alaska', + 'Arizona', + 'Arkansas', + 'California', + 'Colorado', + 'Connecticut', + 'Delaware', + 'Florida', + 'Georgia', + 'Hawaii', + 'Idaho', + 'Illinois', + 'Indiana', + 'Iowa', + 'Kansas', + 'Kentucky', + 'Louisiana', + 'Maine', + 'Maryland', + 'Massachusetts', + 'Michigan', + 'Minnesota', + 'Mississippi', + 'Missouri', + 'Montana', + 'Nebraska', + 'Nevada', + 'New Hampshire', + 'New Jersey', + 'New Mexico', + 'New York', + 'North Dakota', + 'North Carolina', + 'Ohio', + 'Oklahoma', + 'Oregon', + 'Pennsylvania', + 'Rhode Island', + 'South Carolina', + 'South Dakota', + 'Tennessee', + 'Texas', + 'Utah', + 'Vermont', + 'Virginia', + 'Washington', + 'West Virginia', + 'Wisconsin', + 'Wyoming' + ]; +} diff --git a/demo/src/app/components/+typeahead/demos/index.ts b/demo/src/app/components/+typeahead/demos/index.ts index 78ceb91d30..1543d5b822 100644 --- a/demo/src/app/components/+typeahead/demos/index.ts +++ b/demo/src/app/components/+typeahead/demos/index.ts @@ -1,100 +1,41 @@ +import { DemoTypeaheadAsyncComponent } from './async/async'; import { DemoTypeaheadBasicComponent } from './basic/basic'; -import { DemoTypeaheadItemTemplateComponent } from './item-template/item-template'; +import { DemoTypeaheadConfigComponent } from './config/config'; +import { DemoTypeaheadContainerComponent } from './container/container'; +import { DemoTypeaheadDelayComponent } from './delay/delay'; +import { DemoTypeaheadDropupComponent } from './dropup/dropup'; import { DemoTypeaheadFieldComponent } from './field/field'; -import { DemoTypeaheadAsyncComponent } from './async/async'; -import { DemoTypeaheadReactiveFormComponent } from './reactive-form/reactive-form'; +import { DemoTypeaheadFormComponent } from './form/form'; import { DemoTypeaheadGroupingComponent } from './grouping/grouping'; -import { DemoTypeaheadDropupComponent } from './dropup/dropup'; -import { DemoTypeaheadScrollableComponent } from './scrollable/scrollable'; -import { DemoTypeaheadDelayComponent } from './delay/delay'; +import { DemoTypeaheadItemTemplateComponent } from './item-template/item-template'; import { DemoTypeaheadLatinizeComponent } from './latinize/latinize'; import { DemoTypeaheadNoResultComponent } from './no-result/no-result'; import { DemoTypeaheadOnBlurComponent } from './on-blur/on-blur'; -import { DemoTypeaheadContainerComponent } from './container/container'; -import { DemoTypeaheadSingleWorldComponent } from './single-world/single-world'; -import { DemoTypeaheadPhraseDelimitersComponent } from './phrase-delimiters/phrase-delimiters'; -import { DemoTypeaheadFormComponent } from './form/form'; 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 { DemoTypeaheadShowOnBlurComponent } from './show-on-blur/show-on-blur'; +import { DemoTypeaheadSingleWorldComponent } from './single-world/single-world'; export const DEMO_COMPONENTS = [ + DemoTypeaheadAsyncComponent, DemoTypeaheadBasicComponent, - DemoTypeaheadItemTemplateComponent, + DemoTypeaheadConfigComponent, + DemoTypeaheadContainerComponent, + DemoTypeaheadDelayComponent, + DemoTypeaheadDropupComponent, DemoTypeaheadFieldComponent, - DemoTypeaheadAsyncComponent, - DemoTypeaheadReactiveFormComponent, + DemoTypeaheadFormComponent, DemoTypeaheadGroupingComponent, - DemoTypeaheadSingleWorldComponent, - DemoTypeaheadPhraseDelimitersComponent, - DemoTypeaheadDropupComponent, - DemoTypeaheadScrollableComponent, - DemoTypeaheadDelayComponent, + DemoTypeaheadItemTemplateComponent, + DemoTypeaheadLatinizeComponent, DemoTypeaheadNoResultComponent, DemoTypeaheadOnBlurComponent, - DemoTypeaheadLatinizeComponent, - DemoTypeaheadContainerComponent, - DemoTypeaheadFormComponent, - DemoTypeaheadOnSelectComponent + DemoTypeaheadOnSelectComponent, + DemoTypeaheadPhraseDelimitersComponent, + DemoTypeaheadReactiveFormComponent, + DemoTypeaheadScrollableComponent, + DemoTypeaheadShowOnBlurComponent, + DemoTypeaheadSingleWorldComponent ]; - -export const DEMOS = { - static: { - component: require('!!raw-loader?lang=typescript!./basic/basic.ts'), - html: require('!!raw-loader?lang=markup!./basic/basic.html') - }, - itemTemplate: { - component: require('!!raw-loader?lang=typescript!./item-template/item-template.ts'), - html: require('!!raw-loader?lang=markup!./item-template/item-template.html') - }, - field: { - component: require('!!raw-loader?lang=typescript!./field/field.ts'), - html: require('!!raw-loader?lang=markup!./field/field.html') - }, - async: { - component: require('!!raw-loader?lang=typescript!./async/async.ts'), - html: require('!!raw-loader?lang=markup!./async/async.html') - }, - delay: { - component: require('!!raw-loader?lang=typescript!./delay/delay.ts'), - html: require('!!raw-loader?lang=markup!./delay/delay.html') - }, - latinize: { - component: require('!!raw-loader?lang=typescript!./latinize/latinize.ts'), - html: require('!!raw-loader?lang=markup!./latinize/latinize.html') - }, - form: { - component: require('!!raw-loader?lang=typescript!./form/form.ts'), - html: require('!!raw-loader?lang=markup!./form/form.html') - }, - reactiveForm: { - component: require('!!raw-loader?lang=typescript!./reactive-form/reactive-form.ts'), - html: require('!!raw-loader?lang=markup!./reactive-form/reactive-form.html') - }, - grouping: { - component: require('!!raw-loader?lang=typescript!./grouping/grouping.ts'), - html: require('!!raw-loader?lang=markup!./grouping/grouping.html') - }, - dropup: { - component: require('!!raw-loader?lang=typescript!./dropup/dropup.ts'), - html: require('!!raw-loader?lang=markup!./dropup/dropup.html') - }, - noResult: { - component: require('!!raw-loader?lang=typescript!./no-result/no-result.ts'), - html: require('!!raw-loader?lang=markup!./no-result/no-result.html') - }, - onBlur: { - component: require('!!raw-loader?lang=typescript!./on-blur/on-blur.ts'), - html: require('!!raw-loader?lang=markup!./on-blur/on-blur.html') - }, - container: { - component: require('!!raw-loader?lang=typescript!./container/container.ts'), - html: require('!!raw-loader?lang=markup!./container/container.html') - }, - scrollable: { - component: require('!!raw-loader?lang=typescript!./scrollable/scrollable.ts'), - html: require('!!raw-loader?lang=markup!./scrollable/scrollable.html') - }, - onSelect: { - component: require('!!raw-loader?lang=typescript!./on-select/on-select.ts'), - html: require('!!raw-loader?lang=markup!./on-select/on-select.html') - } -}; diff --git a/demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.html b/demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.html new file mode 100644 index 0000000000..a2be162190 --- /dev/null +++ b/demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.html @@ -0,0 +1,11 @@ + +
+  typeaheadHideResultsOnBlur: {{typeaheadHideResultsOnBlur}}
+  Model: {{selected | json}}
+
+ diff --git a/demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.ts b/demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.ts new file mode 100644 index 0000000000..765b651be2 --- /dev/null +++ b/demo/src/app/components/+typeahead/demos/show-on-blur/show-on-blur.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'demo-typeahead-show-on-blur', + templateUrl: './show-on-blur.html' +}) +export class DemoTypeaheadShowOnBlurComponent { + typeaheadHideResultsOnBlur = false; + selected: string; + states = [ + 'Alabama', + 'Alaska', + 'Arizona', + 'Arkansas', + 'California', + 'Colorado', + 'Connecticut', + 'Delaware', + 'Florida', + 'Georgia', + 'Hawaii', + 'Idaho', + 'Illinois', + 'Indiana', + 'Iowa', + 'Kansas', + 'Kentucky', + 'Louisiana', + 'Maine', + 'Maryland', + 'Massachusetts', + 'Michigan', + 'Minnesota', + 'Mississippi', + 'Missouri', + 'Montana', + 'Nebraska', + 'Nevada', + 'New Hampshire', + 'New Jersey', + 'New Mexico', + 'New York', + 'North Dakota', + 'North Carolina', + 'Ohio', + 'Oklahoma', + 'Oregon', + 'Pennsylvania', + 'Rhode Island', + 'South Carolina', + 'South Dakota', + 'Tennessee', + 'Texas', + 'Utah', + 'Vermont', + 'Virginia', + 'Washington', + 'West Virginia', + 'Wisconsin', + 'Wyoming' + ]; +} diff --git a/demo/src/app/components/+typeahead/typeahead-section.list.ts b/demo/src/app/components/+typeahead/typeahead-section.list.ts index 316c67a1ba..65da52368b 100644 --- a/demo/src/app/components/+typeahead/typeahead-section.list.ts +++ b/demo/src/app/components/+typeahead/typeahead-section.list.ts @@ -1,27 +1,29 @@ +import { DemoTypeaheadAsyncComponent } from './demos/async/async'; import { DemoTypeaheadBasicComponent } from './demos/basic/basic'; -import { DemoTypeaheadItemTemplateComponent } from './demos/item-template/item-template'; +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 { DemoTypeaheadDropupComponent } from './demos/dropup/dropup'; -import { DemoTypeaheadScrollableComponent } from './demos/scrollable/scrollable'; -import { DemoTypeaheadDelayComponent } from './demos/delay/delay'; +import { DemoTypeaheadItemTemplateComponent } from './demos/item-template/item-template'; import { DemoTypeaheadLatinizeComponent } from './demos/latinize/latinize'; import { DemoTypeaheadNoResultComponent } from './demos/no-result/no-result'; import { DemoTypeaheadOnBlurComponent } from './demos/on-blur/on-blur'; -import { DemoTypeaheadFormComponent } from './demos/form/form'; -import { DemoTypeaheadContainerComponent } from './demos/container/container'; -import { DemoTypeaheadSingleWorldComponent } from './demos/single-world/single-world'; -import { DemoTypeaheadPhraseDelimitersComponent } from './demos/phrase-delimiters/phrase-delimiters'; 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 { DemoTypeaheadShowOnBlurComponent } from './demos/show-on-blur/show-on-blur'; +import { DemoTypeaheadSingleWorldComponent } from './demos/single-world/single-world'; +import { ApiSectionsComponent } from '../../docs/demo-section-components/demo-api-section'; import { ContentSection } from '../../docs/models/content-section.model'; import { DemoTopSectionComponent } from '../../docs/demo-section-components/demo-top-section'; import { ExamplesComponent } from '../../docs/demo-section-components/demo-examples-section'; -import { ApiSectionsComponent } from '../../docs/demo-section-components/demo-api-section'; -import { NgApiDocComponent } from '../../docs/api-docs'; +import { NgApiDocComponent, NgApiDocConfigComponent } from '../../docs/api-docs'; export const demoComponentContent: ContentSection[] = [ { @@ -195,6 +197,24 @@ export const demoComponentContent: ContentSection[] = [ component: require('!!raw-loader?lang=typescript!./demos/on-select/on-select.ts'), html: require('!!raw-loader?lang=markup!./demos/on-select/on-select.html'), outlet: DemoTypeaheadOnSelectComponent + }, + { + title: 'Show results on blur', + anchor: 'show-on-blur', + description: ` +

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?lang=typescript!./demos/show-on-blur/show-on-blur.ts'), + html: require('!!raw-loader?lang=markup!./demos/show-on-blur/show-on-blur.html'), + outlet: DemoTypeaheadShowOnBlurComponent + }, + { + title: 'Configuring defaults', + anchor: 'configuration', + component: require('!!raw-loader?lang=typescript!./demos/config/config'), + html: require('!!raw-loader?lang=markup!./demos/config/config.html'), + outlet: DemoTypeaheadConfigComponent } ] }, @@ -207,6 +227,11 @@ export const demoComponentContent: ContentSection[] = [ title: 'TypeaheadDirective', anchor: 'TypeaheadDirective', outlet: NgApiDocComponent + }, + { + title: 'TypeaheadConfig', + anchor: 'bs-typeahead-config', + outlet: NgApiDocConfigComponent } ] } diff --git a/demo/src/ng-api-doc.ts b/demo/src/ng-api-doc.ts index 993a176fa5..1df19bd2de 100644 --- a/demo/src/ng-api-doc.ts +++ b/demo/src/ng-api-doc.ts @@ -151,13 +151,13 @@ export const ngdoc: any = { { "name": "btnCheckboxFalse", "defaultValue": "false", - "type": "any", + "type": "boolean", "description": "

Falsy value, will be set to ngModel

\n" }, { "name": "btnCheckboxTrue", "defaultValue": "true", - "type": "any", + "type": "boolean", "description": "

Truthy value, will be set to ngModel

\n" } ], @@ -183,7 +183,7 @@ export const ngdoc: any = { "inputs": [ { "name": "btnRadio", - "type": "any", + "type": "string", "description": "

Radio button value, will be set to ngModel

\n" }, { @@ -198,7 +198,7 @@ export const ngdoc: any = { }, { "name": "value", - "type": "any", + "type": "string", "description": "

Current value of radio component or group

\n" } ], @@ -366,7 +366,7 @@ export const ngdoc: any = { "name": "restartTimer", "description": "

Starts loop of auto changing of slides

\n", "args": [], - "returnType": "any" + "returnType": "void" }, { "name": "resetTimer", @@ -653,7 +653,7 @@ export const ngdoc: any = { "properties": [] }, "ListenOptions": { - "fileName": "src/component-loader/listen-options.model.ts", + "fileName": "src/utils/triggers.ts", "className": "ListenOptions", "description": "", "methods": [], @@ -1848,7 +1848,7 @@ export const ngdoc: any = { ] }, "Action": { - "fileName": "src/mini-ngrx/index.ts", + "fileName": "src/mini-ngrx/public_api.ts", "className": "Action", "description": "", "methods": [], @@ -2052,6 +2052,55 @@ export const ngdoc: any = { } ] }, + "ClassName": { + "fileName": "src/modal/models/index.ts", + "className": "ClassName", + "description": "", + "methods": [], + "properties": [] + }, + "Selector": { + "fileName": "src/modal/models/index.ts", + "className": "Selector", + "description": "", + "methods": [], + "properties": [] + }, + "TransitionDurations": { + "fileName": "src/modal/models/index.ts", + "className": "TransitionDurations", + "description": "", + "methods": [], + "properties": [] + }, + "DismissReasons": { + "fileName": "src/modal/models/index.ts", + "className": "DismissReasons", + "description": "", + "methods": [], + "properties": [] + }, + "ConfigModel": { + "fileName": "src/pagination/models/index.ts", + "className": "ConfigModel", + "description": "", + "methods": [], + "properties": [] + }, + "PagesModel": { + "fileName": "src/pagination/models/index.ts", + "className": "PagesModel", + "description": "", + "methods": [], + "properties": [] + }, + "PagerModel": { + "fileName": "src/pagination/models/index.ts", + "className": "PagerModel", + "description": "", + "methods": [], + "properties": [] + }, "PagerComponent": { "fileName": "src/pagination/pager.component.ts", "className": "PagerComponent", @@ -2198,7 +2247,7 @@ export const ngdoc: any = { { "name": "pageBtnClass", "type": "string", - "description": "

add class to <li>

\n" + "description": "

add class to <li>

\n" }, { "name": "previousText", @@ -2448,7 +2497,7 @@ export const ngdoc: any = { { "name": "animate", "type": "boolean", - "description": "

if true changing value of progress bar will be animated

\n" + "description": "

if true changing value of progress bar will be animated

\n" }, { "name": "max", @@ -2495,6 +2544,20 @@ export const ngdoc: any = { } ] }, + "RatingResults": { + "fileName": "src/rating/models/index.ts", + "className": "RatingResults", + "description": "", + "methods": [], + "properties": [] + }, + "AccessorContent": { + "fileName": "src/rating/models/index.ts", + "className": "AccessorContent", + "description": "", + "methods": [], + "properties": [] + }, "RatingComponent": { "fileName": "src/rating/rating.component.ts", "className": "RatingComponent", @@ -2520,7 +2583,7 @@ export const ngdoc: any = { { "name": "titles", "type": "string[]", - "description": "

array of icons titles

\n" + "description": "

array of icons titles, default: (["one", "two", "three", "four", "five"])

\n" } ], "outputs": [ @@ -2749,6 +2812,13 @@ export const ngdoc: any = { } ] }, + "ControlValueAccessorModel": { + "fileName": "src/timepicker/models/index.ts", + "className": "ControlValueAccessorModel", + "description": "", + "methods": [], + "properties": [] + }, "TimepickerActions": { "fileName": "src/timepicker/reducer/timepicker.actions.ts", "className": "TimepickerActions", @@ -3156,6 +3226,20 @@ export const ngdoc: any = { "properties": [], "methods": [] }, + "TypeaheadConfig": { + "fileName": "src/typeahead/typeahead.config.ts", + "className": "TypeaheadConfig", + "description": "

Default values provider for typeahead

\n", + "methods": [], + "properties": [ + { + "name": "hideResultsOnBlur", + "defaultValue": "true", + "type": "boolean", + "description": "

used to hide results on blur

\n" + } + ] + }, "TypeaheadDirective": { "fileName": "src/typeahead/typeahead.directive.ts", "className": "TypeaheadDirective", @@ -3194,6 +3278,11 @@ export const ngdoc: any = { "type": "string", "description": "

when options source is an array of objects, the name of field that\ncontains the group value, matches are grouped by this field when set.

\n" }, + { + "name": "typeaheadHideResultsOnBlur", + "type": "boolean", + "description": "

used to hide result on blur

\n" + }, { "name": "typeaheadItemTemplate", "type": "TemplateRef", diff --git a/src/spec/typeahead-match.class.spec.ts b/src/spec/typeahead-match.class.spec.ts new file mode 100644 index 0000000000..094e2c69c3 --- /dev/null +++ b/src/spec/typeahead-match.class.spec.ts @@ -0,0 +1,33 @@ +import { TypeaheadMatch } from '../typeahead'; + +describe('TypeaheadMatch tests', () => { + let typeaheadMatch: TypeaheadMatch = null; + + beforeEach(() => { + typeaheadMatch = new TypeaheadMatch('test', 'test text', false); + }); + + it('should have a valid constructor', () => { + expect(typeaheadMatch).not.toBeNull(); + }); + + it('should set values correctly through constructor', () => { + typeaheadMatch = new TypeaheadMatch('Alabama', 'test', true); + expect(typeaheadMatch.item).toEqual('Alabama'); + expect(typeaheadMatch.value).toEqual('test'); + }); + + it('should call isHeader and return header', () => { + const valueHeader = typeaheadMatch.isHeader(); + expect(valueHeader).toEqual(false); + }); + + it('should call toString and return value', () => { + const value = typeaheadMatch.toString(); + expect(value).toEqual('test text'); + }); + + afterEach(() => { + typeaheadMatch = null; + }); +}); diff --git a/src/spec/typeahead.directive.spec.ts b/src/spec/typeahead.directive.spec.ts index db490e8333..d7ed2a0132 100644 --- a/src/spec/typeahead.directive.spec.ts +++ b/src/spec/typeahead.directive.spec.ts @@ -78,6 +78,9 @@ describe('Directive: Typeahead', () => { it('should set a default value for typeaheadAsync', () => { expect(directive.typeaheadAsync).toBeFalsy(); }); + it('should set a default value for typeaheadHideResultsOnBlur', () => { + expect(directive.typeaheadHideResultsOnBlur).toBeTruthy(); + }); it('should typeaheadAsync to false, if typeahead is an observable', () => { directive.typeahead = of(component.states); @@ -255,4 +258,35 @@ describe('Directive: Typeahead', () => { fixture.detectChanges(); }); }); + + describe('if typeaheadHideResultsOnBlur', () => { + beforeEach( + fakeAsync(() => { + inputElement.value = 'Ala'; + fireEvent(inputElement, 'input'); + directive.typeaheadHideResultsOnBlur = false; + fixture.detectChanges(); + tick(100); + }) + ); + + it('equal true should be opened', + fakeAsync(() => { + document.dispatchEvent(new Event('click')); + tick(); + + expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open'); + }) + ); + + it('equal false should be closed', + fakeAsync(() => { + directive.typeaheadHideResultsOnBlur = true; + document.dispatchEvent(new Event('click')); + tick(); + + expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull(); + }) + ); + }); }); diff --git a/src/typeahead/public_api.ts b/src/typeahead/public_api.ts index 9275dbb68c..e379109ba0 100644 --- a/src/typeahead/public_api.ts +++ b/src/typeahead/public_api.ts @@ -11,3 +11,4 @@ export { export { TypeaheadContainerComponent } from './typeahead-container.component'; export { TypeaheadDirective } from './typeahead.directive'; export { TypeaheadModule } from './typeahead.module'; +export { TypeaheadConfig } from './typeahead.config'; diff --git a/src/typeahead/typeahead.config.ts b/src/typeahead/typeahead.config.ts new file mode 100644 index 0000000000..50e375ed42 --- /dev/null +++ b/src/typeahead/typeahead.config.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@angular/core'; + +/** Default values provider for typeahead */ +@Injectable() +export class TypeaheadConfig { + /** used to hide results on blur */ + hideResultsOnBlur = true; +} diff --git a/src/typeahead/typeahead.directive.ts b/src/typeahead/typeahead.directive.ts index a24ff04252..044669d73f 100644 --- a/src/typeahead/typeahead.directive.ts +++ b/src/typeahead/typeahead.directive.ts @@ -21,6 +21,7 @@ import { TypeaheadContainerComponent } from './typeahead-container.component'; import { TypeaheadMatch } from './typeahead-match.class'; import { getValueFromObject, latinize, tokenize } from './typeahead-utils'; import { debounceTime, filter, mergeMap, switchMap, toArray } from 'rxjs/operators'; +import { TypeaheadConfig } from './typeahead.config'; @Directive({selector: '[typeahead]', exportAs: 'bs-typeahead'}) export class TypeaheadDirective implements OnInit, OnDestroy { @@ -83,6 +84,8 @@ export class TypeaheadDirective implements OnInit, OnDestroy { @Input() typeaheadScrollable = false; /** specifies number of options to show in scroll view */ @Input() typeaheadOptionsInScrollableView = 5; + /** used to hide result on blur */ + @Input() typeaheadHideResultsOnBlur: boolean; /** fired when 'busy' state of this component was changed, * fired on async mode only, returns boolean */ @@ -137,19 +140,26 @@ export class TypeaheadDirective implements OnInit, OnDestroy { private element: ElementRef, viewContainerRef: ViewContainerRef, private renderer: Renderer2, + config: TypeaheadConfig, cis: ComponentLoaderFactory, private changeDetection: ChangeDetectorRef) { + this._typeahead = cis.createLoader( element, viewContainerRef, renderer - ); + ) + .provide({ provide: TypeaheadConfig, useValue: config }); + + Object.assign(this, { typeaheadHideResultsOnBlur: config.hideResultsOnBlur }); } ngOnInit(): void { this.typeaheadOptionsLimit = this.typeaheadOptionsLimit || 20; + this.typeaheadMinLength = this.typeaheadMinLength === void 0 ? 1 : this.typeaheadMinLength; + this.typeaheadWaitMs = this.typeaheadWaitMs || 0; // async should be false in case of array @@ -301,6 +311,9 @@ export class TypeaheadDirective implements OnInit, OnDestroy { if (this.typeaheadMinLength === 0 && this.element.nativeElement.contains(e.target)) { return undefined; } + if (!this.typeaheadHideResultsOnBlur || this.element.nativeElement.contains(e.target)) { + return undefined; + } this.onOutsideClick(); }); diff --git a/src/typeahead/typeahead.module.ts b/src/typeahead/typeahead.module.ts index d12e1cc0ce..35b0e8b573 100644 --- a/src/typeahead/typeahead.module.ts +++ b/src/typeahead/typeahead.module.ts @@ -5,6 +5,7 @@ import { TypeaheadContainerComponent } from './typeahead-container.component'; import { TypeaheadDirective } from './typeahead.directive'; import { ComponentLoaderFactory } from 'ngx-bootstrap/component-loader'; import { PositioningService } from 'ngx-bootstrap/positioning'; +import { TypeaheadConfig } from './typeahead.config'; @NgModule({ imports: [CommonModule], @@ -16,7 +17,7 @@ export class TypeaheadModule { static forRoot(): ModuleWithProviders { return { ngModule: TypeaheadModule, - providers: [ComponentLoaderFactory, PositioningService] + providers: [ComponentLoaderFactory, PositioningService, TypeaheadConfig] }; } }