From 24308a6b2a35e4ef993bef9329e1d6f08c216fd6 Mon Sep 17 00:00:00 2001 From: Dharan <14145706+dhrn@users.noreply.github.com> Date: Mon, 29 Nov 2021 15:10:59 +0530 Subject: [PATCH] [AAE-6623] Search and select multiple dropdown options (#7391) * [AAE-6623] Search and select multiple dropdown options * [ci:force] fix comment --- .../select-filter-input.component.spec.ts | 177 +++++++++++++----- .../select-filter-input.component.ts | 28 +++ 2 files changed, 160 insertions(+), 45 deletions(-) diff --git a/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.spec.ts b/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.spec.ts index c110dcb939..d6d05df6ea 100644 --- a/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.spec.ts +++ b/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.spec.ts @@ -20,12 +20,55 @@ import { setupTestBed } from '../../../../testing/setup-test-bed'; import { CoreTestingModule } from '../../../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; import { SelectFilterInputComponent } from './select-filter-input.component'; -import { MatSelect } from '@angular/material/select'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Component, ViewChild } from '@angular/core'; +import { By } from '@angular/platform-browser'; import { ESCAPE } from '@angular/cdk/keycodes'; +import { MatSelect } from '@angular/material/select'; + +@Component({ + selector: 'adf-test-filter', + template: ` + + + {{opt.name}} + + + ` +}) +export class TestComponent { + @ViewChild(SelectFilterInputComponent) filterInputComponent: SelectFilterInputComponent; + field: any = { value : '' }; + showInputFilter = true; + multiple = false; + standardOptions = [ + { 'id': '1', 'name': 'one' }, + { 'id': '2', 'name': 'two' }, + { 'id': '3', 'name': 'three' } + ]; + options = this.standardOptions; + + compare(obj1, obj2) { + if (!obj1 || !obj2) { + return false; + } + return obj1.id === obj2.id; + } + + onChange(search: string) { + if (!search) { + this.options = this.standardOptions; + } else { + this.options = this.standardOptions.filter(({ name }) => name.includes(search)); + } + } +} describe('SelectFilterInputComponent', () => { - + let testFixture: ComponentFixture; + let testComponent: TestComponent; let fixture: ComponentFixture; let component: SelectFilterInputComponent; let matSelect: MatSelect; @@ -36,58 +79,102 @@ describe('SelectFilterInputComponent', () => { CoreTestingModule, NoopAnimationsModule ], - providers: [ MatSelect ] + declarations: [ + TestComponent + ], + providers: [ + MatSelect + ] }); - beforeEach(() => { - fixture = TestBed.createComponent(SelectFilterInputComponent); - component = fixture.componentInstance; - matSelect = TestBed.inject(MatSelect); - fixture.detectChanges(); + describe('component', () => { + + beforeEach(() => { + fixture = TestBed.createComponent(SelectFilterInputComponent); + component = fixture.componentInstance; + matSelect = TestBed.inject(MatSelect); + fixture.detectChanges(); + }); + + it('should focus input on initialization', async () => { + spyOn(component.selectFilterInput.nativeElement, 'focus'); + matSelect.openedChange.next(true); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.selectFilterInput.nativeElement.focus).toHaveBeenCalled(); + }); + + it('should clear search term on close', async () => { + component.onModelChange('some-search-term'); + expect(component.term).toBe('some-search-term'); + + matSelect.openedChange.next(false); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.term).toBe(''); + }); + + it('should emit event when value changes', async () => { + spyOn(component.change, 'next'); + component.onModelChange('some-search-term'); + expect(component.change.next).toHaveBeenCalledWith('some-search-term'); + }); + + it('should reset value on reset() event', () => { + component.onModelChange('some-search-term'); + expect(component.term).toBe('some-search-term'); + + component.reset(); + expect(component.term).toBe(''); + }); + + it('should reset value on Escape event', () => { + component.onModelChange('some-search-term'); + expect(component.term).toBe('some-search-term'); + + component.selectFilterInput.nativeElement.dispatchEvent(new KeyboardEvent('keydown', {'keyCode': ESCAPE} as any)); + fixture.detectChanges(); + expect(component.term).toBe(''); + }); + }); - it('should focus input on initialization', async () => { - spyOn(component.selectFilterInput.nativeElement, 'focus'); - matSelect.openedChange.next(true); + describe('testComponent', () => { + beforeEach(() => { + testFixture = TestBed.createComponent(TestComponent); + testComponent = testFixture.componentInstance; + }); - fixture.detectChanges(); - await fixture.whenStable(); + afterEach(() => testFixture.destroy()); - expect(component.selectFilterInput.nativeElement.focus).toHaveBeenCalled(); - }); + it('should preserve the values for multiple search', async () => { + const userSelection = [{ 'id': '3', 'name': 'three' }]; + const preSelected = [ + { 'id': '1', 'name': 'one' }, + { 'id': '2', 'name': 'two' } + ]; + testComponent.field.value = preSelected; + testComponent.multiple = true; + testFixture.detectChanges(); - it('should clear search term on close', async () => { - component.onModelChange('some-search-term'); - expect(component.term).toBe('some-search-term'); + const dropdown: HTMLElement = testFixture.nativeElement.querySelector('.mat-select-trigger'); + dropdown.click(); + await testFixture.whenStable(); + testFixture.detectChanges(); - matSelect.openedChange.next(false); + const filter = testFixture.debugElement.query(By.css('input')); + filter.triggerEventHandler('input', { target: { value: 'three' } }); + testFixture.detectChanges(); - fixture.detectChanges(); - await fixture.whenStable(); + const option = testFixture.debugElement.query(By.css('mat-option')); + option.triggerEventHandler('click', null); + testFixture.detectChanges(); - expect(component.term).toBe(''); - }); - - it('should emit event when value changes', async () => { - spyOn(component.change, 'next'); - component.onModelChange('some-search-term'); - expect(component.change.next).toHaveBeenCalledWith('some-search-term'); - }); - - it('should reset value on reset() event', () => { - component.onModelChange('some-search-term'); - expect(component.term).toBe('some-search-term'); - - component.reset(); - expect(component.term).toBe(''); - }); - - it('should reset value on Escape event', () => { - component.onModelChange('some-search-term'); - expect(component.term).toBe('some-search-term'); - - component.selectFilterInput.nativeElement.dispatchEvent(new KeyboardEvent('keydown', {'keyCode': ESCAPE} as any)); - fixture.detectChanges(); - expect(component.term).toBe(''); + expect(testComponent.field.value).toEqual([...preSelected, ...userSelection]); + }); }); }); diff --git a/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.ts b/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.ts index ec17755c07..2ed6cbf379 100644 --- a/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.ts +++ b/lib/core/card-view/components/card-view-selectitem/select-filter-input/select-filter-input.component.ts @@ -33,6 +33,7 @@ export class SelectFilterInputComponent implements OnDestroy { @Output() change = new EventEmitter(); term = ''; + previousSelected: any[]; private onDestroy$ = new Subject(); constructor(@Inject(MatSelect) private matSelect: MatSelect) {} @@ -55,6 +56,33 @@ export class SelectFilterInputComponent implements OnDestroy { this.change.next(''); } }); + + if (this.matSelect.ngControl) { + this.previousSelected = this.matSelect.ngControl.value; + this.matSelect.ngControl.valueChanges + .pipe(takeUntil(this.onDestroy$)) + .subscribe((values) => { + let restoreSelection = false; + if (this.matSelect.multiple && Array.isArray(this.previousSelected)) { + if (!Array.isArray(values)) { + values = []; + } + const options = this.matSelect.options.map(option => option.value); + this.previousSelected.forEach((previous) => { + const isSelected = [...values, ...options].some(current => this.matSelect.compareWith(current, previous)); + if (!isSelected) { + values.push(previous); + restoreSelection = true; + } + }); + } + + this.previousSelected = values; + if (restoreSelection) { + this.matSelect._onChange(values); + } + }); + } } reset(event?: Event) {