diff --git a/src/app/playground-components.ts b/src/app/playground-components.ts
index 62c75f7165..f8782fe708 100644
--- a/src/app/playground-components.ts
+++ b/src/app/playground-components.ts
@@ -1161,6 +1161,12 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [
component: 'SelectIconComponent',
name: 'Select Icon',
},
+ {
+ path: 'select-search-showcase.component',
+ link: '/select/select-search-showcase.component',
+ component: 'SelectSearchShowcaseComponent',
+ name: 'Select Search Showcase',
+ },
],
},
{
diff --git a/src/framework/theme/components/form-field/form-field.component.scss b/src/framework/theme/components/form-field/form-field.component.scss
index 563aae7950..a33796a6b5 100644
--- a/src/framework/theme/components/form-field/form-field.component.scss
+++ b/src/framework/theme/components/form-field/form-field.component.scss
@@ -7,6 +7,9 @@
:host {
display: flex;
align-items: center;
+ &[hidden] {
+ display: none;
+ }
}
.nb-form-control-container {
diff --git a/src/framework/theme/components/option/option.component.scss b/src/framework/theme/components/option/option.component.scss
index 81da9f3399..f830b3b1f3 100644
--- a/src/framework/theme/components/option/option.component.scss
+++ b/src/framework/theme/components/option/option.component.scss
@@ -8,6 +8,9 @@
:host {
display: flex;
+ &[hidden] {
+ display: none;
+ }
&:hover {
cursor: pointer;
diff --git a/src/framework/theme/components/select/select.component.html b/src/framework/theme/components/select/select.component.html
index 400faf85c9..61a4e1ad2a 100644
--- a/src/framework/theme/components/select/select.component.html
+++ b/src/framework/theme/components/select/select.component.html
@@ -1,4 +1,5 @@
+
+
+
+
+
= new EventEmitter();
+ @Output() selectOpen: EventEmitter = new EventEmitter();
+ @Output() selectClose: EventEmitter = new EventEmitter();
+ @Output() optionSearchChange: EventEmitter = new EventEmitter();
/**
* List of `NbOptionComponent`'s components passed as content.
@@ -726,7 +741,8 @@ export class NbSelectComponent
* */
@ViewChild(NbPortalDirective) portal: NbPortalDirective;
- @ViewChild('selectButton', { read: ElementRef }) button: ElementRef;
+ @ViewChild('selectButton', { read: ElementRef }) button: ElementRef | undefined;
+ @ViewChild('optionSearchInput', { read: ElementRef }) optionSearchInput: ElementRef | undefined;
/**
* Determines is select opened.
@@ -736,6 +752,10 @@ export class NbSelectComponent
return this.ref && this.ref.hasAttached();
}
+ get isOptionSearchInputAllowed(): boolean {
+ return this.withOptionSearch && this.isOpen && !this.multiple;
+ }
+
/**
* List of selected options.
* */
@@ -822,6 +842,9 @@ export class NbSelectComponent
* Returns width of the select button.
* */
get hostWidth(): number {
+ if (this.isOptionSearchInputAllowed) {
+ return this.optionSearchInput.nativeElement.getBoundingClientRect().width;
+ }
return this.button.nativeElement.getBoundingClientRect().width;
}
@@ -849,7 +872,7 @@ export class NbSelectComponent
return this.selectionModel.map((option: NbOptionComponent) => option.content).join(', ');
}
- return this.selectionModel[0].content;
+ return this.selectionModel[0]?.content ?? '';
}
ngOnChanges({ disabled, status, size, fullWidth }: SimpleChanges) {
@@ -911,14 +934,24 @@ export class NbSelectComponent
}
}
+ onInput(event: Event) {
+ this.optionSearchChange.emit((event.target as HTMLInputElement).value);
+ }
+
show() {
if (this.shouldShow()) {
this.attachToOverlay();
this.positionStrategy.positionChange.pipe(take(1), takeUntil(this.destroy$)).subscribe(() => {
- this.setActiveOption();
+ if (this.isOptionSearchInputAllowed) {
+ this.optionSearchInput.nativeElement.focus();
+ } else {
+ this.setActiveOption();
+ }
});
+ this.selectOpen.emit();
+
this.cd.markForCheck();
}
}
@@ -927,6 +960,10 @@ export class NbSelectComponent
if (this.isOpen) {
this.ref.detach();
this.cd.markForCheck();
+ this.selectClose.emit();
+
+ this.optionSearchInput.nativeElement.value = this.selectionView;
+ this.optionSearchChange.emit('');
}
}
@@ -1061,8 +1098,11 @@ export class NbSelectComponent
}
protected createPositionStrategy(): NbAdjustableConnectedPositionStrategy {
+ const element: ElementRef = this.withOptionSearch
+ ? this.optionSearchInput
+ : this.button;
return this.positionBuilder
- .connectedTo(this.button)
+ .connectedTo(element)
.position(NbPosition.BOTTOM)
.offset(this.optionsOverlayOffset)
.adjustment(NbAdjustment.VERTICAL);
@@ -1123,9 +1163,9 @@ export class NbSelectComponent
)
.subscribe((event: KeyboardEvent) => {
if (event.keyCode === ESCAPE) {
- this.button.nativeElement.focus();
this.hide();
- } else {
+ this.button.nativeElement.focus();
+ } else if (!this.isOptionSearchInputAllowed) {
this.keyManager.onKeydown(event);
}
});
@@ -1137,11 +1177,21 @@ export class NbSelectComponent
}
protected subscribeOnButtonFocus() {
- this.focusMonitor
- .monitor(this.button)
+ const buttonFocus$ = this.focusMonitor.monitor(this.button).pipe(
+ map((origin) => !!origin),
+ startWith(false),
+ finalize(() => this.focusMonitor.stopMonitoring(this.button)),
+ );
+
+ const filterInputFocus$ = this.focusMonitor.monitor(this.optionSearchInput).pipe(
+ map((origin) => !!origin),
+ startWith(false),
+ finalize(() => this.focusMonitor.stopMonitoring(this.button)),
+ );
+
+ combineLatest([buttonFocus$, filterInputFocus$])
.pipe(
- map((origin) => !!origin),
- finalize(() => this.focusMonitor.stopMonitoring(this.button)),
+ map(([buttonFocus, filterInputFocus]) => buttonFocus || filterInputFocus),
takeUntil(this.destroy$),
)
.subscribe(this.focused$);
diff --git a/src/framework/theme/components/select/select.module.ts b/src/framework/theme/components/select/select.module.ts
index 4a72fe8601..47b39289ef 100644
--- a/src/framework/theme/components/select/select.module.ts
+++ b/src/framework/theme/components/select/select.module.ts
@@ -8,11 +8,9 @@ import { NbButtonModule } from '../button/button.module';
import { NbSelectComponent, NbSelectLabelComponent } from './select.component';
import { NbOptionModule } from '../option/option-list.module';
import { NbIconModule } from '../icon/icon.module';
+import { NbFormFieldModule } from '../form-field/form-field.module';
-const NB_SELECT_COMPONENTS = [
- NbSelectComponent,
- NbSelectLabelComponent,
-];
+const NB_SELECT_COMPONENTS = [NbSelectComponent, NbSelectLabelComponent];
@NgModule({
imports: [
@@ -23,12 +21,9 @@ const NB_SELECT_COMPONENTS = [
NbCardModule,
NbIconModule,
NbOptionModule,
+ NbFormFieldModule,
],
- exports: [
- ...NB_SELECT_COMPONENTS,
- NbOptionModule,
- ],
+ exports: [...NB_SELECT_COMPONENTS, NbOptionModule],
declarations: [...NB_SELECT_COMPONENTS],
})
-export class NbSelectModule {
-}
+export class NbSelectModule {}
diff --git a/src/framework/theme/components/select/select.spec.ts b/src/framework/theme/components/select/select.spec.ts
index e30c3e5e4f..89cd092bd9 100644
--- a/src/framework/theme/components/select/select.spec.ts
+++ b/src/framework/theme/components/select/select.spec.ts
@@ -73,6 +73,8 @@ const TEST_GROUPS = [
[multiple]="multiple"
[selected]="selected"
(selectedChange)="selectedChange.emit($event)"
+ (selectOpen)="opened = true"
+ (selectClose)="opened = false"
>
{{ selected.split('').reverse().join('') }}
@@ -93,6 +95,7 @@ export class NbSelectTestComponent {
@Output() selectedChange: EventEmitter = new EventEmitter();
@ViewChildren(NbOptionComponent) options: QueryList>;
groups = TEST_GROUPS;
+ opened = false;
}
@Component({
@@ -689,6 +692,22 @@ describe('Component: NbSelectComponent', () => {
selectFixture.debugElement.query(By.css('.select-button')).triggerEventHandler('blur', {});
expect(touchedSpy).not.toHaveBeenCalled();
}));
+
+ it('should emit open event after opening and close event after closing', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectTestComponent);
+ select = selectFixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+
+ selectFixture.detectChanges();
+ expect(selectFixture.componentInstance.opened).toBe(false);
+ select.show();
+ selectFixture.detectChanges();
+ flush();
+ expect(selectFixture.componentInstance.opened).toBe(true);
+ select.hide();
+ selectFixture.detectChanges();
+ flush();
+ expect(selectFixture.componentInstance.opened).toBe(false);
+ }));
});
describe('NbSelectComponent - falsy values', () => {
@@ -1216,3 +1235,115 @@ describe('NbSelect - dynamic options', () => {
}));
});
});
+
+@Component({
+ template: `
+
+
+
+ {{ option }}
+
+
+
+ `,
+})
+export class NbSelectWithExperimentalSearchComponent {
+ options: number[] = [1, 2, 3, 4, 5];
+ selectedValue: number = null;
+ filterValue: string = '';
+ isOpened: boolean = false;
+ @ViewChild(NbSelectComponent) selectComponent: NbSelectComponent;
+}
+
+describe('NbSelect - experimental search', () => {
+ let fixture: ComponentFixture;
+ let testComponent: NbSelectWithExperimentalSearchComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule.withRoutes([]),
+ FormsModule,
+ NbThemeModule.forRoot(),
+ NbLayoutModule,
+ NbSelectModule,
+ ],
+ declarations: [NbSelectWithExperimentalSearchComponent],
+ });
+
+ fixture = TestBed.createComponent(NbSelectWithExperimentalSearchComponent);
+ testComponent = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it("should update search input and don't emit filterChange when value of select is changed", fakeAsync(() => {
+ const searchInput = testComponent.selectComponent.optionSearchInput.nativeElement;
+
+ expect(searchInput.value).toEqual('');
+ expect(testComponent.filterValue).toEqual('');
+
+ testComponent.selectedValue = 1;
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+ expect(searchInput.value).toEqual('1');
+ expect(testComponent.filterValue).toEqual('');
+
+ testComponent.selectedValue = 2;
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+ expect(searchInput.value).toEqual('2');
+ expect(testComponent.filterValue).toEqual('');
+ }));
+
+ it('should mark touched when select button loose focus and select closed', fakeAsync(() => {
+ const touchedSpy = jasmine.createSpy('touched spy');
+
+ const selectFixture = TestBed.createComponent(NbSelectComponent);
+ const selectComponent: NbSelectComponent = selectFixture.componentInstance;
+ selectFixture.detectChanges();
+ flush();
+
+ selectComponent.registerOnTouched(touchedSpy);
+ selectFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', {});
+ expect(touchedSpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should make filter value empty and restore input to default after select is closed', fakeAsync(() => {
+ const searchInput = fixture.debugElement.query(By.css('input'));
+
+ testComponent.selectedValue = 1;
+
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ const initialValue = searchInput.nativeElement.value;
+
+ testComponent.selectComponent.show();
+ searchInput.triggerEventHandler('input', { target: { value: '123' } });
+
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(testComponent.filterValue).toBe('123');
+
+ testComponent.selectComponent.hide();
+
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(testComponent.filterValue).toBe('');
+ expect(searchInput.nativeElement.value).toBe(initialValue);
+ }));
+});
diff --git a/src/playground/with-layout/select/select-routing.module.ts b/src/playground/with-layout/select/select-routing.module.ts
index 3e43a17148..da1b2b25ed 100644
--- a/src/playground/with-layout/select/select-routing.module.ts
+++ b/src/playground/with-layout/select/select-routing.module.ts
@@ -5,7 +5,7 @@
*/
import { NgModule } from '@angular/core';
-import { RouterModule, Route} from '@angular/router';
+import { RouterModule, Route } from '@angular/router';
import { SelectCleanComponent } from './select-clean.component';
import { SelectDisabledComponent } from './select-disabled.component';
import { SelectFormComponent } from './select-form.component';
@@ -23,6 +23,7 @@ import { SelectInteractiveComponent } from './select-interactive.component';
import { SelectTestComponent } from './select-test.component';
import { SelectCompareWithComponent } from './select-compare-with.component';
import { SelectIconComponent } from './select-icon.component';
+import { SelectSearchShowcaseComponent } from './select-search-showcase.component';
const routes: Route[] = [
{
@@ -93,10 +94,14 @@ const routes: Route[] = [
path: 'select-icon.component',
component: SelectIconComponent,
},
+ {
+ path: 'select-search-showcase.component',
+ component: SelectSearchShowcaseComponent,
+ },
];
@NgModule({
- imports: [ RouterModule.forChild(routes) ],
- exports: [ RouterModule ],
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
})
export class SelectRoutingModule {}
diff --git a/src/playground/with-layout/select/select-search-showcase.component.html b/src/playground/with-layout/select/select-search-showcase.component.html
new file mode 100644
index 0000000000..c023421de4
--- /dev/null
+++ b/src/playground/with-layout/select/select-search-showcase.component.html
@@ -0,0 +1,25 @@
+
+
+
+ Option empty
+ Option 0
+ Option 1
+ Option 2
+ Option 3
+ Option 4
+
+
+ Option empty
+ Option 0
+ Option 1
+ Option 2
+ Option 3
+ Option 4
+
+
+
diff --git a/src/playground/with-layout/select/select-search-showcase.component.ts b/src/playground/with-layout/select/select-search-showcase.component.ts
new file mode 100644
index 0000000000..429f8686b5
--- /dev/null
+++ b/src/playground/with-layout/select/select-search-showcase.component.ts
@@ -0,0 +1,42 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'npg-select-search-showcase',
+ templateUrl: './select-search-showcase.component.html',
+})
+export class SelectSearchShowcaseComponent {
+ selectedItem = '2';
+ filterValue = '';
+
+ isEqual(a?, b?, c?): boolean {
+ if (!a) {
+ return true;
+ }
+
+ a = a?.toString().toLowerCase() ?? '';
+ a = new RegExp(
+ a
+ .split('')
+ .map((letter) => letter + '.*')
+ .join(''),
+ );
+ b = b?.toString().toLowerCase() ?? '';
+ c = c?.toString().toLowerCase() ?? '';
+
+ if (b && a.test(b)) {
+ return true;
+ }
+
+ if (c && a.test(c)) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/playground/with-layout/select/select.module.ts b/src/playground/with-layout/select/select.module.ts
index 72eae09de5..1f1ba089df 100644
--- a/src/playground/with-layout/select/select.module.ts
+++ b/src/playground/with-layout/select/select.module.ts
@@ -12,6 +12,7 @@ import {
NbCardModule,
NbFormFieldModule,
NbIconModule,
+ NbInputModule,
NbRadioModule,
NbSelectModule,
} from '@nebular/theme';
@@ -33,7 +34,7 @@ import { SelectInteractiveComponent } from './select-interactive.component';
import { SelectTestComponent } from './select-test.component';
import { SelectCompareWithComponent } from './select-compare-with.component';
import { SelectIconComponent } from './select-icon.component';
-
+import { SelectSearchShowcaseComponent } from './select-search-showcase.component';
@NgModule({
declarations: [
@@ -54,6 +55,7 @@ import { SelectIconComponent } from './select-icon.component';
SelectTestComponent,
SelectCompareWithComponent,
SelectIconComponent,
+ SelectSearchShowcaseComponent,
],
imports: [
FormsModule,
@@ -66,6 +68,7 @@ import { SelectIconComponent } from './select-icon.component';
NbButtonModule,
NbIconModule,
NbFormFieldModule,
+ NbInputModule,
],
})
-export class SelectModule { }
+export class SelectModule {}