Skip to content

Commit

Permalink
feat: convert jQuery to native element on few more filters
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed May 29, 2021
1 parent 3a80996 commit 7d5e1e8
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 63 deletions.
109 changes: 61 additions & 48 deletions packages/common/src/filters/nativeSelectFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ import {
SlickGrid,
} from '../interfaces/index';
import { OperatorType, OperatorString, SearchTerm } from '../enums/index';
import { emptyElement } from '../services/utilities';
import { TranslaterService } from '../services/translater.service';
import { BindingEventService } from '../services/bindingEvent.service';

export class NativeSelectFilter implements Filter {
protected _bindEventService: BindingEventService;
protected _clearFilterTriggered = false;
protected _shouldTriggerQuery = true;
protected _currentValues: any | any[] = [];
$filterElm: any;
filterElm!: HTMLSelectElement;
grid!: SlickGrid;
searchTerms: SearchTerm[] = [];
columnDef!: Column;
callback!: FilterCallback;

constructor(protected readonly translater: TranslaterService) { }
constructor(protected readonly translater: TranslaterService) {
this._bindEventService = new BindingEventService();
}

/** Getter for the Column Filter itself */
protected get columnFilter(): ColumnFilter {
Expand Down Expand Up @@ -75,39 +80,34 @@ export class NativeSelectFilter implements Filter {
searchTerm = `${searchTerm ?? ''}`;
}

// step 1, create HTML string template
const filterTemplate = this.buildTemplateHtmlString();

// step 2, create the DOM Element of the filter & initialize it if searchTerm is filled
this.$filterElm = this.createDomElement(filterTemplate, searchTerm);
// step 1, create the DOM Element of the filter & initialize it if searchTerm is filled
this.filterElm = this.createDomElement(searchTerm);

// step 3, subscribe to the change event and run the callback when that happens
// step 2, subscribe to the change event and run the callback when that happens
// also add/remove "filled" class for styling purposes
this.$filterElm.change(this.handleOnChange.bind(this));
this._bindEventService.bind(this.filterElm, 'change', this.handleOnChange.bind(this));
}

/**
* Clear the filter values
*/
clear(shouldTriggerQuery = true) {
if (this.$filterElm) {
if (this.filterElm) {
this._clearFilterTriggered = true;
this._shouldTriggerQuery = shouldTriggerQuery;
this.searchTerms = [];
this._currentValues = [];
this.$filterElm.val('');
this.$filterElm.trigger('change');
this.filterElm.value = '';
this.filterElm.dispatchEvent(new Event('change'));
}
}

/**
* destroy the filter
*/
destroy() {
if (this.$filterElm) {
this.$filterElm.off('change').remove();
}
this.$filterElm = null;
this._bindEventService.unbindAll();
this.filterElm?.remove?.();
}

/**
Expand All @@ -121,10 +121,10 @@ export class NativeSelectFilter implements Filter {
/** Set value(s) on the DOM element */
setValues(values: SearchTerm | SearchTerm[], operator?: OperatorType | OperatorString) {
if (Array.isArray(values)) {
this.$filterElm.val(values[0]);
this.filterElm.value = `${values[0] ?? ''}`;
this._currentValues = values;
} else if (values) {
this.$filterElm.val(values);
this.filterElm.value = `${values ?? ''}`;
this._currentValues = [values];
}

Expand All @@ -136,63 +136,76 @@ export class NativeSelectFilter implements Filter {
// protected functions
// ------------------

protected buildTemplateHtmlString() {
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.');
}

/**
* Create and return a select dropdown HTML element created from a collection
* @param {Array<Object>} values - list of option values/labels
* @returns {Object} selectElm - Select Dropdown HTML Element
*/
buildFilterSelectFromCollection(collection: any[]): HTMLSelectElement {
const columnId = this.columnDef?.id ?? '';
const labelName = (this.columnFilter.customStructure) ? this.columnFilter.customStructure.label : 'label';
const valueName = (this.columnFilter.customStructure) ? this.columnFilter.customStructure.value : 'value';
const isEnabledTranslate = (this.columnFilter.enableTranslateLabel) ? this.columnFilter.enableTranslateLabel : false;
const selectElm = document.createElement('select');
selectElm.className = `form-control search-filter filter-${columnId}`;

let options = '';
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: string) => {
options += `<option value="${option}" label="${option}">${option}</option>`;
});
for (const option of collection) {
const selectOption = document.createElement('option');
selectOption.value = option;
selectOption.label = option;
selectOption.textContent = option;
selectElm.appendChild(selectOption);
}
} else {
collection.forEach((option: any) => {
for (const option of collection) {
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 && this.translater.translate && this.translater.getCurrentLanguage && this.translater.getCurrentLanguage()) ? this.translater.translate(labelKey || ' ') : labelKey;
options += `<option value="${option[valueName]}">${textLabel}</option>`;
});

const selectOption = document.createElement('option');
selectOption.value = option[valueName];
selectOption.textContent = textLabel;
selectElm.appendChild(selectOption);
}
}
return `<select class="form-control search-filter filter-${columnId}">${options}</select>`;

return selectElm;
}

