diff --git a/src/app/examples/custom-angularComponentFilter.ts b/src/app/examples/custom-angularComponentFilter.ts index a736f9e85..eda20e959 100644 --- a/src/app/examples/custom-angularComponentFilter.ts +++ b/src/app/examples/custom-angularComponentFilter.ts @@ -62,7 +62,7 @@ export class CustomAngularComponentFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; if (!this.columnFilter || !this.columnFilter.params.component || !(this.angularUtilService instanceof AngularUtilService)) { throw new Error(`[Angular-Slickgrid] For Filter with Angular Component to work properly, you need to provide your component to the "component" property and make sure to add it to your "entryComponents" array. diff --git a/src/app/examples/custom-inputFilter.ts b/src/app/examples/custom-inputFilter.ts index 8d4e71f0b..f7e61d303 100644 --- a/src/app/examples/custom-inputFilter.ts +++ b/src/app/examples/custom-inputFilter.ts @@ -42,10 +42,10 @@ export class CustomInputFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // filter input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create HTML string template const filterTemplate = this.buildTemplateHtmlString(); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/compoundInputFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/compoundInputFilter.spec.ts index f29604fa2..200cf1e67 100644 --- a/src/app/modules/angular-slickgrid/filters/__tests__/compoundInputFilter.spec.ts +++ b/src/app/modules/angular-slickgrid/filters/__tests__/compoundInputFilter.spec.ts @@ -190,6 +190,17 @@ describe('CompoundInputFilter', () => { expect(filterInputElm.value).toBe('xyz'); }); + it('should expect the input not to have the "filled" css class when the search term provided is an empty string', () => { + filterArguments.searchTerms = ['']; + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-duration input'); + const filterFilledElms = divContainer.querySelectorAll('.search-filter.filter-duration.filled'); + + expect(filterInputElm.value).toBe(''); + expect(filterFilledElms.length).toBe(0); + }); + it('should create the input filter with operator dropdown options related to numbers when column definition type is FieldType.number', () => { mockColumn.type = FieldType.number; filterArguments.searchTerms = ['9']; diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/inputFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/inputFilter.spec.ts index f4bcdd8a9..adab7e64e 100644 --- a/src/app/modules/angular-slickgrid/filters/__tests__/inputFilter.spec.ts +++ b/src/app/modules/angular-slickgrid/filters/__tests__/inputFilter.spec.ts @@ -139,6 +139,17 @@ describe('InputFilter', () => { expect(filterElm.value).toBe('xyz'); }); + it('should expect the input not to have the "filled" css class when the search term provided is an empty string', () => { + filterArguments.searchTerms = ['']; + + filter.init(filterArguments); + const filterElm = divContainer.querySelector('input.filter-duration'); + const filterFilledElms = divContainer.querySelectorAll('input.filter-duration.filled'); + + expect(filterElm.value).toBe(''); + expect(filterFilledElms.length).toBe(0); + }); + it('should trigger a callback with the clear filter set when calling the "clear" method', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); filterArguments.searchTerms = ['xyz']; @@ -148,7 +159,6 @@ describe('InputFilter', () => { const filterElm = divContainer.querySelector('input.filter-duration'); const filterFilledElms = divContainer.querySelectorAll('input.filter-duration.filled'); - expect(filterElm.value).toBe(''); expect(filterFilledElms.length).toBe(0); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/nativeSelectFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/nativeSelectFilter.spec.ts new file mode 100644 index 000000000..f650cdcd4 --- /dev/null +++ b/src/app/modules/angular-slickgrid/filters/__tests__/nativeSelectFilter.spec.ts @@ -0,0 +1,441 @@ +import { TestBed } from '@angular/core/testing'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { Column, FilterArguments, GridOption, FieldType, OperatorType } from '../../models'; +import { CollectionService } from './../../services/collection.service'; +import { Filters } from '..'; +import { NativeSelectFilter } from '../nativeSelectFilter'; +import { of, Subject } from 'rxjs'; + +const containerId = 'demo-container'; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), +}; + +describe('NativeSelectFilter', () => { + let divContainer: HTMLDivElement; + let filter: NativeSelectFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let collectionService: CollectionService; + let translate: TranslateService; + + beforeEach(() => { + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { + id: 'gender', field: 'gender', filterable: true, + filter: { + model: Filters.select, + } + }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + TestBed.configureTestingModule({ + providers: [CollectionService], + imports: [TranslateModule.forRoot()] + }); + collectionService = TestBed.get(CollectionService); + translate = TestBed.get(TranslateService); + + translate.setTranslation('en', { + ALL_SELECTED: 'All Selected', + FEMALE: 'Female', + MALE: 'Male', + OK: 'OK', + OTHER: 'Other', + SELECT_ALL: 'Select All', + X_OF_Y_SELECTED: '# of % selected', + }); + translate.setTranslation('fr', { + ALL_SELECTED: 'Tout sélectionnés', + FEMALE: 'Femme', + MALE: 'Mâle', + OK: 'Terminé', + OTHER: 'Autre', + SELECT_ALL: 'Sélectionner tout', + X_OF_Y_SELECTED: '# de % sélectionnés', + }); + translate.setDefaultLang('en'); + + filter = new NativeSelectFilter(translate); + }); + + afterEach(() => { + filter.destroy(); + }); + + it('should throw an error when trying to call init without any arguments', () => { + expect(() => filter.init(null)).toThrowError('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.'); + }); + + it('should throw an error when there is no collection provided in the filter property', (done) => { + try { + mockColumn.filter.collection = undefined; + filter.init(filterArguments); + } catch (e) { + expect(e.toString()).toContain(`[Angular-SlickGrid] You need to pass a "collection" for the Native Select Filter to work correctly.`); + done(); + } + }); + + it('should throw an error when collection is not a valid array', (done) => { + try { + // @ts-ignore + mockColumn.filter.collection = { hello: 'world' }; + filter.init(filterArguments); + } catch (e) { + expect(e.toString()).toContain(`The "collection" passed to the Native Select Filter is not a valid array.`); + done(); + } + }); + + it('should throw an error when collection is not a valid value/label pair array', (done) => { + try { + mockColumn.filter.collection = [{ hello: 'world' }]; + filter.init(filterArguments); + } catch (e) { + expect(e.toString()).toContain(`A collection with value/label (or value/labelKey when using Locale) is required to populate the Native Select Filter list`); + done(); + } + }); + + it('should throw an error when "enableTranslateLabel" is set without a valid TranslateService', (done) => { + try { + translate = undefined; + mockColumn.filter.enableTranslateLabel = true; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter = new NativeSelectFilter(translate); + + filter.init(filterArguments); + } catch (e) { + expect(e.toString()).toContain(`The ngx-translate TranslateService is required for the Native Select Filter to work correctly when "enableTranslateLabel" is set.`); + done(); + } + }); + + it('should initialize the filter', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments); + const filterCount = divContainer.querySelectorAll('select.form-control.search-filter.filter-gender').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + }); + + it('should trigger select change event and expect the callback to be called with the search terms we select from dropdown list', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.value = 'female'; + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should trigger select change event and expect this to work with a regular array of strings', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + mockColumn.filter.collection = ['male', 'female']; + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.value = 'female'; + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should trigger select change event and expect the callback to be called with numbers converted as string in the option values', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = [{ value: 1, label: 'male' }, { value: 2, label: 'female' }]; + + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.value = '2'; + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['2'], shouldTriggerQuery: true }); + }); + + it('should trigger select change event and expect the callback to be called with booleans converted as string in the option values', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = [{ value: true, label: 'True' }, { value: false, label: 'False' }]; + + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.value = 'false'; + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['false'], shouldTriggerQuery: true }); + }); + + it('should pass a different operator then trigger an input change event and expect the callback to be called with the search terms we select from dropdown list', () => { + mockColumn.filter.operator = 'NE'; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.value = 'female'; + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'NE', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should have same value in "getValues" after being set in "setValues" with a single value', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments); + filter.setValues('female'); + const values = filter.getValues(); + + expect(values).toEqual(['female']); + expect(values.length).toBe(1); + }); + + it('should have same value in "getValues" after being set in "setValues" with an array having a single value', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments); + filter.setValues(['female']); + const values = filter.getValues(); + + expect(values).toEqual(['female']); + expect(values.length).toBe(1); + }); + + it('should have empty array returned from "getValues" when nothing is set', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments); + const values = filter.getValues(); + + expect(values).toEqual([]); + expect(values.length).toBe(0); + }); + + it('should have empty array returned from "getValues" even when filter is not yet created', () => { + const values = filter.getValues(); + + expect(values).toEqual([]); + expect(values.length).toBe(0); + }); + + it('should create the select filter with "customStructure" with a default search term when passed as a filter argument', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter = { + collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }], + customStructure: { + value: 'value', + label: 'description', + }, + }; + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('other'); + expect(filterListElm[1].textContent).toBe('male'); + expect(filterListElm[2].textContent).toBe('female'); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should create the select filter with a default search term when passed as a filter argument', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should create the select filter with empty search term when passed an empty string as a filter argument and not expect "filled" css class either', () => { + mockColumn.filter.collection = [{ value: '', label: '' }, { value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = ['']; + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(3); + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true }); + }); + + it('should create the select filter with a default boolean search term that is converted to strings as option values and pre-selected as option', () => { + mockColumn.filter.collection = [{ value: true, label: 'True' }, { value: false, label: 'False' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = [false]; + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['false'], shouldTriggerQuery: true }); + }); + + it('should create the select filter with a default number search term that is converted to strings as option values and pre-selected as option', () => { + mockColumn.filter.collection = [{ value: 1, label: 'male' }, { value: 2, label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = [2]; + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector(`select.search-filter.filter-gender`); + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + filterSelectElm.dispatchEvent(new CustomEvent('change')); + + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['2'], shouldTriggerQuery: true }); + }); + + it('should trigger a callback with the clear filter set when calling the "clear" method', () => { + filterArguments.searchTerms = ['female']; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.clear(); + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + + expect(filter.searchTerms.length).toBe(0); + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); + }); + + it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments); + filter.clear(false); + const filterFilledElms = divContainer.querySelectorAll('select.search-filter.filter-gender.filled'); + + expect(filter.searchTerms.length).toBe(0); + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + }); + + it('should work with English locale when locale is changed', (done) => { + translate.use('en'); + gridOptionMock.enableTranslate = true; + mockColumn.filter = { + enableTranslateLabel: true, + collection: [ + { value: 'other', labelKey: 'OTHER' }, + { value: 'male', labelKey: 'MALE' }, + { value: 'female', labelKey: 'FEMALE' } + ], + filterOptions: { minimumCountSelected: 1 } + }; + + filterArguments.searchTerms = ['male', 'female']; + filter.init(filterArguments); + + setTimeout(() => { + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('Other'); + expect(filterListElm[1].textContent).toBe('Male'); + expect(filterListElm[2].textContent).toBe('Female'); + done(); + }); + }); + + it('should work with French locale when locale is changed', (done) => { + translate.use('fr'); + gridOptionMock.enableTranslate = true; + mockColumn.filter = { + enableTranslateLabel: true, + collection: [ + { value: 'other', labelKey: 'OTHER' }, + { value: 'male', labelKey: 'MALE' }, + { value: 'female', labelKey: 'FEMALE' } + ], + filterOptions: { minimumCountSelected: 1 } + }; + + filterArguments.searchTerms = ['male', 'female']; + filter.init(filterArguments); + setTimeout(() => { + const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('Autre'); + expect(filterListElm[1].textContent).toBe('Mâle'); + expect(filterListElm[2].textContent).toBe('Femme'); + done(); + }); + }); +}); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts index e2821413a..ca121a070 100644 --- a/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts +++ b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts @@ -272,6 +272,44 @@ describe('SelectFilter', () => { expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); }); + it('should create the multi-select filter with default boolean search term converted as strings when passed as a filter argument', () => { + mockColumn.filter.collection = [{ value: true, label: 'True' }, { value: false, label: 'False' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = [false]; + filter.init(filterArguments, true); + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + filterBtnElm.click(); + filterOkElm.click(); + + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['false'], shouldTriggerQuery: true }); + }); + + it('should create the multi-select filter with default number search term converted as strings when passed as a filter argument', () => { + mockColumn.filter.collection = [{ value: 1, label: 'male' }, { value: 2, label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = [2]; + filter.init(filterArguments, true); + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + filterBtnElm.click(); + filterOkElm.click(); + + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['2'], shouldTriggerQuery: true }); + }); + it('should create the multi-select filter with a default search term when passed as a filter argument even with collection an array of strings', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); mockColumn.filter.collection = ['male', 'female']; diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/singleSelectFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/singleSelectFilter.spec.ts index b340ae896..26471ff26 100644 --- a/src/app/modules/angular-slickgrid/filters/__tests__/singleSelectFilter.spec.ts +++ b/src/app/modules/angular-slickgrid/filters/__tests__/singleSelectFilter.spec.ts @@ -90,6 +90,18 @@ describe('SingleSelectFilter', () => { expect(filter.isMultipleSelect).toBe(false); }); + it('should create the select filter with empty search term when passed an empty string as a filter argument and not expect "filled" css class either', () => { + mockColumn.filter.collection = [{ value: '', label: '' }, { value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + + filterArguments.searchTerms = ['']; + filter.init(filterArguments, true); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=radio]`); + + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(3); + expect(filterFilledElms.length).toBe(0); + }); + it('should trigger single select change event and expect the callback to be called when we select a single search term from dropdown list', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; diff --git a/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts b/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts index 517d56782..a82e6435f 100644 --- a/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts @@ -90,7 +90,7 @@ export class AutoCompleteFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; if (!this.grid || !this.columnDef || !this.columnFilter || (!this.columnFilter.collection && !this.columnFilter.collectionAsync && !this.columnFilter.filterOptions)) { throw new Error(`[Angular-SlickGrid] You need to pass a "collection" (or "collectionAsync") for the AutoComplete Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.autoComplete, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); @@ -244,7 +244,7 @@ export class AutoCompleteFilter implements Filter { newCollection = this.sortCollection(newCollection); // filter input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create HTML string template const filterTemplate = this.buildTemplateHtmlString(); diff --git a/src/app/modules/angular-slickgrid/filters/compoundDateFilter.ts b/src/app/modules/angular-slickgrid/filters/compoundDateFilter.ts index 47aa5466e..ca9524e4e 100644 --- a/src/app/modules/angular-slickgrid/filters/compoundDateFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/compoundDateFilter.ts @@ -69,10 +69,10 @@ export class CompoundDateFilter implements Filter { this.callback = args.callback; this.columnDef = args.columnDef; this.operator = args.operator || ''; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // date input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create the DOM Element of the filter which contain the compound Operator+Input // and initialize it if searchTerm is filled diff --git a/src/app/modules/angular-slickgrid/filters/compoundInputFilter.ts b/src/app/modules/angular-slickgrid/filters/compoundInputFilter.ts index 3525d376c..37f99387f 100644 --- a/src/app/modules/angular-slickgrid/filters/compoundInputFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/compoundInputFilter.ts @@ -75,13 +75,13 @@ export class CompoundInputFilter implements Filter { this.callback = args.callback; this.columnDef = args.columnDef; this.operator = args.operator; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // get locales provided by user in forRoot or else use default English locales via the Constants this._locales = this.gridOptions && this.gridOptions.locales || Constants.locales; // filter input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create the DOM Element of the filter which contain the compound Operator+Input // and initialize it if searchTerms is filled diff --git a/src/app/modules/angular-slickgrid/filters/compoundSliderFilter.ts b/src/app/modules/angular-slickgrid/filters/compoundSliderFilter.ts index 4baf0bc9e..7cd3bd54f 100644 --- a/src/app/modules/angular-slickgrid/filters/compoundSliderFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/compoundSliderFilter.ts @@ -66,14 +66,14 @@ export class CompoundSliderFilter implements Filter { this.callback = args.callback; this.columnDef = args.columnDef; this.operator = args.operator || ''; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // define the input & slider number IDs this._elementRangeInputId = `rangeInput_${this.columnDef.field}`; this._elementRangeOutputId = `rangeOutput_${this.columnDef.field}`; // filter input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create the DOM Element of the filter which contain the compound Operator+Input // and initialize it if searchTerm is filled diff --git a/src/app/modules/angular-slickgrid/filters/dateRangeFilter.ts b/src/app/modules/angular-slickgrid/filters/dateRangeFilter.ts index 487b9d90b..0a8f68205 100644 --- a/src/app/modules/angular-slickgrid/filters/dateRangeFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/dateRangeFilter.ts @@ -79,7 +79,7 @@ export class DateRangeFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // step 1, create the DOM Element of the filter which contain the compound Operator+Input this.$filterElm = this.createDomElement(this.searchTerms); diff --git a/src/app/modules/angular-slickgrid/filters/inputFilter.ts b/src/app/modules/angular-slickgrid/filters/inputFilter.ts index bbcd7adc8..8e652e0ac 100644 --- a/src/app/modules/angular-slickgrid/filters/inputFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/inputFilter.ts @@ -60,10 +60,10 @@ export class InputFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // filter input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create HTML string template const filterTemplate = this.buildTemplateHtmlString(); diff --git a/src/app/modules/angular-slickgrid/filters/inputMaskFilter.ts b/src/app/modules/angular-slickgrid/filters/inputMaskFilter.ts index c344a6a5b..b9968c19f 100644 --- a/src/app/modules/angular-slickgrid/filters/inputMaskFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/inputMaskFilter.ts @@ -25,7 +25,7 @@ export class InputMaskFilter extends InputFilter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // get input mask from params (can be in columnDef or columnFilter params) if (this.columnDef && this.columnDef.params && this.columnDef.params.mask) { @@ -40,7 +40,7 @@ export class InputMaskFilter extends InputFilter { } // filter input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create HTML string template const filterTemplate = this.buildTemplateHtmlString(); diff --git a/src/app/modules/angular-slickgrid/filters/nativeSelectFilter.ts b/src/app/modules/angular-slickgrid/filters/nativeSelectFilter.ts index 07671fc63..dfae9929a 100644 --- a/src/app/modules/angular-slickgrid/filters/nativeSelectFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/nativeSelectFilter.ts @@ -1,9 +1,11 @@ import { TranslateService } from '@ngx-translate/core'; import { Column, + ColumnFilter, Filter, FilterArguments, FilterCallback, + GridOption, OperatorType, OperatorString, SearchTerm, @@ -16,6 +18,7 @@ declare var $: any; export class NativeSelectFilter implements Filter { private _clearFilterTriggered = false; private _shouldTriggerQuery = true; + private _currentValues: any | any[] = []; $filterElm: any; grid: any; searchTerms: SearchTerm[]; @@ -24,6 +27,16 @@ export class NativeSelectFilter implements Filter { constructor(@Optional() private translate: TranslateService) { } + /** Getter for the Column Filter itself */ + protected get columnFilter(): ColumnFilter { + return this.columnDef && this.columnDef.filter; + } + + /** Getter for the Grid Options pulled through the Grid Object */ + protected get gridOptions(): GridOption { + return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; + } + get operator(): OperatorType | OperatorString { return (this.columnDef && this.columnDef.filter && this.columnDef.filter.operator) || OperatorType.equal; } @@ -32,13 +45,24 @@ export class NativeSelectFilter implements Filter { * Initialize the Filter */ init(args: FilterArguments) { + if (!args) { + throw new Error('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.'); + } this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; + + if (!this.grid || !this.columnDef || !this.columnFilter || !this.columnFilter.collection) { + throw new Error(`[Angular-SlickGrid] You need to pass a "collection" for the Native Select Filter to work correctly.`); + } + + if (this.columnFilter.enableTranslateLabel && !this.gridOptions.enableTranslate && (!this.translate || typeof this.translate.instant !== 'function')) { + throw new Error(`The ngx-translate TranslateService is required for the Native Select Filter to work correctly when "enableTranslateLabel" is set.`); + } // filter input can only have 1 search term, so we will use the 1st array index if it exist - let searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + let searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; if (typeof searchTerm === 'boolean' || typeof searchTerm === 'number') { searchTerm = `${searchTerm}`; } @@ -53,6 +77,8 @@ export class NativeSelectFilter implements Filter { // also add/remove "filled" class for styling purposes this.$filterElm.change((e: any) => { const value = e && e.target && e.target.value || ''; + this._currentValues = [value]; + if (this._clearFilterTriggered) { this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); this.$filterElm.removeClass('filled'); @@ -74,6 +100,7 @@ export class NativeSelectFilter implements Filter { this._clearFilterTriggered = true; this._shouldTriggerQuery = shouldTriggerQuery; this.searchTerms = []; + this._currentValues = []; this.$filterElm.val(''); this.$filterElm.trigger('change'); } @@ -88,12 +115,24 @@ export class NativeSelectFilter implements Filter { } } + /** + * Get selected values retrieved from the multiple-selected element + * @params selected items + */ + getValues(): any[] { + return this._currentValues; + } + /** * Set value(s) on the DOM element */ setValues(values: SearchTerm | SearchTerm[]) { - if (values) { + if (Array.isArray(values)) { + this.$filterElm.val(values[0]); + this._currentValues = values; + } else if (values) { this.$filterElm.val(values); + this._currentValues = [values]; } } @@ -102,26 +141,25 @@ export class NativeSelectFilter implements Filter { // ------------------ private buildTemplateHtmlString() { - if (!this.columnDef || !this.columnDef.filter || !this.columnDef.filter.collection) { - throw new Error(`[Angular-SlickGrid] You need to pass a "collection" for the Select Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.select, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); + const collection = this.columnFilter && this.columnFilter.collection || []; + if (!Array.isArray(collection)) { + throw new Error('The "collection" passed to the Native Select Filter is not a valid array.'); } - const fieldId = this.columnDef && this.columnDef.id; - const optionCollection = this.columnDef.filter.collection || []; const labelName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.label : 'label'; const valueName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.value : 'value'; let options = ''; // collection could be an Array of Strings OR Objects - if (optionCollection.every(x => typeof x === 'string')) { - optionCollection.forEach((option: string) => { + if (collection.every(x => typeof x === 'string')) { + collection.forEach((option: string) => { options += ``; }); } else { - optionCollection.forEach((option: any) => { + collection.forEach((option: any) => { if (!option || (option[labelName] === undefined && option.labelKey === undefined)) { - throw new Error(`A collection with value/label (or value/labelKey when using Locale) is required to populate the Select list, for example:: { filter: model: Filters.select, collection: [ { value: '1', label: 'One' } ]')`); + throw new Error(`A collection with value/label (or value/labelKey when using Locale) is required to populate the Native Select Filter list, for example:: { filter: model: Filters.select, collection: [ { value: '1', label: 'One' } ]')`); } const labelKey = option.labelKey || option[labelName]; const textLabel = ((option.labelKey || this.columnDef.filter.enableTranslateLabel) && this.translate && typeof this.translate.instant === 'function') ? this.translate.instant(labelKey || ' ') : labelKey; @@ -148,6 +186,10 @@ export class NativeSelectFilter implements Filter { $filterElm.attr('id', `filter-${fieldId}`); $filterElm.data('columnId', fieldId); + if (searchTermInput) { + this._currentValues = [searchTermInput]; + } + // append the new DOM element to the header row if ($filterElm && typeof $filterElm.appendTo === 'function') { $filterElm.appendTo($headerElm); diff --git a/src/app/modules/angular-slickgrid/filters/selectFilter.ts b/src/app/modules/angular-slickgrid/filters/selectFilter.ts index a6b28a35f..3d5f330e7 100644 --- a/src/app/modules/angular-slickgrid/filters/selectFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/selectFilter.ts @@ -106,7 +106,7 @@ export class SelectFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; if (!this.grid || !this.columnDef || !this.columnFilter || (!this.columnFilter.collection && !this.columnFilter.collectionAsync)) { throw new Error(`[Angular-SlickGrid] You need to pass a "collection" (or "collectionAsync") for the MultipleSelect/SingleSelect Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.multipleSelect, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); @@ -331,7 +331,8 @@ export class SelectFilter implements Filter { options += ``; // if there's at least 1 search term found, we will add the "filled" class for styling purposes - if (selected) { + // on a single select, we'll also make sure the single value is not an empty string to consider this being filled + if ((selected && this.isMultipleSelect) || (selected && !this.isMultipleSelect && option !== '')) { this.isFilled = true; } }); @@ -371,7 +372,8 @@ export class SelectFilter implements Filter { options += ``; // if there's at least 1 search term found, we will add the "filled" class for styling purposes - if (selected) { + // on a single select, we'll also make sure the single value is not an empty string to consider this being filled + if ((selected && this.isMultipleSelect) || (selected && !this.isMultipleSelect && option[this.valueName] !== '')) { this.isFilled = true; } }); diff --git a/src/app/modules/angular-slickgrid/filters/sliderFilter.ts b/src/app/modules/angular-slickgrid/filters/sliderFilter.ts index d02f15a53..0abec4089 100644 --- a/src/app/modules/angular-slickgrid/filters/sliderFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/sliderFilter.ts @@ -51,14 +51,14 @@ export class SliderFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // define the input & slider number IDs this._elementRangeInputId = `rangeInput_${this.columnDef.field}`; this._elementRangeOutputId = `rangeOutput_${this.columnDef.field}`; // filter input can only have 1 search term, so we will use the 1st array index if it exist - const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || ''; + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; // step 1, create HTML string template const filterTemplate = this.buildTemplateHtmlString(); diff --git a/src/app/modules/angular-slickgrid/filters/sliderRangeFilter.ts b/src/app/modules/angular-slickgrid/filters/sliderRangeFilter.ts index 5ee63e570..7c77a10fa 100644 --- a/src/app/modules/angular-slickgrid/filters/sliderRangeFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/sliderRangeFilter.ts @@ -76,7 +76,7 @@ export class SliderRangeFilter implements Filter { this.grid = args.grid; this.callback = args.callback; this.columnDef = args.columnDef; - this.searchTerms = args.searchTerms || []; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; // step 1, create the DOM Element of the filter & initialize it if searchTerm is filled this.$filterElm = this.createDomElement(this.searchTerms);