From ff87982c4da7365a88b7e2cf1db207e1f86da728 Mon Sep 17 00:00:00 2001 From: samuelbrowne Date: Thu, 15 Sep 2022 13:39:21 +0930 Subject: [PATCH 1/3] Add support for objects with other properties. --- .../src/multiselect.component.ts | 105 ++++++++++-------- .../src/multiselect.model.ts | 2 + 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/ng-multiselect-dropdown/src/multiselect.component.ts b/src/ng-multiselect-dropdown/src/multiselect.component.ts index 66aa260..5db82b2 100644 --- a/src/ng-multiselect-dropdown/src/multiselect.component.ts +++ b/src/ng-multiselect-dropdown/src/multiselect.component.ts @@ -1,19 +1,20 @@ -import { Component, HostListener, forwardRef, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from "@angular/core"; -import { NG_VALUE_ACCESSOR, ControlValueAccessor } from "@angular/forms"; -import { ListItem, IDropdownSettings } from "./multiselect.model"; -import { ListFilterPipe } from "./list-filter.pipe"; +import {Component, HostListener, forwardRef, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef} from '@angular/core'; +import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; +import {ListItem, IDropdownSettings} from './multiselect.model'; +import {ListFilterPipe} from './list-filter.pipe'; export const DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MultiSelectComponent), multi: true }; -const noop = () => {}; +const noop = () => { +}; @Component({ - selector: "ng-multiselect-dropdown", - templateUrl: "./multi-select.component.html", - styleUrls: ["./multi-select.component.scss"], + selector: 'ng-multiselect-dropdown', + templateUrl: './multi-select.component.html', + styleUrls: ['./multi-select.component.scss'], providers: [DROPDOWN_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -22,26 +23,26 @@ export class MultiSelectComponent implements ControlValueAccessor { public _data: Array = []; public selectedItems: Array = []; public isDropdownOpen = true; - _placeholder = "Select"; + _placeholder = 'Select'; private _sourceDataType = null; // to keep note of the source data type. could be array of string/number/object private _sourceDataFields: Array = []; // store source data fields names filter: ListItem = new ListItem(this.data); defaultSettings: IDropdownSettings = { singleSelection: false, - idField: "id", - textField: "text", - disabledField: "isDisabled", + idField: 'id', + textField: 'text', + disabledField: 'isDisabled', enableCheckAll: true, - selectAllText: "Select All", - unSelectAllText: "UnSelect All", + selectAllText: 'Select All', + unSelectAllText: 'UnSelect All', allowSearchFilter: false, limitSelection: -1, clearSearchFilter: true, maxHeight: 197, itemsShowLimit: 999999999999, - searchPlaceholderText: "Search", - noDataAvailablePlaceholderText: "No data available", - noFilteredDataAvailablePlaceholderText: "No filtered data available", + searchPlaceholderText: 'Search', + noDataAvailablePlaceholderText: 'No data available', + noFilteredDataAvailablePlaceholderText: 'No filtered data available', closeDropDownOnSelection: false, showSelectedItemsAtTop: false, defaultOpen: false, @@ -53,9 +54,10 @@ export class MultiSelectComponent implements ControlValueAccessor { if (value) { this._placeholder = value; } else { - this._placeholder = "Select"; + this._placeholder = 'Select'; } } + @Input() disabled = false; @@ -76,33 +78,35 @@ export class MultiSelectComponent implements ControlValueAccessor { const firstItem = value[0]; this._sourceDataType = typeof firstItem; this._sourceDataFields = this.getFields(firstItem); + this._data = value.map((item: any) => - typeof item === "string" || typeof item === "number" + typeof item === 'string' || typeof item === 'number' ? new ListItem(item) : new ListItem({ - id: item[this._settings.idField], - text: item[this._settings.textField], - isDisabled: item[this._settings.disabledField] - }) - ); + id: item[this._settings.idField], + text: item[this._settings.textField], + sourceObj: {...this.getSourceObj(item)}, // keep other object props + isDisabled: item[this._settings.disabledField] + }) + ); } } - @Output("onFilterChange") + @Output('onFilterChange') onFilterChange: EventEmitter = new EventEmitter(); - @Output("onDropDownClose") + @Output('onDropDownClose') onDropDownClose: EventEmitter = new EventEmitter(); - @Output("onSelect") + @Output('onSelect') onSelect: EventEmitter = new EventEmitter(); - @Output("onDeSelect") + @Output('onDeSelect') onDeSelect: EventEmitter = new EventEmitter(); - @Output("onSelectAll") + @Output('onSelectAll') onSelectAll: EventEmitter> = new EventEmitter>(); - @Output("onDeSelectAll") + @Output('onDeSelectAll') onDeSelectAll: EventEmitter> = new EventEmitter>(); private onTouchedCallback: () => void = noop; @@ -113,7 +117,7 @@ export class MultiSelectComponent implements ControlValueAccessor { } constructor( - private listFilterPipe:ListFilterPipe, + private listFilterPipe: ListFilterPipe, private cdr: ChangeDetectorRef ) {} @@ -143,13 +147,13 @@ export class MultiSelectComponent implements ControlValueAccessor { if (value.length >= 1) { const firstItem = value[0]; this.selectedItems = [ - typeof firstItem === "string" || typeof firstItem === "number" + typeof firstItem === 'string' || typeof firstItem === 'number' ? new ListItem(firstItem) : new ListItem({ - id: firstItem[this._settings.idField], - text: firstItem[this._settings.textField], - isDisabled: firstItem[this._settings.disabledField] - }) + id: firstItem[this._settings.idField], + text: firstItem[this._settings.textField], + isDisabled: firstItem[this._settings.disabledField] + }) ]; } } catch (e) { @@ -157,13 +161,13 @@ export class MultiSelectComponent implements ControlValueAccessor { } } else { const _data = value.map((item: any) => - typeof item === "string" || typeof item === "number" + typeof item === 'string' || typeof item === 'number' ? new ListItem(item) : new ListItem({ - id: item[this._settings.idField], - text: item[this._settings.textField], - isDisabled: item[this._settings.disabledField] - }) + id: item[this._settings.idField], + text: item[this._settings.textField], + isDisabled: item[this._settings.disabledField] + }) ); if (this._settings.limitSelection > 0) { this.selectedItems = _data.splice(0, this._settings.limitSelection); @@ -190,7 +194,7 @@ export class MultiSelectComponent implements ControlValueAccessor { } // Set touched on blur - @HostListener("blur") + @HostListener('blur') public onTouched() { // this.closeDropdown(); this.onTouchedCallback(); @@ -216,7 +220,7 @@ export class MultiSelectComponent implements ControlValueAccessor { isAllItemsSelected(): boolean { // get disabld item count - let filteredItems = this.listFilterPipe.transform(this._data,this.filter); + let filteredItems = this.listFilterPipe.transform(this._data, this.filter); const itemDisabledCount = filteredItems.filter(item => item.isDisabled).length; // take disabled items into consideration when checking if ((!this.data || this.data.length === 0) && this._settings.allowRemoteDataSearch) { @@ -279,7 +283,7 @@ export class MultiSelectComponent implements ControlValueAccessor { objectify(val: ListItem) { if (this._sourceDataType === 'object') { - const obj = {}; + const obj = {...val.sourceObj}; obj[this._settings.idField] = val.id; obj[this._settings.textField] = val.text; if (this._sourceDataFields.includes(this._settings.disabledField)) { @@ -309,7 +313,7 @@ export class MultiSelectComponent implements ControlValueAccessor { this._settings.defaultOpen = false; // clear search text if (this._settings.clearSearchFilter) { - this.filter.text = ""; + this.filter.text = ''; } this.onDropDownClose.emit(); } @@ -320,7 +324,7 @@ export class MultiSelectComponent implements ControlValueAccessor { } if (!this.isAllItemsSelected()) { // filter out disabled item first before slicing - this.selectedItems = this.listFilterPipe.transform(this._data,this.filter).filter(item => !item.isDisabled).slice(); + this.selectedItems = this.listFilterPipe.transform(this._data, this.filter).filter(item => !item.isDisabled).slice(); this.onSelectAll.emit(this.emittedValue(this.selectedItems)); } else { this.selectedItems = []; @@ -331,7 +335,7 @@ export class MultiSelectComponent implements ControlValueAccessor { getFields(inputData) { const fields = []; - if (typeof inputData !== "object") { + if (typeof inputData !== 'object') { return fields; } // tslint:disable-next-line:forin @@ -341,4 +345,11 @@ export class MultiSelectComponent implements ControlValueAccessor { return fields; } + getSourceObj(value: {}) { + const omitProps = [this._settings.idField, this._settings.textField, this._settings.disabledField]; + const obj = Object.assign({}, value); + omitProps.forEach(prop => delete obj[prop]); + return obj; + } + } diff --git a/src/ng-multiselect-dropdown/src/multiselect.model.ts b/src/ng-multiselect-dropdown/src/multiselect.model.ts index 5f77d3e..ac475ca 100644 --- a/src/ng-multiselect-dropdown/src/multiselect.model.ts +++ b/src/ng-multiselect-dropdown/src/multiselect.model.ts @@ -24,6 +24,7 @@ export class ListItem { id: String | number; text: String | number; isDisabled?: boolean; + sourceObj?: {} | undefined; public constructor(source: any) { if (typeof source === 'string' || typeof source === 'number') { @@ -33,6 +34,7 @@ export class ListItem { if (typeof source === 'object') { this.id = source.id; this.text = source.text; + this.sourceObj = source.sourceObj; this.isDisabled = source.isDisabled; } } From e2f1780893979dac846f5e2268d76b0e411f6711 Mon Sep 17 00:00:00 2001 From: samuelbrowne Date: Thu, 15 Sep 2022 13:43:06 +0930 Subject: [PATCH 2/3] adding properties to demo data --- src/app/components/select/multiple-demo.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/components/select/multiple-demo.ts b/src/app/components/select/multiple-demo.ts index 2edd20f..afa5d6a 100644 --- a/src/app/components/select/multiple-demo.ts +++ b/src/app/components/select/multiple-demo.ts @@ -112,8 +112,8 @@ export class MultipleDemoComponent implements OnInit { ngOnInit() { this.cities = [ - { item_id: 1, item_text: 'New Delhi' }, - { item_id: 2, item_text: 'Mumbai' }, + { item_id: 1, item_text: 'New Delhi', poulation: 11_034_555 }, + { item_id: 2, item_text: 'Mumbai', timezone: 'India Standard Time' }, { item_id: 3, item_text: 'Bangalore', isDisabled: this.disableBangalore }, { item_id: 4, item_text: 'Pune' }, { item_id: 5, item_text: 'Chennai' }, @@ -191,7 +191,6 @@ export class MultipleDemoComponent implements OnInit { itemsShowLimit: 999999 }); } - console.log() } From 2f47e8b9c1b9a29217b603a888cf8d108295883f Mon Sep 17 00:00:00 2001 From: samuelbrowne Date: Thu, 15 Sep 2022 13:43:06 +0930 Subject: [PATCH 3/3] - Adding unit tests for custom property functionality. issue#172 - Adding custom properties to demo data - changing use of the deconstruct/spread operator (...{}), in MultiSelectComponent.getSourceObj to use Object.assign(), instead. --- src/app/components/select/multiple-demo.ts | 11 ++- .../src/multiselect.component.ts | 4 +- ...lti-select.component3-custom-props.spec.ts | 73 +++++++++++++++++++ 3 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 src/ng-multiselect-dropdown/test/multi-select.component3-custom-props.spec.ts diff --git a/src/app/components/select/multiple-demo.ts b/src/app/components/select/multiple-demo.ts index 2edd20f..156ab54 100644 --- a/src/app/components/select/multiple-demo.ts +++ b/src/app/components/select/multiple-demo.ts @@ -52,9 +52,9 @@ export class MultipleDemoComponent implements OnInit { ngOnInit() { this.cities = [ - { item_id: 1, item_text: 'New Delhi' }, - { item_id: 2, item_text: 'Mumbai' }, - { item_id: 3, item_text: 'Bangalore' }, + { item_id: 1, item_text: 'New Delhi', population: 1_173_902 }, + { item_id: 2, item_text: 'Mumbai', timezone: 'India Standard Time' }, + { item_id: 3, item_text: 'Bangalore', isDisabled: this.disableBangalore }, { item_id: 4, item_text: 'Pune' }, { item_id: 5, item_text: 'Chennai' }, { item_id: 6, item_text: 'Navsari' } @@ -112,8 +112,8 @@ export class MultipleDemoComponent implements OnInit { ngOnInit() { this.cities = [ - { item_id: 1, item_text: 'New Delhi' }, - { item_id: 2, item_text: 'Mumbai' }, + { item_id: 1, item_text: 'New Delhi', population: 1_173_902 }, + { item_id: 2, item_text: 'Mumbai', timezone: 'India Standard Time' }, { item_id: 3, item_text: 'Bangalore', isDisabled: this.disableBangalore }, { item_id: 4, item_text: 'Pune' }, { item_id: 5, item_text: 'Chennai' }, @@ -191,7 +191,6 @@ export class MultipleDemoComponent implements OnInit { itemsShowLimit: 999999 }); } - console.log() } diff --git a/src/ng-multiselect-dropdown/src/multiselect.component.ts b/src/ng-multiselect-dropdown/src/multiselect.component.ts index 5db82b2..bab8215 100644 --- a/src/ng-multiselect-dropdown/src/multiselect.component.ts +++ b/src/ng-multiselect-dropdown/src/multiselect.component.ts @@ -85,7 +85,7 @@ export class MultiSelectComponent implements ControlValueAccessor { : new ListItem({ id: item[this._settings.idField], text: item[this._settings.textField], - sourceObj: {...this.getSourceObj(item)}, // keep other object props + sourceObj: Object.assign({}, this.getSourceObj(item)), // keep other object props isDisabled: item[this._settings.disabledField] }) ); @@ -283,7 +283,7 @@ export class MultiSelectComponent implements ControlValueAccessor { objectify(val: ListItem) { if (this._sourceDataType === 'object') { - const obj = {...val.sourceObj}; + const obj = Object.assign({}, val.sourceObj); obj[this._settings.idField] = val.id; obj[this._settings.textField] = val.text; if (this._sourceDataFields.includes(this._settings.disabledField)) { diff --git a/src/ng-multiselect-dropdown/test/multi-select.component3-custom-props.spec.ts b/src/ng-multiselect-dropdown/test/multi-select.component3-custom-props.spec.ts new file mode 100644 index 0000000..40e3cfa --- /dev/null +++ b/src/ng-multiselect-dropdown/test/multi-select.component3-custom-props.spec.ts @@ -0,0 +1,73 @@ +import {Component, ViewChild} from '@angular/core'; +import {ComponentFixture, fakeAsync} from '@angular/core/testing'; +import {MultiSelectComponent, IDropdownSettings} from './../src'; +import {createTestingModule} from './helper'; +import {ListItem} from 'ng-multiselect-dropdown/multiselect.model'; + + +@Component({ + template: `` +}) +class Ng2MultiSelectDropdownMultipleSelect { + @ViewChild(MultiSelectComponent, {static: false}) select: MultiSelectComponent; + cities = [ + {item_id: 0, item_text: 'Navsari'}, + {item_id: 1, item_text: 'Mumbai', population: 20_961_000}, + {item_id: 2, item_text: 'Bangalore', timezone: 'India Standard Time', population: 13_193_000}, + {item_id: 3, item_text: 'Pune', }, + {item_id: 5, item_text: 'New Delhi'} + ]; + selectedItem = [{item_id: 0, item_text: 'Navsari'}]; + disabled: false; + dropdownSettings: IDropdownSettings = { + singleSelection: false, + idField: 'item_id', + textField: 'item_text', + selectAllText: 'Select All', + unSelectAllText: 'UnSelect All', + allowSearchFilter: true, + closeDropDownOnSelection: true, + }; +} + +/** + * @see https://github.com/NileshPatel17/ng-multiselect-dropdown/issues/172 + */ +describe('ng-multiselect-component: Issue#172 - custom properties support', function () { + describe('multi-select', function () { + let fixture: ComponentFixture; + beforeEach( + fakeAsync(() => { + fixture = createTestingModule( + Ng2MultiSelectDropdownMultipleSelect, + `
+ + +
`); + }) + ); + + it('getSourceObj() should return an new object instance with the omitted "item_id", "item_text", and "disabled" properties', () => { + const mockObject = {item_id: 2, item_text: 'Bangalore', timezone: 'India Standard Time', population: 13_193_000}; + const expected = {timezone: 'India Standard Time', population: 13_193_000}; + + const result = fixture.componentInstance.select.getSourceObj(mockObject); + + expect(result).toEqual(expected); + expect(Object.keys(mockObject).length).toEqual(4); // check original object is not mutated + }); + + it('objectify() should return an new object instance with the custom properties included', () => { + const mockObject = {id: 2, text: 'Bangalore', sourceObj: {timezone: 'India Standard Time', population: 13_193_000}}; + const expected = {item_id: 2, item_text: 'Bangalore', timezone: 'India Standard Time', population: 13_193_000}; + + const result = fixture.componentInstance.select.objectify(mockObject as ListItem); + + expect(result).toEqual(expected); + }); + }); +}); +