/**
* From the html template string, create a DOM element
* @param filterTemplate
*/
protected createDomElement(filterTemplate: string, searchTerm?: SearchTerm) {
protected createDomElement(searchTerm?: SearchTerm): HTMLSelectElement {
const columnId = this.columnDef?.id ?? '';
const $headerElm = this.grid.getHeaderRowColumn(columnId);
$($headerElm).empty();
const headerElm = this.grid.getHeaderRowColumn(columnId);
emptyElement(headerElm);

// create the DOM element & add an ID and filter class
const $filterElm = $(filterTemplate);
const searchTermInput = (searchTerm || '') as string;

$filterElm.val(searchTermInput);
$filterElm.data('columnId', columnId);
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];
}

// append the new DOM element to the header row
if ($filterElm && typeof $filterElm.appendTo === 'function') {
$filterElm.appendTo($headerElm);
}
headerElm.appendChild(selectElm);

return $filterElm;
return selectElm;
}

protected handleOnChange(e: any) {
Expand All @@ -201,9 +214,9 @@ export class NativeSelectFilter implements Filter {

if (this._clearFilterTriggered) {
this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery });
this.$filterElm.removeClass('filled');
this.filterElm.classList.remove('filled');
} else {
value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('filled');
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 });
}

Expand Down
23 changes: 11 additions & 12 deletions packages/common/src/services/__tests__/filter.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
SlickGrid,
SlickNamespace,
} from '../../interfaces/index';
import { Filters, InputFilter } from '../../filters';
import { Filters, InputFilter, NativeSelectFilter } from '../../filters';
import { FilterService } from '../filter.service';
import { FilterFactory } from '../../filters/filterFactory';
import { getParsedSearchTermsByFieldType } from '../../filter-conditions';
Expand All @@ -31,7 +31,6 @@ import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
import { PubSubService } from '../pubSub.service';
import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';

jest.mock('flatpickr', () => { });
declare const Slick: SlickNamespace;
const DOM_ELEMENT_ID = 'row-detail123';

Expand Down Expand Up @@ -201,7 +200,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]).toContainEntry(['$filterElm', expect.anything()]);
expect(filterMetadataArray[0] instanceof NativeSelectFilter).toBeTruthy();
expect(filterMetadataArray[0]).toContainEntry(['searchTerms', [true]]);
});

