diff --git a/docs/migrations/migration-to-5.x.md b/docs/migrations/migration-to-5.x.md index c00b79c01..4819c2cd1 100644 --- a/docs/migrations/migration-to-5.x.md +++ b/docs/migrations/migration-to-5.x.md @@ -109,6 +109,23 @@ const myCheckmarkFormatter: Formatter = (row: number, cell: number, value: any, ``` ## Grid Functionalities + +### Native Select Filter (removed) +I would be surprised if anyone uses the `Filters.select` and so it was removed, you should simply use the `Filters.singleSelect` or `Filters.multipleSelect` + +```diff +prepareGrid() { + this.columnDefinitions = [{ + id: 'isActive', name: 'Active', field: 'isActive', + filter: { +- model: Filters.select, ++ model: Filters.singleSelect, + collection: [ { value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' } ], + } + }]; +} +``` + ### Date Editor/Filter We migrated from Flatpicker to Vanilla-Calendar and this require some changes since the option names are different. The changes are the same for both the Filter and the Editor. diff --git a/packages/common/src/filters/__tests__/nativeSelectFilter.spec.ts b/packages/common/src/filters/__tests__/nativeSelectFilter.spec.ts deleted file mode 100644 index 5f936f839..000000000 --- a/packages/common/src/filters/__tests__/nativeSelectFilter.spec.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { Column, FilterArguments, GridOption } from '../../interfaces/index'; -import { Filters } from '../filters.index'; -import { NativeSelectFilter } from '../nativeSelectFilter'; -import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { SlickGrid } from '../../core/index'; - -jest.useFakeTimers(); - -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(), -} as unknown as SlickGrid; - -describe('NativeSelectFilter', () => { - let translateService: TranslateServiceStub; - let divContainer: HTMLDivElement; - let filter: NativeSelectFilter; - let filterArguments: FilterArguments; - let spyGetHeaderRow; - let mockColumn: Column; - - beforeEach(() => { - translateService = new TranslateServiceStub(); - - 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(), - filterContainerElm: gridStub.getHeaderRowColumn(mockColumn.id) - }; - - filter = new NativeSelectFilter(translateService); - }); - - afterEach(() => { - filter.destroy(); - }); - - it('should throw an error when trying to call init without any arguments', () => { - expect(() => filter.init(null as any)).toThrowError('[Slickgrid-Universal] 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(`[Slickgrid-Universal] 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 { - mockColumn.filter!.collection = { hello: 'world' } as any; - 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 I18N Service', (done) => { - try { - translateService = undefined as any; - mockColumn.filter!.enableTranslateLabel = true; - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - filter = new NativeSelectFilter(translateService); - - filter.init(filterArguments); - } catch (e) { - expect(e.message).toContain(`The I18N Service 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 have an aria-label when creating the filter', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('select.form-control.search-filter.filter-gender') as HTMLInputElement; - - expect(filterInputElm.ariaLabel).toBe('Gender Search Filter'); - }); - - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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`) as HTMLInputElement; - 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', () => { - translateService.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); - jest.runAllTimers(); // fast-forward timer - - 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'); - }); - - it('should work with French locale when locale is changed', () => { - translateService.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); - const filterListElm = divContainer.querySelectorAll(`select.search-filter.filter-gender option`); - jest.runAllTimers(); // fast-forward timer - - expect(filterListElm.length).toBe(3); - expect(filterListElm[0].textContent).toBe('Autre'); - expect(filterListElm[1].textContent).toBe('Mâle'); - expect(filterListElm[2].textContent).toBe('Femme'); - }); -}); diff --git a/packages/common/src/filters/filters.index.ts b/packages/common/src/filters/filters.index.ts index 6a6af96d8..a1b0b4ea3 100644 --- a/packages/common/src/filters/filters.index.ts +++ b/packages/common/src/filters/filters.index.ts @@ -9,7 +9,6 @@ import { InputMaskFilter } from './inputMaskFilter'; import { InputNumberFilter } from './inputNumberFilter'; import { InputPasswordFilter } from './inputPasswordFilter'; import { MultipleSelectFilter } from './multipleSelectFilter'; -import { NativeSelectFilter } from './nativeSelectFilter'; import { DateRangeFilter } from './dateRangeFilter'; import { SingleSelectFilter } from './singleSelectFilter'; import { SingleSliderFilter } from './singleSliderFilter'; @@ -61,9 +60,6 @@ export const Filters = { /** Multiple Select filter, which uses 3rd party lib "multiple-select.js" */ multipleSelect: MultipleSelectFilter, - /** Select filter, which uses native DOM element select */ - select: NativeSelectFilter, - /** Single Select filter, which uses 3rd party lib "multiple-select.js" */ singleSelect: SingleSelectFilter, diff --git a/packages/common/src/filters/index.ts b/packages/common/src/filters/index.ts index 7a21b0bea..1c89aa56b 100644 --- a/packages/common/src/filters/index.ts +++ b/packages/common/src/filters/index.ts @@ -13,7 +13,6 @@ export * from './inputMaskFilter'; export * from './inputNumberFilter'; export * from './inputPasswordFilter'; export * from './multipleSelectFilter'; -export * from './nativeSelectFilter'; export * from './selectFilter'; export * from './singleSelectFilter'; export * from './singleSliderFilter'; diff --git a/packages/common/src/filters/nativeSelectFilter.ts b/packages/common/src/filters/nativeSelectFilter.ts deleted file mode 100644 index 76c3b8d6d..000000000 --- a/packages/common/src/filters/nativeSelectFilter.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { BindingEventService } from '@slickgrid-universal/binding'; -import { createDomElement, emptyElement, toSentenceCase } from '@slickgrid-universal/utils'; - -import type { - Column, - ColumnFilter, - Filter, - FilterArguments, - FilterCallback, - GridOption, -} from '../interfaces/index'; -import { OperatorType, type OperatorString, type SearchTerm } from '../enums/index'; -import type { TranslaterService } from '../services/translater.service'; -import { type SlickGrid } from '../core/index'; - -export class NativeSelectFilter implements Filter { - protected _bindEventService: BindingEventService; - protected _clearFilterTriggered = false; - protected _shouldTriggerQuery = true; - protected _currentValues: any | any[] = []; - filterElm!: HTMLSelectElement; - grid!: SlickGrid; - searchTerms: SearchTerm[] = []; - columnDef!: Column; - callback!: FilterCallback; - filterContainerElm!: HTMLDivElement; - - constructor(protected readonly translater?: TranslaterService) { - this._bindEventService = new BindingEventService(); - } - - /** Getter for the Column Filter itself */ - protected get columnFilter(): ColumnFilter { - return this.columnDef?.filter ?? {}; - } - - /** Getter to know what would be the default operator when none is specified */ - get defaultOperator(): OperatorType | OperatorString { - return OperatorType.equal; - } - - /** Getter for the Grid Options pulled through the Grid Object */ - protected get gridOptions(): GridOption { - return this.grid?.getOptions() ?? {}; - } - - /** Getter for the current Operator */ - get operator(): OperatorType | OperatorString { - return this.columnFilter?.operator ?? this.defaultOperator; - } - - /** Setter for the filter operator */ - set operator(operator: OperatorType | OperatorString) { - if (this.columnFilter) { - this.columnFilter.operator = operator; - } - } - - /** - * Initialize the Filter - */ - init(args: FilterArguments) { - if (!args) { - throw new Error('[Slickgrid-Universal] 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.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; - this.filterContainerElm = args.filterContainerElm; - - if (!this.grid || !this.columnDef || !this.columnFilter || !this.columnFilter.collection) { - throw new Error(`[Slickgrid-Universal] You need to pass a "collection" for the Native Select Filter to work correctly.`); - } - - if (this.columnFilter.enableTranslateLabel && !this.gridOptions.enableTranslate && (!this.translater || typeof this.translater.translate !== 'function')) { - throw new Error(`The I18N Service 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.length >= 0) ? this.searchTerms[0] : ''; - if (typeof searchTerm === 'boolean' || typeof searchTerm === 'number') { - searchTerm = `${searchTerm ?? ''}`; - } - - // step 1, create the DOM Element of the filter & initialize it if searchTerm is filled - this.filterElm = this.createFilterElement(searchTerm); - - // step 2, subscribe to the change event and run the callback when that happens - // also add/remove "filled" class for styling purposes - this._bindEventService.bind(this.filterElm, 'change', this.handleOnChange.bind(this)); - } - - /** - * Clear the filter values - */ - clear(shouldTriggerQuery = true) { - if (this.filterElm) { - this._clearFilterTriggered = true; - this._shouldTriggerQuery = shouldTriggerQuery; - this.searchTerms = []; - this._currentValues = []; - this.filterElm.value = ''; - this.filterElm.classList.remove('filled'); - this.filterElm.dispatchEvent(new Event('change')); - } - } - - /** - * destroy the filter - */ - destroy() { - this._bindEventService.unbindAll(); - this.filterElm?.remove?.(); - } - - /** - * Get selected values retrieved from the select element - * @params selected items - */ - getValues(): any[] { - return this._currentValues || []; - } - - /** Set value(s) on the DOM element */ - setValues(values: SearchTerm | SearchTerm[], operator?: OperatorType | OperatorString) { - if (Array.isArray(values)) { - this.filterElm.value = `${values[0] ?? ''}`; - this._currentValues = values; - } else if (values) { - this.filterElm.value = `${values ?? ''}`; - this._currentValues = [values]; - } - this.getValues().length > 0 ? this.filterElm.classList.add('filled') : this.filterElm.classList.remove('filled'); - - // set the operator when defined - this.operator = operator || this.defaultOperator; - } - - // - // protected functions - // ------------------ - - /** - * Create and return a select dropdown HTML element created from a collection - * @param {Array} values - list of option values/labels - * @returns {Object} selectElm - Select Dropdown HTML Element - */ - buildFilterSelectFromCollection(collection: any[]): HTMLSelectElement { - const columnId = this.columnDef?.id ?? ''; - const selectElm = createDomElement('select', { - className: `form-control search-filter filter-${columnId}`, - ariaLabel: this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter` - }); - - const labelName = this.columnFilter.customStructure?.label ?? 'label'; - const valueName = this.columnFilter.customStructure?.value ?? 'value'; - const isEnabledTranslate = this.columnFilter?.enableTranslateLabel ?? false; - - // collection could be an Array of Strings OR Objects - if (collection.every(x => typeof x === 'string')) { - collection.forEach(option => { - selectElm.appendChild( - createDomElement('option', { value: option, label: option, textContent: option }) - ); - }); - } else { - collection.forEach(option => { - 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 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 || isEnabledTranslate) && this.translater !== undefined && this.translater?.getCurrentLanguage?.()) ? this.translater.translate(labelKey || ' ') : labelKey; - - selectElm.appendChild( - createDomElement('option', { value: option[valueName], textContent: textLabel }) - ); - }); - } - - return selectElm; - } - - /** - * From the html template string, create a DOM element - * @param filterTemplate - */ - protected createFilterElement(searchTerm?: SearchTerm): HTMLSelectElement { - const columnId = this.columnDef?.id ?? ''; - emptyElement(this.filterContainerElm); - - // create the DOM element & add an ID and filter class - const searchTermInput = (searchTerm || '') as string; - - const collection = this.columnFilter?.collection ?? []; - if (!Array.isArray(collection)) { - throw new Error('The "collection" passed to the Native Select Filter is not a valid array.'); - } - - const selectElm = this.buildFilterSelectFromCollection(collection); - selectElm.value = searchTermInput; - selectElm.dataset.columnid = `${columnId || ''}`; - - if (searchTermInput) { - this._currentValues = [searchTermInput]; - } - - this.filterContainerElm.appendChild(selectElm); - - return selectElm; - } - - protected handleOnChange(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.classList.remove('filled'); - } else { - value === '' ? this.filterElm.classList.remove('filled') : this.filterElm.classList.add('filled'); - this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: [value], shouldTriggerQuery: this._shouldTriggerQuery }); - } - - // reset both flags for next use - this._clearFilterTriggered = false; - this._shouldTriggerQuery = true; - } -} diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts index e13f98165..4ce2fa24d 100644 --- a/packages/common/src/services/__tests__/filter.service.spec.ts +++ b/packages/common/src/services/__tests__/filter.service.spec.ts @@ -13,7 +13,7 @@ import { MenuCommandItem, RowDetailView, } from '../../interfaces/index'; -import { Filters, InputFilter, NativeSelectFilter } from '../../filters'; +import { Filters, InputFilter, SingleSelectFilter } from '../../filters'; import { FilterService } from '../filter.service'; import { FilterFactory } from '../../filters/filterFactory'; import { getParsedSearchTermsByFieldType } from '../../filter-conditions'; @@ -187,7 +187,7 @@ describe('FilterService', () => { const mockColumn = { id: 'isActive', field: 'isActive', filterable: true, type: FieldType.boolean, filter: { - model: Filters.select, searchTerms: [true], collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], + model: Filters.singleSelect, searchTerms: [true], collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } } as Column; const mockArgs = { grid: gridStub, column: mockColumn, node: document.getElementById(DOM_ELEMENT_ID), }; @@ -205,7 +205,7 @@ describe('FilterService', () => { isActive: { columnDef: mockColumn, columnId: 'isActive', operator: 'EQ', searchTerms: [true], parsedSearchTerms: true, type: FieldType.boolean }, }); expect(filterMetadataArray.length).toBe(1); - expect(filterMetadataArray[0] instanceof NativeSelectFilter).toBeTruthy(); + expect(filterMetadataArray[0] instanceof SingleSelectFilter).toBeTruthy(); expect(filterMetadataArray[0]).toContainEntry(['searchTerms', [true]]); gridStub.onBeforeHeaderRowCellDestroy.notify(mockArgs as any, new SlickEventData(), gridStub); @@ -217,7 +217,7 @@ describe('FilterService', () => { const mockColumn = { id: 'isActive', field: 'isActive', filterable: true, filter: { - model: Filters.select, searchTerms: [true], collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], + model: Filters.singleSelect, searchTerms: [true], collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } } as Column; const mockSearchArgs = { @@ -305,7 +305,7 @@ describe('FilterService', () => { const mockColumn = { id: 'firstName', field: 'firstName', filterable: true, filter: { - model: Filters.select, searchTerms: [true], collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], + model: Filters.singleSelect, searchTerms: [true], collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } } as Column; const tmpDivElm = document.createElement('div'); @@ -497,7 +497,7 @@ describe('FilterService', () => { describe('clearFilterByColumnId method', () => { it('should clear the filter by passing a column id as argument on a backend grid', async () => { const filterExpectation = { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], targetSelector: '', type: FieldType.string }; - const newEvent = new CustomEvent(`mouseup`); + const newEvent = new SlickEventData(new CustomEvent(`mouseup`)); const spyClear = jest.spyOn(service.getFiltersMetadata()[0], 'clear'); const spyFilterChange = jest.spyOn(service, 'onBackendFilterChange'); const spyEmitter = jest.spyOn(service, 'emitFilterChanged'); @@ -519,7 +519,7 @@ describe('FilterService', () => { it('should not call "onBackendFilterChange" method when the filter is previously empty', async () => { const filterFirstExpectation = { columnDef: mockColumn1, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'], parsedSearchTerms: ['John'], targetSelector: '', type: FieldType.string }; const filterLastExpectation = { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], targetSelector: '', type: FieldType.string }; - const newEvent = new Event('mouseup'); + const newEvent = new SlickEventData(new CustomEvent(`mouseup`)); const spyClear = jest.spyOn(service.getFiltersMetadata()[2], 'clear'); const spyFilterChange = jest.spyOn(service, 'onBackendFilterChange'); const spyEmitter = jest.spyOn(service, 'emitFilterChanged'); @@ -640,9 +640,9 @@ describe('FilterService', () => { const spyClear = jest.spyOn(service.getFiltersMetadata()[0], 'clear'); const spyEmitter = jest.spyOn(service, 'emitFilterChanged'); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); - + const newEvent = new SlickEventData(new CustomEvent(`mouseup`)); const filterCountBefore = Object.keys(service.getColumnFilters()).length; - await service.clearFilterByColumnId(new CustomEvent(`mouseup`), 'firstName'); + await service.clearFilterByColumnId(newEvent, 'firstName'); const filterCountAfter = Object.keys(service.getColumnFilters()).length; expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterClear`, { columnId: 'firstName' }, 0); @@ -1210,7 +1210,7 @@ describe('FilterService', () => { gridOptionMock.enableTreeData = false; gridOptionMock.backendServiceApi = undefined; mockColumn1 = { id: 'firstName', name: 'firstName', field: 'firstName', filterable: true, filter: { model: Filters.inputText } }; - mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, filterable: true, filter: { model: Filters.select, collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } }; + mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, filterable: true, filter: { model: Filters.singleSelect, collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } }; mockArgs1 = { grid: gridStub, column: mockColumn1, node: document.getElementById(DOM_ELEMENT_ID) }; mockArgs2 = { grid: gridStub, column: mockColumn2, node: document.getElementById(DOM_ELEMENT_ID) }; mockNewFilters = [ @@ -1674,7 +1674,7 @@ describe('FilterService', () => { gridOptionMock.enableTreeData = false; gridOptionMock.backendServiceApi = undefined; mockColumn1 = { id: 'firstName', name: 'firstName', field: 'firstName', filterable: true, filter: { model: Filters.inputText } }; - mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, filterable: true, filter: { model: Filters.select, collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } }; + mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, filterable: true, filter: { model: Filters.singleSelect, collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } }; mockColumn3 = { id: 'name', field: 'name', filterable: true, filter: { model: Filters.inputText } }; mockArgs1 = { grid: gridStub, column: mockColumn1, node: document.getElementById(DOM_ELEMENT_ID) }; mockArgs2 = { grid: gridStub, column: mockColumn2, node: document.getElementById(DOM_ELEMENT_ID) }; @@ -1835,7 +1835,7 @@ describe('FilterService', () => { gridOptionMock.enableFiltering = true; gridOptionMock.backendServiceApi = undefined; mockColumn1 = { id: 'file', name: 'file', field: 'file', filterable: true, filter: { model: Filters.inputText } }; - mockColumn2 = { id: 'dateModified', name: 'dateModified', field: 'dateModified', filterable: true, filter: { model: Filters.select, collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } }; + mockColumn2 = { id: 'dateModified', name: 'dateModified', field: 'dateModified', filterable: true, filter: { model: Filters.singleSelect, collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } }; mockColumn3 = { id: 'size', name: 'size', field: 'size', filterable: true, filter: { model: Filters.inputText } }; mockArgs1 = { grid: gridStub, column: mockColumn1, node: document.getElementById(DOM_ELEMENT_ID) }; mockArgs2 = { grid: gridStub, column: mockColumn2, node: document.getElementById(DOM_ELEMENT_ID) };