diff --git a/projects/go-lib/karma.conf.js b/projects/go-lib/karma.conf.js index fb859c3ea..0e00ef2b4 100644 --- a/projects/go-lib/karma.conf.js +++ b/projects/go-lib/karma.conf.js @@ -22,7 +22,7 @@ module.exports = function (config) { thresholds: { statements: 93, lines: 92, - branches: 79, + branches: 75, functions: 88, }, }, diff --git a/projects/go-lib/src/lib/components/go-select/go-select.component.html b/projects/go-lib/src/lib/components/go-select/go-select.component.html index 355136a30..c3ac2862b 100644 --- a/projects/go-lib/src/lib/components/go-select/go-select.component.html +++ b/projects/go-lib/src/lib/components/go-select/go-select.component.html @@ -6,10 +6,11 @@ {{ label }} - + (close)="onClose()" + (scroll)="onScroll($event)" + #select +> + ng-header-tmp + > + @@ -78,4 +86,4 @@ [theme]="theme"> - \ No newline at end of file + diff --git a/projects/go-lib/src/lib/components/go-select/go-select.component.spec.ts b/projects/go-lib/src/lib/components/go-select/go-select.component.spec.ts index 0192a663c..ab68c1bb9 100644 --- a/projects/go-lib/src/lib/components/go-select/go-select.component.spec.ts +++ b/projects/go-lib/src/lib/components/go-select/go-select.component.spec.ts @@ -7,6 +7,7 @@ import { GoFormErrorsModule } from '../go-form-errors/go-form-errors.module'; import { GoHintModule } from '../go-hint/go-hint.module'; import { GoRequiredTextModule } from '../go-required-text/go-required-text.module'; import { GoSelectComponent } from './go-select.component'; +import { Subject } from 'rxjs'; describe('GoSelectComponent', () => { let component: GoSelectComponent; @@ -25,8 +26,7 @@ describe('GoSelectComponent', () => { FormsModule, ReactiveFormsModule ] - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { @@ -41,6 +41,11 @@ describe('GoSelectComponent', () => { }); describe('onSelectAll()', () => { + + beforeEach(() => { + component.multiple = true; + }); + it('adds all of the available items to the form control value', () => { component.bindValue = undefined; component.items = [ @@ -66,15 +71,111 @@ describe('GoSelectComponent', () => { expect(component.control.value).toEqual([1, 2, 3]); }); + + it('should select only filtered list, when filtered and selectAll', () => { + component.bindValue = 'id'; + component.items = [ + { id: 1, label: 'banana' }, + { id: 2, label: 'apple' }, + { id: 3, label: 'green apple' } + ]; + const filteredItems: any[] = [ + { id: 2, label: 'apple' }, + { id: 3, label: 'green apple' } + ]; + component.ngSelect.searchTerm = 'apple'; + component.handleInput({ items: filteredItems, term: 'apple' }); + component.onSelectAll(); + expect(component.control.value).toEqual([2, 3]); + }); + + it('should select filtered list with existing items in control value, when filtered and selectAll', () => { + component.bindValue = 'id'; + component.control.patchValue([4]); + component.items = [ + { id: 1, label: 'banana' }, + { id: 2, label: 'apple' }, + { id: 3, label: 'green apple' }, + { id: 4, label: 'grapes' } + ]; + const filteredItems: any[] = [ + { id: 2, label: 'apple' }, + { id: 3, label: 'green apple' } + ]; + component.ngSelect.searchTerm = 'apple'; + component.handleInput({ items: filteredItems, term: 'apple' }); + component.onSelectAll(); + expect(component.control.value.length).toEqual(3); + }); + }); + + describe('onSelectAll() with typeahead', () => { + beforeEach(() => { + component.typeahead = new Subject(); + component.multiple = true; + }); + + it('should store items in previousSelectedItems', () => { + const initialItems: any[] = [ + { id: 1, label: 'banana' }, + { id: 2, label: 'apple' }, + ]; + component.items = initialItems; + component['handleTypeAheadSelectAll'](); + expect(component['previousSelectedItems']).toEqual(initialItems); + }); + + it('should add items in previousSelectedItems', () => { + component.handleItemAdd({ id: 1, label: 'banana' }); + expect(component['previousSelectedItems']).toEqual([ + { id: 1, label: 'banana' }, + ]); + }); + + it('should remove items from previousSelectedItems', () => { + component['previousSelectedItems'] = [{ id: 1, label: 'banana' }]; + component.handleItemRemove({ value: { id: 1, label: 'banana' } }); + expect(component['previousSelectedItems']).toEqual([]); + }); + + it('handleControlInitialValue(), should assign previousSelectedItems', () => { + component.control.patchValue([1]); + component.bindValue = 'id'; + component.items = [ + { id: 1, label: 'banana' }, + { id: 2, label: 'apple' }, + ]; + component['handleControlInitialValue'](); + expect(component['previousSelectedItems']).toEqual([ + { id: 1, label: 'banana' }, + ]); + }); + }); + + describe('processSelectAll', () => { + it('process select all and patch value in form', () => { + component.bindValue = 'id'; + const items: any[] = [ + { id: 1, label: 'banana' }, + { id: 2, label: 'apple' }, + { id: 3, label: 'green apple' }, + { id: 4, label: 'grapes' }, + ]; + + component['processSelectAll'](items); + + expect(component.control.value).toEqual([1, 2, 3, 4]); + }); }); describe('onRemoveAll', () => { it('uses removed the selected values', () => { component.bindValue = 'id'; + spyOn(component, 'resetTypeAheadItems'); component.items = [ { id: 1, label: 'Label 1' }, { id: 2, label: 'Label 2' }, - { id: 3, label: 'Label 3' } + { id: 3, label: 'Label 3' }, ]; component.onSelectAll(); @@ -82,6 +183,7 @@ describe('GoSelectComponent', () => { component.onRemoveAll(); expect(component.control.value).toBeNull(); + expect(component['resetTypeAheadItems']).toHaveBeenCalled(); }); }); }); diff --git a/projects/go-lib/src/lib/components/go-select/go-select.component.ts b/projects/go-lib/src/lib/components/go-select/go-select.component.ts index 9cb67b68a..6de5341bb 100644 --- a/projects/go-lib/src/lib/components/go-select/go-select.component.ts +++ b/projects/go-lib/src/lib/components/go-select/go-select.component.ts @@ -1,6 +1,18 @@ -import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef, ViewEncapsulation } from '@angular/core'; -import { Subject } from 'rxjs'; +import { + Component, + ContentChild, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; import { GoFormBaseComponent } from '../go-form-base/go-form-base.component'; +import { NgSelectComponent } from '@ng-select/ng-select'; @Component({ encapsulation: ViewEncapsulation.None, @@ -8,7 +20,8 @@ import { GoFormBaseComponent } from '../go-form-base/go-form-base.component'; templateUrl: './go-select.component.html', styleUrls: ['./go-select.component.scss'] }) -export class GoSelectComponent extends GoFormBaseComponent implements OnInit { +export class GoSelectComponent extends GoFormBaseComponent implements OnInit, OnDestroy { + @ViewChild(NgSelectComponent) ngSelect: NgSelectComponent; @Input() appendTo: string; @Input() bindLabel: string; @@ -34,22 +47,47 @@ export class GoSelectComponent extends GoFormBaseComponent implements OnInit { @Input() virtualScroll: boolean = false; @Output() scrollToEnd: EventEmitter = new EventEmitter(); - @Output() scroll: EventEmitter<{ start: number, end: number }> = new EventEmitter<{ start: number; end: number }>(); + @Output() scroll: EventEmitter<{ start: number, end: number }> = new EventEmitter<{ start: number, end: number }>(); @ContentChild('goSelectOption') goSelectOption: TemplateRef; @ContentChild('goSelectOptionGroup') goSelectOptionGroup: TemplateRef; @ContentChild('goSelectSelectedOption') goSelectSelectedOption: TemplateRef; + // store refined items after search + refinedItems: any[] = []; + // stores previous selected items when typeahead is enabled only in case of selectAll. + private previousSelectedItems: any[] = []; + private controlSubscription: Subscription; + ngOnInit(): void { this.closeOnSelect = this.multiple ? false : this.closeOnSelect; + this.handleControlInitialValue(); + this.subscribeToControlChanges(); + } + + ngOnDestroy(): void { + this.controlSubscription?.unsubscribe(); } onSelectAll(): void { - this.control.patchValue(this.items.map((item: any) => this.bindValue ? item[this.bindValue] : item)); + if (this.typeahead) { + this.handleTypeAheadSelectAll(); + return; + } + + const items: any[] = this.ngSelect.searchTerm ? this.refinedItems : this.items; + this.processSelectAll(items); + } + + handleInput(search: { term: string; items: any[] }): void { + if (this.multiple) { + this.refinedItems = search.items; + } } onRemoveAll(): void { this.control.reset(); + this.resetTypeAheadItems(); } onScrollToEnd(): void { @@ -61,4 +99,107 @@ export class GoSelectComponent extends GoFormBaseComponent implements OnInit { onScroll($event: { start: number; end: number }): void { this.scroll.emit($event); } + + onClose(): void { + this.emptyRefinedItems(); + } + + // store previous selected items incase of multiple and typeahead. + handleItemAdd(item: any): void { + if (!this.multiple || !this.typeahead) { + return; + } + this.previousSelectedItems.push(item); + } + + // remove item from previous selected items incase of multiple and typeahead. + handleItemRemove(item: any): void { + if (!this.multiple || !this.typeahead ) { + return; + } + + const index: number = this.previousSelectedItems.findIndex((prev: any) => prev[this.bindValue] === item.value[this.bindValue]); + this.previousSelectedItems.splice(index, 1); + } + + private subscribeToControlChanges(): void { + if (this.multiple && this.showSelectAll) { + this.controlSubscription = this.control.valueChanges.subscribe((value: any) => { + this.handleMultipleControlChanges(value); + }); + } + } + + private handleMultipleControlChanges(value: any): void { + this.emptyRefinedItems(); + if (!value?.length) { + this.resetTypeAheadItems(); + } + } + + private handleTypeAheadSelectAll(): void { + // because spread operator is not supported due to tslib version + const items: any[] = JSON.parse(JSON.stringify(this.items)); + for (const previousItem of this.previousSelectedItems) { + const exists: boolean = items.some( + (item: any) => item[this.bindValue] === previousItem[this.bindValue] + ); + if (!exists) { + items.unshift(previousItem); + } + } + this.previousSelectedItems = items; + this.items = items; + this.control.reset([], { emitEvent: false }); + this.processSelectAll(items); + } + + private resetTypeAheadItems(): void { + if (this.typeahead) { + this.items = []; + this.previousSelectedItems = []; + } + } + + private emptyRefinedItems(): void { + if (!this.ngSelect.searchTerm) { + this.refinedItems = []; + } + } + + private processSelectAll(items: any[]): void { + const refinedArr: any[] = items.map((item: any) => + this.bindValue ? item[this.bindValue] : item + ); + + const existing: any[] = Array.isArray(this.control.value) ? this.control.value : []; + const uniq: any[] = Array.from(new Set(existing.concat(refinedArr))); + this.control.patchValue(uniq); + this.ngSelect.searchTerm = ''; + this.ngSelect.itemsList.resetFilteredItems(); + } + + private shouldHandleControlInitialValue(): boolean { + return (this.typeahead || this.multiple) && Array.isArray(this.control.value); + } + + private findItemByValue(value: any): any { + return this.items.find((item: any) => item[this.bindValue] === value); + } + + private handleControlInitialValue(): void { + if (!this.shouldHandleControlInitialValue()) { + return; + } + + const selected: any[] = this.control.value; + + for (const value of selected) { + const exist: any = this.findItemByValue(value); + if (exist) { + this.previousSelectedItems.push(exist); + } + } + } + }