Expand Down Expand Up @@ -342,7 +341,7 @@ describe('FilterService', () => {
service.init(gridStub);
service.bindLocalOnFilter(gridStub);
gridStub.onHeaderRowCellRendered.notify(mockArgs as any, new Slick.EventData(), gridStub);
service.getFiltersMetadata()[0].callback(new CustomEvent('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true });
service.getFiltersMetadata()[0].callback(new Event('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true });

setTimeout(() => {
expect(service.getColumnFilters()).toContainEntry(['firstName', expectationColumnFilter]);
Expand Down Expand Up @@ -371,7 +370,7 @@ describe('FilterService', () => {
service.bindLocalOnFilter(gridStub);
gridStub.onHeaderRowCellRendered.notify(mockArgs as any, new Slick.EventData(), gridStub);

const mockEvent = new CustomEvent('input');
const mockEvent = new Event('input');
Object.defineProperty(mockEvent, 'target', { writable: true, configurable: true, value: { value: 'John' } });
service.getFiltersMetadata()[0].callback(mockEvent, { columnDef: mockColumn, operator: 'EQ', shouldTriggerQuery: true });

Expand All @@ -393,7 +392,7 @@ describe('FilterService', () => {
service.init(gridStub);
service.bindLocalOnFilter(gridStub);
gridStub.onHeaderRowCellRendered.notify(mockArgs as any, new Slick.EventData(), gridStub);
service.getFiltersMetadata()[0].callback(new CustomEvent('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true });
service.getFiltersMetadata()[0].callback(new Event('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true });

expect(service.getColumnFilters()).toEqual({});
});
Expand All @@ -407,7 +406,7 @@ describe('FilterService', () => {
service.bindLocalOnFilter(gridStub);
mockArgs.column.filter = { emptySearchTermReturnAllValues: false };
gridStub.onHeaderRowCellRendered.notify(mockArgs as any, new Slick.EventData(), gridStub);
service.getFiltersMetadata()[0].callback(new CustomEvent('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true });
service.getFiltersMetadata()[0].callback(new Event('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true });

expect(service.getColumnFilters()).toContainEntry(['firstName', expectationColumnFilter]);
expect(spySearchChange).toHaveBeenCalledWith({
Expand Down Expand Up @@ -468,9 +467,9 @@ describe('FilterService', () => {
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
gridStub.onHeaderRowCellRendered.notify(mockArgs3 as any, new Slick.EventData(), gridStub);
service.getFiltersMetadata()[1].callback(new CustomEvent('input'), { columnDef: mockColumn3 });
service.getFiltersMetadata()[0].callback(new CustomEvent('input'), { columnDef: mockColumn1, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true });
service.getFiltersMetadata()[1].callback(new CustomEvent('input'), { columnDef: mockColumn2, operator: 'NE', searchTerms: ['Doe'], shouldTriggerQuery: true });
service.getFiltersMetadata()[1].callback(new Event('input'), { columnDef: mockColumn3 });
service.getFiltersMetadata()[0].callback(new Event('input'), { columnDef: mockColumn1, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true });
service.getFiltersMetadata()[1].callback(new Event('input'), { columnDef: mockColumn2, operator: 'NE', searchTerms: ['Doe'], shouldTriggerQuery: true });
});

describe('clearFilterByColumnId method', () => {
Expand Down Expand Up @@ -605,8 +604,8 @@ describe('FilterService', () => {
service.bindLocalOnFilter(gridStub);
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
service.getFiltersMetadata()[0].callback(new CustomEvent('input'), { columnDef: mockColumn1, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true });
service.getFiltersMetadata()[1].callback(new CustomEvent('input'), { columnDef: mockColumn2, operator: 'NE', searchTerms: ['Doe'], shouldTriggerQuery: true });
service.getFiltersMetadata()[0].callback(new Event('input'), { columnDef: mockColumn1, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true });
service.getFiltersMetadata()[1].callback(new Event('input'), { columnDef: mockColumn2, operator: 'NE', searchTerms: ['Doe'], shouldTriggerQuery: true });
});

describe('clearFilterByColumnId method', () => {
Expand Down
5 changes: 2 additions & 3 deletions packages/common/src/services/filter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,8 @@ export class FilterService {

dispose() {
// unsubscribe all SlickGrid events
if (this._eventHandler && this._eventHandler.unsubscribeAll) {
this._eventHandler.unsubscribeAll();
}
this._eventHandler.unsubscribeAll();

if (this.httpCancelRequests$ && this.rxjs?.isObservable(this.httpCancelRequests$)) {
this.httpCancelRequests$.next(); // this cancels any pending http requests
this.httpCancelRequests$.complete();
Expand Down

0 comments on commit 7d5e1e8

Please sign in to comment.