diff --git a/projects/components/src/multi-select/multi-select.component.test.ts b/projects/components/src/multi-select/multi-select.component.test.ts index 34c594da1..54f7a1526 100644 --- a/projects/components/src/multi-select/multi-select.component.test.ts +++ b/projects/components/src/multi-select/multi-select.component.test.ts @@ -4,6 +4,8 @@ import { CommonModule } from '@angular/common'; import { fakeAsync, flush } from '@angular/core/testing'; import { IconLibraryTestingModule, IconType } from '@hypertrace/assets-library'; import { NavigationService } from '@hypertrace/common'; +import { PopoverComponent } from '@hypertrace/components'; +import { runFakeRxjs } from '@hypertrace/test-utils'; import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; import { NEVER } from 'rxjs'; @@ -12,7 +14,6 @@ import { CheckboxComponent } from '../checkbox/checkbox.component'; import { DividerComponent } from '../divider/divider.component'; import { LabelComponent } from '../label/label.component'; import { LoadAsyncModule } from '../load-async/load-async.module'; -import { PopoverComponent } from '../popover/popover.component'; import { PopoverModule } from '../popover/popover.module'; import { SearchBoxComponent } from '../search-box/search-box.component'; import { SelectOptionComponent } from '../select/select-option.component'; @@ -69,15 +70,20 @@ describe('Multi Select Component', () => { spectator.tick(); - expect(spectator.component.triggerLabel).toEqual(selectionOptions[1].label); expect(spectator.query('.trigger-content')).toExist(); expect(spectator.query('.trigger-label-container')).toExist(); expect(spectator.query('.trigger-label')).toExist(); expect(spectator.query('.trigger-icon')).toExist(); expect(spectator.query('.trigger-more-items')).not.toExist(); - // Selected element is 1 as set in hostProps - expect(spectator.component.selectedItemsCount).toBe(1); + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.component.triggerValues$).toBe('x', { + x: { + label: selectionOptions[1].label, + selectedItemsCount: 1 // Selected element is 1 as set in hostProps + } + }); + }); const popoverComponent = spectator.query(PopoverComponent); expect(popoverComponent?.closeOnClick).toEqual(false); @@ -101,11 +107,19 @@ describe('Multi Select Component', () => { spectator.tick(); const selectedCheckboxElements = spectator.queryAll('ht-checkbox', { root: true }); expect(spectator.query('.trigger-more-items')).toExist(); - expect(spectator.component.selectedItemsCount).toBe(2); expect( selectedCheckboxElements.filter(checkboxElement => checkboxElement.getAttribute('ng-reflect-checked') === 'true') .length ).toBe(2); + + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.component.triggerValues$).toBe('x', { + x: { + label: selectionOptions[1].label, + selectedItemsCount: 2 + } + }); + }); })); test('should display provided options with icons when clicked', fakeAsync(() => { @@ -136,12 +150,20 @@ describe('Multi Select Component', () => { const selectedCheckboxElements = spectator.queryAll('ht-checkbox', { root: true }); expect(spectator.query('.trigger-more-items')).toExist(); - expect(spectator.component.selectedItemsCount).toBe(2); expect( selectedCheckboxElements.filter(checkboxElement => checkboxElement.getAttribute('ng-reflect-checked') === 'true') .length ).toBe(2); + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.component.triggerValues$).toBe('x', { + x: { + label: 'second', + selectedItemsCount: 2 + } + }); + }); + optionElements.forEach((element, index) => { expect(element).toHaveText(selectionOptions[index].label); expect(element.querySelector('ht-icon')).toExist(); @@ -204,8 +226,17 @@ describe('Multi Select Component', () => { expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith([selectionOptions[1].value, selectionOptions[2].value]); expect(spectator.query('.trigger-more-items')).toExist(); - expect(spectator.component.selectedItemsCount).toBe(2); expect(spectator.query(LabelComponent)?.label).toEqual('second'); + + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.component.triggerValues$).toBe('x', { + x: { + label: 'second', + selectedItemsCount: 2 + } + }); + }); + flush(); })); @@ -332,7 +363,16 @@ describe('Multi Select Component', () => { expect(onChange).toHaveBeenCalledWith([selectionOptions[1].value, selectionOptions[2].value]); expect(spectator.query(LabelComponent)?.label).toEqual('Placeholder'); expect(spectator.query('.trigger-more-items')).not.toExist(); - expect(spectator.component.selectedItemsCount).toBe(0); + + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.component.triggerValues$).toBe('(x|)', { + x: { + label: 'Placeholder', + selectedItemsCount: 0 + } + }); + }); + flush(); })); @@ -354,7 +394,15 @@ describe('Multi Select Component', () => { ); spectator.tick(); - expect(spectator.component.triggerLabel).toEqual(selectionOptions[1].label); + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.component.triggerValues$).toBe('x', { + x: { + label: selectionOptions[1].label, + selectedItemsCount: 1 + } + }); + }); + expect(spectator.query('.trigger-content')).toExist(); expect(spectator.query('.trigger-label-container')).toExist(); expect(spectator.query('.trigger-label')).toExist(); diff --git a/projects/components/src/multi-select/multi-select.component.ts b/projects/components/src/multi-select/multi-select.component.ts index cdf927f95..307d0bcb7 100644 --- a/projects/components/src/multi-select/multi-select.component.ts +++ b/projects/components/src/multi-select/multi-select.component.ts @@ -10,8 +10,8 @@ import { QueryList } from '@angular/core'; import { IconType } from '@hypertrace/assets-library'; -import { queryListAndChanges$ } from '@hypertrace/common'; -import { BehaviorSubject, combineLatest, EMPTY, Observable, Subject } from 'rxjs'; +import { queryListAndChanges$, SubscriptionLifecycle } from '@hypertrace/common'; +import { BehaviorSubject, combineLatest, EMPTY, Observable, of, Subject } from 'rxjs'; import { map } from 'rxjs/operators'; import { ButtonRole, ButtonStyle } from '../button/button'; import { IconSize } from '../icon/icon-size'; @@ -24,6 +24,7 @@ import { MultiSelectJustify } from './multi-select-justify'; selector: 'ht-multi-select', styleUrls: ['./multi-select.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SubscriptionLifecycle], template: `
- -
- - +{{ this.selectedItemsCount - 1 }} - -
+ + +
+ + +{{ triggerValues.selectedItemsCount - 1 }} + +
+
@@ -163,8 +166,7 @@ export class MultiSelectComponent implements AfterContentInit, OnChanges { private readonly searchSubject: Subject = new BehaviorSubject(''); public popoverOpen: boolean = false; - public triggerLabel?: string; - public selectedItemsCount: number = 0; + public triggerValues$: Observable = new Observable(); public ngAfterContentInit(): void { this.allOptions$ = this.allOptionsList !== undefined ? queryListAndChanges$(this.allOptionsList) : EMPTY; @@ -236,22 +238,32 @@ export class MultiSelectComponent implements AfterContentInit, OnChanges { private setTriggerLabel(): void { if (this.triggerLabelDisplayMode === TriggerLabelDisplayMode.Placeholder) { - this.triggerLabel = this.placeholder; + this.triggerValues$ = of({ + label: this.placeholder, + selectedItemsCount: 0 + }); return; } - const selectedItems: SelectOptionComponent[] | undefined = this.allOptionsList?.filter(item => - this.isSelectedItem(item) - ); + this.triggerValues$ = this.allOptions$?.pipe( + map(options => { + const selectedItems: SelectOptionComponent[] = options.filter(item => this.isSelectedItem(item)); - this.selectedItemsCount = selectedItems?.length ?? 0; - - // Trigger label is placeholder in case there is element selected on multiselect - this.triggerLabel = this.selectedItemsCount === 0 ? this.placeholder : (selectedItems || [])[0]?.label; + return { + label: selectedItems.length === 0 ? this.placeholder : selectedItems[0]?.label, + selectedItemsCount: selectedItems.length + }; + }) + ); } } +interface TriggerValues { + label: string | undefined; + selectedItemsCount: number; +} + export const enum TriggerLabelDisplayMode { // These may be used as css classes Placeholder = 'placeholder-mode',