Skip to content

Commit 624009a

Browse files
authored
feat: adding server side search support (#723)
* feat: adding server side search support * refactor: addressing review comments * refactor: remove hiding search on options length logic * refactor: addressing review comments * refactor: update comment
1 parent 17dddb5 commit 624009a

File tree

3 files changed

+127
-46
lines changed

3 files changed

+127
-46
lines changed

projects/components/src/multi-select/multi-select.component.test.ts

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import { CommonModule } from '@angular/common';
22
import { fakeAsync, flush } from '@angular/core/testing';
3-
import { IconType } from '@hypertrace/assets-library';
3+
import { IconLibraryTestingModule, IconType } from '@hypertrace/assets-library';
44
import { NavigationService } from '@hypertrace/common';
55
import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest';
66
import { MockComponent } from 'ng-mocks';
77
import { NEVER } from 'rxjs';
88
import { ButtonComponent } from '../button/button.component';
99
import { DividerComponent } from '../divider/divider.component';
1010
import { LabelComponent } from '../label/label.component';
11+
import { LoadAsyncModule } from '../load-async/load-async.module';
1112
import { PopoverComponent } from '../popover/popover.component';
1213
import { PopoverModule } from '../popover/popover.module';
1314
import { SearchBoxComponent } from '../search-box/search-box.component';
1415
import { SelectOptionComponent } from '../select/select-option.component';
1516
import { MultiSelectJustify } from './multi-select-justify';
16-
import { MultiSelectComponent, TriggerLabelDisplayMode } from './multi-select.component';
17+
import { MultiSelectComponent, MultiSelectSearchMode, TriggerLabelDisplayMode } from './multi-select.component';
1718

1819
describe('Multi Select Component', () => {
1920
const hostFactory = createHostFactory<MultiSelectComponent<string>>({
2021
component: MultiSelectComponent,
21-
imports: [PopoverModule, CommonModule],
22+
imports: [PopoverModule, CommonModule, LoadAsyncModule, IconLibraryTestingModule],
2223
providers: [
2324
mockProvider(NavigationService, {
2425
navigation$: NEVER
@@ -48,14 +49,15 @@ describe('Multi Select Component', () => {
4849
test('should display initial selections', fakeAsync(() => {
4950
spectator = hostFactory(
5051
`
51-
<ht-multi-select [selected]="selected" [triggerLabelDisplayMode]="triggerLabelDisplayMode" [enableSearch]="true">
52+
<ht-multi-select [selected]="selected" [triggerLabelDisplayMode]="triggerLabelDisplayMode" [searchMode]="searchMode">
5253
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
5354
</ht-select-option>
5455
</ht-multi-select>`,
5556
{
5657
hostProps: {
5758
options: selectionOptions,
5859
selected: [selectionOptions[1].value],
60+
searchMode: MultiSelectSearchMode.CaseInsensitive,
5961
triggerLabelDisplayMode: TriggerLabelDisplayMode.Selection
6062
}
6163
}
@@ -187,7 +189,7 @@ describe('Multi Select Component', () => {
187189

188190
spectator = hostFactory(
189191
`
190-
<ht-multi-select [selected]="selected" (selectedChange)="onChange($event)" [placeholder]="placeholder" [enableSearch]="enableSearch">
192+
<ht-multi-select [selected]="selected" (selectedChange)="onChange($event)" [placeholder]="placeholder" [searchMode]="searchMode">
191193
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
192194
</ht-select-option>
193195
</ht-multi-select>`,
@@ -196,7 +198,7 @@ describe('Multi Select Component', () => {
196198
options: selectionOptions,
197199
selected: [selectionOptions[1].value],
198200
placeholder: 'Select options',
199-
enableSearch: true,
201+
searchMode: MultiSelectSearchMode.CaseInsensitive,
200202
onChange: onChange
201203
}
202204
}
@@ -231,7 +233,7 @@ describe('Multi Select Component', () => {
231233
expect(spectator.query(LabelComponent)?.label).toEqual('first and 5 more');
232234

233235
spectator.setHostInput({
234-
enableSearch: false
236+
searchMode: MultiSelectSearchMode.Disabled
235237
});
236238

237239
expect(spectator.query('.search-bar', { root: true })).not.toExist();
@@ -313,16 +315,19 @@ describe('Multi Select Component', () => {
313315
}));
314316

315317
test('should show searchbox if applicable and function as expected', fakeAsync(() => {
318+
const onSearchValueChangeSpy = jest.fn();
319+
316320
spectator = hostFactory(
317321
`
318-
<ht-multi-select [enableSearch]="enableSearch">
322+
<ht-multi-select [searchMode]="searchMode" (searchValueChange)="onSearchValueChange($event)">
319323
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
320324
</ht-select-option>
321325
</ht-multi-select>`,
322326
{
323327
hostProps: {
324328
options: selectionOptions,
325-
enableSearch: true
329+
searchMode: MultiSelectSearchMode.CaseInsensitive,
330+
onSearchValueChange: onSearchValueChangeSpy
326331
}
327332
}
328333
);
@@ -334,13 +339,15 @@ describe('Multi Select Component', () => {
334339

335340
spectator.component.searchOptions('fi');
336341
spectator.tick();
342+
expect(onSearchValueChangeSpy).toHaveBeenLastCalledWith('fi');
337343

338344
let options = spectator.queryAll('.multi-select-option', { root: true });
339345
expect(options.length).toBe(2);
340346
expect(options[0]).toContainText('first');
341347

342348
spectator.component.searchOptions('i');
343349
spectator.tick();
350+
expect(onSearchValueChangeSpy).toHaveBeenLastCalledWith('i');
344351

345352
options = spectator.queryAll('.multi-select-option', { root: true });
346353
expect(options.length).toBe(4);
@@ -351,15 +358,64 @@ describe('Multi Select Component', () => {
351358
expect(spectator.query('.clear-selected', { root: true })).not.toExist(); // Due to initial selection
352359
expect(spectator.query('.select-all', { root: true })).toExist();
353360

354-
// Set selected options to less than 5 and search box and buttons should hide
361+
// Set options list to less than 1 and search control buttons should be hidden
355362
spectator.setHostInput({
356-
options: selectionOptions.slice(0, 3)
363+
options: []
357364
});
358365

359-
expect(spectator.query('.search-bar', { root: true })).not.toExist();
360-
expect(spectator.query('.divider', { root: true })).not.toExist();
366+
expect(spectator.query('.search-bar', { root: true })).toExist();
367+
expect(spectator.query('.divider', { root: true })).toExist();
361368
expect(spectator.query('.clear-selected', { root: true })).not.toExist();
362369
expect(spectator.query('.select-all', { root: true })).not.toExist();
363370
flush();
364371
}));
372+
373+
test('should show search box and emit only when search mode is emit only', fakeAsync(() => {
374+
const onSearchValueChangeSpy = jest.fn();
375+
376+
spectator = hostFactory(
377+
`
378+
<ht-multi-select [searchMode]="searchMode" (searchValueChange)="onSearchValueChange($event)">
379+
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
380+
</ht-select-option>
381+
</ht-multi-select>`,
382+
{
383+
hostProps: {
384+
options: selectionOptions,
385+
searchMode: MultiSelectSearchMode.EmitOnly,
386+
onSearchValueChange: onSearchValueChangeSpy
387+
}
388+
}
389+
);
390+
391+
spectator.click('.trigger-content');
392+
393+
const searchBar = spectator.query('.search-bar', { root: true });
394+
expect(searchBar).toExist();
395+
396+
spectator.component.searchOptions('fi');
397+
spectator.tick();
398+
expect(onSearchValueChangeSpy).toHaveBeenLastCalledWith('fi');
399+
400+
// No change in options length since for this test, externally we did not filter any option
401+
let options = spectator.queryAll('.multi-select-option', { root: true });
402+
expect(options.length).toBe(6);
403+
spectator.component.searchOptions('i');
404+
spectator.tick();
405+
expect(onSearchValueChangeSpy).toHaveBeenLastCalledWith('i');
406+
407+
options = spectator.queryAll('.multi-select-option', { root: true });
408+
expect(options.length).toBe(6);
409+
410+
// Set selected options to less than 5 and search box and buttons should still be visible
411+
spectator.setHostInput({
412+
options: selectionOptions.slice(0, 3)
413+
});
414+
415+
expect(spectator.query('.search-bar', { root: true })).toExist();
416+
expect(spectator.query('.divider', { root: true })).toExist();
417+
expect(spectator.query('.clear-selected', { root: true })).not.toExist();
418+
expect(spectator.query('.select-all', { root: true })).toExist();
419+
flush();
420+
}));
365421
});

projects/components/src/multi-select/multi-select.component.ts

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -56,41 +56,39 @@ import { MultiSelectJustify } from './multi-select-justify';
5656
</ht-popover-trigger>
5757
<ht-popover-content>
5858
<div class="multi-select-content" [ngStyle]="{ 'min-width.px': triggerContainer.offsetWidth }">
59-
<ng-container *ngIf="this.enableSearch">
59+
<ng-container *ngIf="this.searchMode !== '${MultiSelectSearchMode.Disabled}'">
6060
<ng-container *ngIf="this.allOptions$ | async as allOptions">
61-
<ng-container *ngIf="allOptions.length > 5">
62-
<ht-search-box
63-
class="search-bar"
64-
(valueChange)="this.searchOptions($event)"
65-
[debounceTime]="200"
66-
displayMode="${SearchBoxDisplayMode.NoBorder}"
67-
></ht-search-box>
68-
<ht-divider class="divider"></ht-divider>
69-
70-
<ht-button
71-
class="clear-selected"
72-
*ngIf="this.isAnyOptionSelected()"
73-
role="${ButtonRole.Primary}"
74-
display="${ButtonStyle.Text}"
75-
label="Clear Selected"
76-
(click)="this.onClearSelected()"
77-
></ht-button>
78-
79-
<ht-button
80-
class="select-all"
81-
*ngIf="!this.isAnyOptionSelected()"
82-
role="${ButtonRole.Primary}"
83-
display="${ButtonStyle.Text}"
84-
label="Select All"
85-
(click)="this.onSelectAll()"
86-
></ht-button>
87-
</ng-container>
61+
<ht-search-box
62+
class="search-bar"
63+
(valueChange)="this.searchOptions($event)"
64+
[debounceTime]="200"
65+
displayMode="${SearchBoxDisplayMode.NoBorder}"
66+
></ht-search-box>
67+
<ht-divider class="divider"></ht-divider>
68+
69+
<ht-button
70+
class="clear-selected"
71+
*ngIf="this.isAnyOptionSelected()"
72+
role="${ButtonRole.Primary}"
73+
display="${ButtonStyle.Text}"
74+
label="Clear Selected"
75+
(click)="this.onClearSelected()"
76+
></ht-button>
77+
78+
<ht-button
79+
class="select-all"
80+
*ngIf="allOptions.length > 0 && !this.isAnyOptionSelected()"
81+
role="${ButtonRole.Primary}"
82+
display="${ButtonStyle.Text}"
83+
label="Select All"
84+
(click)="this.onSelectAll()"
85+
></ht-button>
8886
</ng-container>
8987
</ng-container>
9088
91-
<div class="multi-select-options">
89+
<div class="multi-select-options" *htLoadAsync="this.filteredOptions$ as filteredOptions">
9290
<div
93-
*ngFor="let item of this.filteredOptions$ | async"
91+
*ngFor="let item of filteredOptions"
9492
(click)="this.onSelectionChange(item)"
9593
class="multi-select-option"
9694
>
@@ -134,7 +132,7 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
134132
public showBorder: boolean = false;
135133

136134
@Input()
137-
public enableSearch: boolean = false;
135+
public searchMode: MultiSelectSearchMode = MultiSelectSearchMode.Disabled;
138136

139137
@Input()
140138
public justify: MultiSelectJustify = MultiSelectJustify.Left;
@@ -145,6 +143,9 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
145143
@Output()
146144
public readonly selectedChange: EventEmitter<V[]> = new EventEmitter<V[]>();
147145

146+
@Output()
147+
public readonly searchValueChange: EventEmitter<string> = new EventEmitter<string>();
148+
148149
@ContentChildren(SelectOptionComponent)
149150
private readonly allOptionsList?: QueryList<SelectOptionComponent<V>>;
150151
public allOptions$!: Observable<QueryList<SelectOptionComponent<V>>>;
@@ -170,7 +171,15 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
170171
}
171172

172173
public searchOptions(searchText: string): void {
173-
this.searchSubject.next(searchText);
174+
if (this.searchMode === MultiSelectSearchMode.Disabled) {
175+
return;
176+
}
177+
178+
if (this.searchMode === MultiSelectSearchMode.CaseInsensitive) {
179+
this.searchSubject.next(searchText);
180+
}
181+
182+
this.searchValueChange.emit(searchText);
174183
}
175184

176185
public onSelectAll(): void {
@@ -233,3 +242,9 @@ export const enum TriggerLabelDisplayMode {
233242
Selection = 'selection-mode',
234243
Icon = 'icon-mode'
235244
}
245+
246+
export const enum MultiSelectSearchMode {
247+
Disabled = 'disabled', // Search is not available
248+
CaseInsensitive = 'case-insensitive', // Current available values are filtered in a case insensitive way and an emit is triggered
249+
EmitOnly = 'emit-only' // Current available values not filtered, but an emit still triggered
250+
}

projects/components/src/multi-select/multi-select.module.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@ import { ButtonModule } from '../button/button.module';
44
import { DividerModule } from '../divider/divider.module';
55
import { IconModule } from '../icon/icon.module';
66
import { LabelModule } from '../label/label.module';
7+
import { LoadAsyncModule } from '../load-async/load-async.module';
78
import { PopoverModule } from '../popover/popover.module';
89
import { TraceSearchBoxModule } from '../search-box/search-box.module';
910
import { MultiSelectComponent } from './multi-select.component';
1011

1112
@NgModule({
12-
imports: [CommonModule, IconModule, LabelModule, PopoverModule, DividerModule, TraceSearchBoxModule, ButtonModule],
13+
imports: [
14+
CommonModule,
15+
IconModule,
16+
LabelModule,
17+
PopoverModule,
18+
DividerModule,
19+
TraceSearchBoxModule,
20+
ButtonModule,
21+
LoadAsyncModule
22+
],
1323
declarations: [MultiSelectComponent],
1424
exports: [MultiSelectComponent]
1525
})

0 commit comments

Comments
 (0)