diff --git a/examples/webpack-demo-vanilla-bundle/src/app.ts b/examples/webpack-demo-vanilla-bundle/src/app.ts index 0aeb43062..e7f9a7420 100644 --- a/examples/webpack-demo-vanilla-bundle/src/app.ts +++ b/examples/webpack-demo-vanilla-bundle/src/app.ts @@ -2,7 +2,14 @@ import { AppRouting } from './app-routing'; import { Renderer } from './renderer'; import { RouterConfig } from './interfaces'; +interface ElementEventListener { + element: Element; + eventName: string; + listener: EventListenerOrEventListenerObject; +} + export class App { + private _boundedEventWithListeners: ElementEventListener[] = []; documentTitle = 'Slickgrid-Universal'; defaultRouteName: string; stateBangChar: string; @@ -40,17 +47,32 @@ export class App { }; } + addElementEventListener(element: Element, eventName: string, listener: EventListenerOrEventListenerObject) { + element.addEventListener(eventName, listener); + this._boundedEventWithListeners.push({ element, eventName, listener }); + } + + /** Dispose of the SPA App */ + disposeApp() { + document.removeEventListener('DOMContentLoaded', this.handleNavbarHamburgerToggle); + } + + /** Dispose of all View Models of the SPA */ disposeAll() { - this.renderer?.dispose(); + this.unbindAllEvents(); for (const vmKey of Object.keys(this.viewModelObj)) { const viewModel = this.viewModelObj[vmKey]; - if (viewModel && viewModel.dispose) { + if (viewModel?.dispose) { viewModel?.dispose(); - delete window[vmKey]; - delete this.viewModelObj[vmKey]; } + // nullify the object and then delete them to make sure it's picked by the garbage collector + window[vmKey] = null; + this.viewModelObj[vmKey] = null; + delete window[vmKey]; + delete this.viewModelObj[vmKey]; } + this.renderer?.dispose(); } loadRoute(routeName: string, changeBrowserState = true) { @@ -63,12 +85,15 @@ export class App { return; } const viewModel = this.renderer.loadViewModel(require(`${mapRoute.moduleId}.ts`)); - if (viewModel && viewModel.dispose) { - window.onunload = viewModel.dispose; // dispose when leaving SPA + if (viewModel?.dispose) { + window.onunload = () => { + viewModel.dispose; // dispose when leaving SPA + this.disposeApp(); + }; } this.renderer.loadView(require(`${mapRoute.moduleId}.html`)); - if (viewModel && viewModel.attached && this.renderer.className) { + if (viewModel?.attached && this.renderer.className) { this.viewModelObj[this.renderer.className] = viewModel; viewModel.attached(); this.dropdownToggle(); // rebind bulma dropdown toggle event handlers @@ -87,14 +112,14 @@ export class App { const $dropdowns = document.querySelectorAll('.dropdown:not(.is-hoverable)'); if ($dropdowns.length > 0) { - $dropdowns.forEach(($el) => { - $el.addEventListener('click', (event) => { + $dropdowns.forEach($el => { + this.addElementEventListener($el, 'click', (event) => { event.stopPropagation(); $el.classList.toggle('is-active'); }); }); - document.addEventListener('click', () => this.closeDropdowns()); + this.addElementEventListener(document.body, 'click', this.closeDropdowns.bind(this)); } } @@ -105,27 +130,38 @@ export class App { /** Add event listener for the navbar hamburger menu toggle when menu shows up on mobile */ navbarHamburgerToggle() { - document.addEventListener('DOMContentLoaded', () => { + document.addEventListener('DOMContentLoaded', this.handleNavbarHamburgerToggle); + } - // Get all "navbar-burger" elements - const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); + handleNavbarHamburgerToggle() { + // Get all "navbar-burger" elements + const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); - // Check if there are any navbar burgers - if ($navbarBurgers.length > 0) { - // Add a click event on each of them - $navbarBurgers.forEach(el => { - el.addEventListener('click', () => { + // Check if there are any navbar burgers + if ($navbarBurgers.length > 0) { + // Add a click event on each of them + $navbarBurgers.forEach(el => { + el.addEventListener('click', () => { - // Get the target from the "data-target" attribute - const target = el.dataset.target; - const $target = document.getElementById(target); + // Get the target from the "data-target" attribute + const target = el.dataset.target; + const $target = document.getElementById(target); - // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" - el.classList.toggle('is-active'); - $target.classList.toggle('is-active'); - }); + // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" + el.classList.toggle('is-active'); + $target.classList.toggle('is-active'); }); + }); + } + } + + /** Unbind All (remove) bounded elements with listeners */ + unbindAllEvents() { + for (const boundedEvent of this._boundedEventWithListeners) { + const { element, eventName, listener } = boundedEvent; + if (element?.removeEventListener) { + element.removeEventListener(eventName, listener); } - }); + } } } diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/event.service.ts b/examples/webpack-demo-vanilla-bundle/src/examples/event.service.ts new file mode 100644 index 000000000..33c0cd5bd --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/event.service.ts @@ -0,0 +1,24 @@ +interface ElementEventListener { + element: Element; + eventName: string; + listener: EventListenerOrEventListenerObject; +} + +export class EventService { + private _boundedEventWithListeners: ElementEventListener[] = []; + + addElementEventListener(element: Element, eventName: string, listener: EventListenerOrEventListenerObject) { + element.addEventListener(eventName, listener); + this._boundedEventWithListeners.push({ element, eventName, listener }); + } + + /** Unbind All (remove) bounded elements with listeners */ + unbindAllEvents() { + for (const boundedEvent of this._boundedEventWithListeners) { + const { element, eventName, listener } = boundedEvent; + if (element?.removeEventListener) { + element.removeEventListener(eventName, listener); + } + } + } +} diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts index bf263921f..41b59c089 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts @@ -20,15 +20,13 @@ export class Example1 { attached() { this.defineGrids(); - const gridContainerElm1 = document.querySelector(`.grid1`); - const gridContainerElm2 = document.querySelector(`.grid2`); // mock some data (different in each dataset) this.dataset1 = this.mockData(NB_ITEMS); this.dataset2 = this.mockData(NB_ITEMS); - this.sgb1 = new Slicker.GridBundle(gridContainerElm1, this.columnDefinitions1, { ...ExampleGridOptions, ...this.gridOptions1 }, this.dataset1); - this.sgb2 = new Slicker.GridBundle(gridContainerElm2, this.columnDefinitions2, { ...ExampleGridOptions, ...this.gridOptions2 }, this.dataset2); + this.sgb1 = new Slicker.GridBundle(document.querySelector(`.grid1`), this.columnDefinitions1, { ...ExampleGridOptions, ...this.gridOptions1 }, this.dataset1); + this.sgb2 = new Slicker.GridBundle(document.querySelector(`.grid2`), this.columnDefinitions2, { ...ExampleGridOptions, ...this.gridOptions2 }, this.dataset2); // setTimeout(() => { // this.slickgridLwc2.dataset = this.dataset2; diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts index 93fd0d3ae..c6afd51fd 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts @@ -20,10 +20,12 @@ import { Slicker, SlickerGridInstance, SlickVanillaGridBundle } from '@slickgrid import { ExampleGridOptions } from './example-grid-options'; import '../material-styles.scss'; import './example02.scss'; +import { EventService } from './event.service'; const NB_ITEMS = 500; export class Example2 { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; dataset: any[]; @@ -41,13 +43,17 @@ export class Example2 { return this.sgb?.instances; } + constructor() { + this.eventService = new EventService(); + } + attached() { this.initializeGrid(); this.dataset = this.loadData(NB_ITEMS); const gridContainerElm = document.querySelector(`.grid2`); - gridContainerElm.addEventListener('onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel')); - gridContainerElm.addEventListener('onafterexporttoexcel', () => console.log('onAfterExportToExcel')); + this.eventService.addElementEventListener(gridContainerElm, 'onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel')); + this.eventService.addElementEventListener(gridContainerElm, 'onafterexporttoexcel', () => console.log('onAfterExportToExcel')); this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); } diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts index 1ad65166f..3f0c966ec 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts @@ -22,6 +22,7 @@ import { Slicker, SlickerGridInstance, SlickVanillaGridBundle } from '@slickgrid import { ExampleGridOptions } from './example-grid-options'; import '../salesforce-styles.scss'; import './example03.scss'; +import { EventService } from './event.service'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; @@ -37,6 +38,7 @@ interface ReportItem { } export class Example3 { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; dataset: any[]; @@ -49,21 +51,26 @@ export class Example3 { draggableGroupingPlugin: SlickDraggableGrouping; selectedGroupingFields: Array = ['', '', '']; + constructor() { + this.eventService = new EventService(); + } + attached() { this.initializeGrid(); this.dataset = this.loadData(500); const gridContainerElm = document.querySelector(`.grid3`); - gridContainerElm.addEventListener('onclick', this.handleOnClick.bind(this)); - gridContainerElm.addEventListener('oncellchange', this.handleOnCellChange.bind(this)); - gridContainerElm.addEventListener('onvalidationerror', this.handleValidationError.bind(this)); - gridContainerElm.addEventListener('onitemdeleted', this.handleItemDeleted.bind(this)); - gridContainerElm.addEventListener('onslickergridcreated', this.handleOnSlickerGridCreated.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'onclick', this.handleOnClick.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'oncellchange', this.handleOnCellChange.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'onvalidationerror', this.handleValidationError.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'onitemdeleted', this.handleItemDeleted.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'onslickergridcreated', this.handleOnSlickerGridCreated.bind(this)); this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); } dispose() { this.sgb?.dispose(); + this.eventService.unbindAllEvents(); } initializeGrid() { diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example04.html b/examples/webpack-demo-vanilla-bundle/src/examples/example04.html index e8fde66ab..e3f962438 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example04.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example04.html @@ -17,7 +17,7 @@

Example 04 - Pinned (frozen) Columns/Rows

diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts index 80cb2a880..d8a94fa6d 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts @@ -13,6 +13,7 @@ import { } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { EventService } from './event.service'; import { ExampleGridOptions } from './example-grid-options'; import './example02.scss'; @@ -41,6 +42,7 @@ const customEditableInputFormatter = (_row: number, _cell: number, _value: any, }; export class Example4 { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; dataset: any[]; @@ -52,18 +54,23 @@ export class Example4 { isFrozenBottom = false; sgb: SlickVanillaGridBundle; + constructor() { + this.eventService = new EventService(); + } + attached() { const dataset = this.initializeGrid(); const gridContainerElm = document.querySelector(`.grid4`); - // gridContainerElm.addEventListener('onclick', handleOnClick); - gridContainerElm.addEventListener('onvalidationerror', this.handleOnValidationError.bind(this)); - gridContainerElm.addEventListener('onitemdeleted', this.handleOnItemDeleted.bind(this)); + // this.eventService.addElementEventListener(gridContainerElm, 'onclick', handleOnClick); + this.eventService.addElementEventListener(gridContainerElm, 'onvalidationerror', this.handleOnValidationError.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'onitemdeleted', this.handleOnItemDeleted.bind(this)); this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, dataset); } dispose() { this.sgb?.dispose(); + this.eventService.unbindAllEvents(); } initializeGrid() { diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts index 3031a3ad1..921604ac1 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts @@ -8,22 +8,28 @@ import { } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { EventService } from './event.service'; import { ExampleGridOptions } from './example-grid-options'; export class Example7 { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; dataset: any[]; sgb: SlickVanillaGridBundle; duplicateTitleHeaderCount = 1; + constructor() { + this.eventService = new EventService(); + } + attached() { this.initializeGrid(); this.dataset = this.loadData(500); const gridContainerElm = document.querySelector(`.grid7`); - gridContainerElm.addEventListener('oncellchange', this.handleOnCellChange.bind(this)); - gridContainerElm.addEventListener('onvalidationerror', this.handleValidationError.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'oncellchange', this.handleOnCellChange.bind(this)); + this.eventService.addElementEventListener(gridContainerElm, 'onvalidationerror', this.handleValidationError.bind(this)); this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); } @@ -107,8 +113,8 @@ export class Example7 { singleRowMove: true, disableRowSelection: true, cancelEditOnDrag: true, - onBeforeMoveRows: (e, args) => this.onBeforeMoveRow(e, args), - onMoveRows: (e, args) => this.onMoveRows(e, args), + onBeforeMoveRows: this.onBeforeMoveRow, + onMoveRows: this.onMoveRows.bind(this), // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -178,7 +184,7 @@ export class Example7 { selectedRows.push(left.length + i); } - this.sgb.slickGrid.resetActiveCell(); + args.grid.resetActiveCell(); this.sgb.dataset = this.dataset; // update dataset and re-render the grid } diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts index 72b272c4e..4b244325e 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts @@ -1,11 +1,13 @@ import { Column, FieldType, Filters, GridOption, GridStateChange, Metrics, OperatorType, } from '@slickgrid-universal/common'; import { GridOdataService, OdataServiceApi, OdataOption } from '@slickgrid-universal/odata'; import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { EventService } from './event.service'; import { ExampleGridOptions } from './example-grid-options'; const defaultPageSize = 20; export class Example09 { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; metrics: Metrics; @@ -18,13 +20,17 @@ export class Example09 { status = ''; statusClass = 'is-success'; + constructor() { + this.eventService = new EventService(); + } + attached() { this.initializeGrid(); const gridContainerElm = document.querySelector(`.grid9`); - gridContainerElm.addEventListener('ongridstatechanged', this.gridStateChanged.bind(this)); - // gridContainerElm.addEventListener('onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel')); - // gridContainerElm.addEventListener('onafterexporttoexcel', () => console.log('onAfterExportToExcel')); + this.eventService.addElementEventListener(gridContainerElm, 'ongridstatechanged', this.gridStateChanged.bind(this)); + // this.eventService.addElementEventListener(gridContainerElm, 'onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel')); + // this.eventService.addElementEventListener(gridContainerElm, 'onafterexporttoexcel', () => console.log('onAfterExportToExcel')); this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, []); } diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example10.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example10.ts index 9cad6d3f0..6ca6134f4 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example10.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example10.ts @@ -16,11 +16,13 @@ import * as moment from 'moment-mini'; import { ExampleGridOptions } from './example-grid-options'; import { TranslateService } from '../translate.service'; import './example10.scss'; +import { EventService } from './event.service'; const defaultPageSize = 20; const GRAPHQL_QUERY_DATASET_NAME = 'users'; export class Example10 { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; dataset = []; @@ -37,6 +39,7 @@ export class Example10 { translateService: TranslateService; constructor() { + this.eventService = new EventService(); // get the Translate Service from the window object, // it might be better with proper Dependency Injection but this project doesn't have any at this point this.translateService = (window).TranslateService; @@ -49,6 +52,7 @@ export class Example10 { if (this.sgb) { this.sgb?.dispose(); } + this.eventService.unbindAllEvents(); // this.saveCurrentGridState(); } @@ -56,8 +60,8 @@ export class Example10 { this.initializeGrid(); const gridContainerElm = document.querySelector(`.grid10`); - // gridContainerElm.addEventListener('onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel')); - // gridContainerElm.addEventListener('onafterexporttoexcel', () => console.log('onAfterExportToExcel')); + // this.eventService.addElementEventListener(gridContainerElm, 'onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel')); + // this.eventService.addElementEventListener(gridContainerElm, 'onafterexporttoexcel', () => console.log('onAfterExportToExcel')); this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); } diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example11-modal.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example11-modal.ts index 913e09925..a467c7899 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example11-modal.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example11-modal.ts @@ -10,8 +10,10 @@ import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bu import { ExampleGridOptions } from './example-grid-options'; import '../salesforce-styles.scss'; import './example11-modal.scss'; +import { EventService } from './event.service'; export class Example11Modal { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; sgb: SlickVanillaGridBundle; @@ -19,6 +21,10 @@ export class Example11Modal { remoteCallbackFn: any; selectedIds: string[] = []; + constructor() { + this.eventService = new EventService(); + } + attached() { this.openBulmaModal(this.handleOnModalClose.bind(this)); this.initializeGrid(); @@ -29,7 +35,7 @@ export class Example11Modal { if (bindings.columnDefinitions) { this.columnDefinitions = bindings.columnDefinitions; this.gridContainerElm = document.querySelector(`.modal-grid`); - this.gridContainerElm.addEventListener('onvalidationerror', this.handleValidationError.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'onvalidationerror', this.handleValidationError.bind(this)); const dataset = [this.createEmptyItem(bindings.columnDefinitions)]; this.sgb = new Slicker.GridBundle(this.gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, dataset); @@ -44,6 +50,8 @@ export class Example11Modal { dispose() { this.sgb?.dispose(); + this.eventService.unbindAllEvents(); + this.gridContainerElm = null; } initializeGrid() { diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts index 24a257fa2..88baedd08 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts @@ -24,6 +24,7 @@ import { ExampleGridOptions } from './example-grid-options'; import { loadComponent } from 'examples/utilities'; import '../salesforce-styles.scss'; import './example11.scss'; +import { EventService } from './event.service'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; @@ -55,6 +56,7 @@ export interface FilterPreset { } export class Example11 { + private eventService: EventService; columnDefinitions: Column[]; gridOptions: GridOption; dataset: any[] = []; @@ -93,6 +95,10 @@ export class Example11 { return this.sgb?.instances; } + constructor() { + this.eventService = new EventService(); + } + attached() { this.initializeGrid(); this.dataset = this.loadData(500); @@ -101,13 +107,15 @@ export class Example11 { this.sgb = new Slicker.GridBundle(this.gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); // bind any of the grid events - this.gridContainerElm.addEventListener('onvalidationerror', this.handleValidationError.bind(this)); - this.gridContainerElm.addEventListener('onitemdeleted', this.handleItemDeleted.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'onvalidationerror', this.handleValidationError.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'onitemdeleted', this.handleItemDeleted.bind(this)); this.recreatePredefinedFilters(); } dispose() { this.sgb?.dispose(); + this.eventService.unbindAllEvents(); + this.gridContainerElm = null; } initializeGrid() { diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts index e088fa371..a826c326b 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts @@ -18,6 +18,7 @@ import { import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Slicker, SlickCompositeEditorComponent, SlickerGridInstance, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { EventService } from './event.service'; import { ExampleGridOptions } from './example-grid-options'; import '../salesforce-styles.scss'; import './example12.scss'; @@ -74,6 +75,7 @@ const customEditableInputFormatter = (_row, _cell, value, columnDef, dataContext }; export class Example12 { + private eventService: EventService; compositeEditorInstance: SlickCompositeEditorComponent; columnDefinitions: Column[]; gridOptions: GridOption; @@ -90,6 +92,10 @@ export class Example12 { return this.sgb?.instances; } + constructor() { + this.eventService = new EventService(); + } + attached() { this.initializeGrid(); this.dataset = this.loadData(500); @@ -99,18 +105,20 @@ export class Example12 { // this.sgb.slickGrid.setActiveCell(0, 0); // bind any of the grid events - this.gridContainerElm.addEventListener('onvalidationerror', this.handleValidationError.bind(this)); - this.gridContainerElm.addEventListener('onitemdeleted', this.handleItemDeleted.bind(this)); - this.gridContainerElm.addEventListener('onbeforeeditcell', this.handleOnBeforeEditCell.bind(this)); - this.gridContainerElm.addEventListener('oncellchange', this.handleOnCellChange.bind(this)); - this.gridContainerElm.addEventListener('onclick', this.handleOnCellClicked.bind(this)); - this.gridContainerElm.addEventListener('ongridstatechanged', this.handleOnSelectedRowsChanged.bind(this)); - this.gridContainerElm.addEventListener('ondblclick', () => this.openCompositeModal('edit', 50)); - this.gridContainerElm.addEventListener('oncompositeeditorchange', this.handleOnCompositeEditorChange.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'onvalidationerror', this.handleValidationError.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'onitemdeleted', this.handleItemDeleted.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'onbeforeeditcell', this.handleOnBeforeEditCell.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'oncellchange', this.handleOnCellChange.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'onclick', this.handleOnCellClicked.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'ongridstatechanged', this.handleOnSelectedRowsChanged.bind(this)); + this.eventService.addElementEventListener(this.gridContainerElm, 'ondblclick', () => this.openCompositeModal('edit', 50)); + this.eventService.addElementEventListener(this.gridContainerElm, 'oncompositeeditorchange', this.handleOnCompositeEditorChange.bind(this)); } dispose() { this.sgb?.dispose(); + this.eventService.unbindAllEvents(); + this.gridContainerElm = null; } initializeGrid() { diff --git a/examples/webpack-demo-vanilla-bundle/src/renderer.ts b/examples/webpack-demo-vanilla-bundle/src/renderer.ts index bb10af4d3..af6efb8b3 100644 --- a/examples/webpack-demo-vanilla-bundle/src/renderer.ts +++ b/examples/webpack-demo-vanilla-bundle/src/renderer.ts @@ -21,7 +21,9 @@ export class Renderer { } dispose() { - this._observers = []; + for (const observer of this._observers) { + observer.unbindAll(); + } } getModuleClassName(module: any): string { diff --git a/packages/common/src/editors/__tests__/checkboxEditor.spec.ts b/packages/common/src/editors/__tests__/checkboxEditor.spec.ts index 20b678049..f3c83c305 100644 --- a/packages/common/src/editors/__tests__/checkboxEditor.spec.ts +++ b/packages/common/src/editors/__tests__/checkboxEditor.spec.ts @@ -457,6 +457,7 @@ describe('CheckboxEditor', () => { editor.loadValue(mockItemData); editor.editorDomElement.checked = true; editor.editorDomElement.dispatchEvent(new (window.window as any).Event('change')); + editor.destroy(); expect(getCellSpy).toHaveBeenCalled(); expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub }); diff --git a/packages/common/src/editors/__tests__/dualInputEditor.spec.ts b/packages/common/src/editors/__tests__/dualInputEditor.spec.ts index 343689c28..6310ab738 100644 --- a/packages/common/src/editors/__tests__/dualInputEditor.spec.ts +++ b/packages/common/src/editors/__tests__/dualInputEditor.spec.ts @@ -898,7 +898,7 @@ describe('DualInputEditor', () => { editor = new DualInputEditor(editorArguments); editor.loadValue(mockItemData); editor.setValues([4, 5]); - editor.editorDomElement.leftInput.dispatchEvent(new (window.window as any).Event('keyup')); + editor.editorDomElement.leftInput.dispatchEvent(new (window.window as any).Event('input')); jest.runTimersToTime(50); @@ -922,9 +922,10 @@ describe('DualInputEditor', () => { editor = new DualInputEditor(editorArguments); editor.loadValue(mockItemData); editor.setValues([4, 5]); - editor.editorDomElement.rightInput.dispatchEvent(new (window.window as any).Event('keyup')); + editor.editorDomElement.rightInput.dispatchEvent(new (window.window as any).Event('input')); jest.runTimersToTime(50); + editor.destroy(); expect(getCellSpy).toHaveBeenCalled(); expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub }); diff --git a/packages/common/src/editors/__tests__/floatEditor.spec.ts b/packages/common/src/editors/__tests__/floatEditor.spec.ts index d49399dd3..37c59725f 100644 --- a/packages/common/src/editors/__tests__/floatEditor.spec.ts +++ b/packages/common/src/editors/__tests__/floatEditor.spec.ts @@ -682,7 +682,7 @@ describe('FloatEditor', () => { expect(editor.editorDomElement.checked).toEqual(false); }); - it('should expect "onCompositeEditorChange" to have been triggered by keyup with the new value showing up in its "formValues" object', () => { + it('should expect "onCompositeEditorChange" to have been triggered by input change with the new value showing up in its "formValues" object', () => { jest.useFakeTimers(); const activeCellMock = { row: 0, cell: 0 }; const getCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); @@ -694,7 +694,7 @@ describe('FloatEditor', () => { editor = new FloatEditor(editorArguments); editor.loadValue(mockItemData); editor.editorDomElement.value = 35; - editor.editorDomElement.dispatchEvent(new (window.window as any).Event('keyup')); + editor.editorDomElement.dispatchEvent(new (window.window as any).Event('input')); jest.runTimersToTime(50); @@ -718,7 +718,31 @@ describe('FloatEditor', () => { editor = new FloatEditor(editorArguments); editor.loadValue(mockItemData); editor.editorDomElement.value = 35; - editor.editorDomElement.dispatchEvent(new (window.window as any).Event('change')); + editor.editorDomElement.dispatchEvent(new (window.window as any).Event('input')); + + jest.runTimersToTime(50); + + expect(getCellSpy).toHaveBeenCalled(); + expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub }); + expect(onBeforeCompositeSpy).toHaveBeenCalledWith({ + ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub, + formValues: { price: 35 }, editors: {} + }, expect.anything()); + }); + + it('should expect "onCompositeEditorChange" to have been triggered by mouse wheel (spinner) with the new value showing up in its "formValues" object', () => { + jest.useFakeTimers(); + const activeCellMock = { row: 0, cell: 0 }; + const getCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); + const onBeforeEditSpy = jest.spyOn(gridStub.onBeforeEditCell, 'notify').mockReturnValue(undefined); + const onBeforeCompositeSpy = jest.spyOn(gridStub.onCompositeEditorChange, 'notify').mockReturnValue(false); + gridOptionMock.autoCommitEdit = true; + mockItemData = { id: 1, price: 35, isActive: true }; + + editor = new FloatEditor(editorArguments); + editor.loadValue(mockItemData); + editor.editorDomElement.value = 35; + editor.editorDomElement.dispatchEvent(new (window.window as any).Event('wheel')); jest.runTimersToTime(50); diff --git a/packages/common/src/editors/__tests__/integerEditor.spec.ts b/packages/common/src/editors/__tests__/integerEditor.spec.ts index b64ef3455..af07c6d49 100644 --- a/packages/common/src/editors/__tests__/integerEditor.spec.ts +++ b/packages/common/src/editors/__tests__/integerEditor.spec.ts @@ -612,7 +612,7 @@ describe('IntegerEditor', () => { expect(editor.editorDomElement.checked).toEqual(false); }); - it('should expect "onCompositeEditorChange" to have been triggered by keyup with the new value showing up in its "formValues" object', () => { + it('should expect "onCompositeEditorChange" to have been triggered by input change with the new value showing up in its "formValues" object', () => { jest.useFakeTimers(); const activeCellMock = { row: 0, cell: 0 }; const getCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); @@ -624,7 +624,7 @@ describe('IntegerEditor', () => { editor = new IntegerEditor(editorArguments); editor.loadValue(mockItemData); editor.editorDomElement.value = 35; - editor.editorDomElement.dispatchEvent(new (window.window as any).Event('keyup')); + editor.editorDomElement.dispatchEvent(new (window.window as any).Event('input')); jest.runTimersToTime(50); @@ -636,7 +636,7 @@ describe('IntegerEditor', () => { }, expect.anything()); }); - it('should expect "onCompositeEditorChange" to have been triggered by change (number spinner) with the new value showing up in its "formValues" object', () => { + it('should expect "onCompositeEditorChange" to have been triggered by by mouse wheel (spinner) with the new value showing up in its "formValues" object', () => { jest.useFakeTimers(); const activeCellMock = { row: 0, cell: 0 }; const getCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); @@ -648,7 +648,7 @@ describe('IntegerEditor', () => { editor = new IntegerEditor(editorArguments); editor.loadValue(mockItemData); editor.editorDomElement.value = 35; - editor.editorDomElement.dispatchEvent(new (window.window as any).Event('change')); + editor.editorDomElement.dispatchEvent(new (window.window as any).Event('wheel')); jest.runTimersToTime(50); diff --git a/packages/common/src/editors/__tests__/longTextEditor.spec.ts b/packages/common/src/editors/__tests__/longTextEditor.spec.ts index 245c9e0c7..1c5d3aa62 100644 --- a/packages/common/src/editors/__tests__/longTextEditor.spec.ts +++ b/packages/common/src/editors/__tests__/longTextEditor.spec.ts @@ -223,7 +223,7 @@ describe('LongTextEditor', () => { describe('isValueChanged method', () => { it('should return True when previously dispatched keyboard event is a new char "a" and it should also update the text counter accordingly', () => { const eventKeyDown = new (window.window as any).KeyboardEvent('keydown', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true }); - const eventKeyUp = new (window.window as any).KeyboardEvent('keyup', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true }); + const eventInput = new (window.window as any).KeyboardEvent('input', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true }); mockColumn.internalColumnEditor.maxLength = 255; editor = new LongTextEditor(editorArguments); @@ -234,7 +234,7 @@ describe('LongTextEditor', () => { editor.focus(); editorElm.dispatchEvent(eventKeyDown); - editorElm.dispatchEvent(eventKeyUp); + editorElm.dispatchEvent(eventInput); expect(currentTextLengthElm.textContent).toBe('1'); expect(maxTextLengthElm.textContent).toBe('255'); @@ -727,7 +727,7 @@ describe('LongTextEditor', () => { editor.loadValue(mockItemData); const editorElm = document.body.querySelector('.editor-title textarea'); editorElm.value = 'task 2'; - editorElm.dispatchEvent(new (window.window as any).Event('keyup')); + editorElm.dispatchEvent(new (window.window as any).Event('input')); jest.runTimersToTime(50); diff --git a/packages/common/src/editors/__tests__/textEditor.spec.ts b/packages/common/src/editors/__tests__/textEditor.spec.ts index 834fb6e04..f5ca146f5 100644 --- a/packages/common/src/editors/__tests__/textEditor.spec.ts +++ b/packages/common/src/editors/__tests__/textEditor.spec.ts @@ -589,9 +589,10 @@ describe('TextEditor', () => { editor = new TextEditor(editorArguments); editor.loadValue(mockItemData); editor.editorDomElement.value = 'task 2'; - editor.editorDomElement.dispatchEvent(new (window.window as any).Event('keyup')); + editor.editorDomElement.dispatchEvent(new (window.window as any).Event('input')); jest.runTimersToTime(50); + editor.destroy(); expect(getCellSpy).toHaveBeenCalled(); expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub }); diff --git a/packages/common/src/editors/autoCompleteEditor.ts b/packages/common/src/editors/autoCompleteEditor.ts index 9b2549e28..e77913124 100644 --- a/packages/common/src/editors/autoCompleteEditor.ts +++ b/packages/common/src/editors/autoCompleteEditor.ts @@ -36,7 +36,7 @@ export class AutoCompleteEditor implements Editor { private _autoCompleteOptions: AutocompleteOption; private _currentValue: any; private _defaultTextValue: string; - private _elementCollection: any[]; + private _elementCollection: any[] | null; private _lastInputKeyEvent: JQuery.Event; /** The JQuery DOM element */ @@ -86,7 +86,7 @@ export class AutoCompleteEditor implements Editor { } /** Getter for the Final Collection used in the AutoCompleted Source (this may vary from the "collection" especially when providing a customStructure) */ - get elementCollection(): any[] { + get elementCollection(): any[] | null { return this._elementCollection; } @@ -157,8 +157,9 @@ export class AutoCompleteEditor implements Editor { if (this._$editorElm) { this._$editorElm.autocomplete('destroy'); this._$editorElm.off('keydown.nav').remove(); - this._$editorElm = null; } + this._$editorElm = null; + this._elementCollection = null; } /** diff --git a/packages/common/src/editors/checkboxEditor.ts b/packages/common/src/editors/checkboxEditor.ts index bff9b7f3c..811d545af 100644 --- a/packages/common/src/editors/checkboxEditor.ts +++ b/packages/common/src/editors/checkboxEditor.ts @@ -90,11 +90,15 @@ export class CheckboxEditor implements Editor { destroy() { const columnId = this.columnDef && this.columnDef.id; + const compositeEditorOptions = this.args.compositeEditorOptions; const elm = document.querySelector(`.editor-checkbox.editor-${columnId}`); if (elm) { elm.removeEventListener('click', this.save); } if (this._input?.remove) { + if (compositeEditorOptions) { + this._input.removeEventListener('change', this.handleChangeOnCompositeEditor.bind(compositeEditorOptions)); + } this._input.remove(); } this._input = null; diff --git a/packages/common/src/editors/dualInputEditor.ts b/packages/common/src/editors/dualInputEditor.ts index 501076b8e..291661162 100644 --- a/packages/common/src/editors/dualInputEditor.ts +++ b/packages/common/src/editors/dualInputEditor.ts @@ -120,13 +120,8 @@ export class DualInputEditor implements Editor { const compositeEditorOptions = this.args?.compositeEditorOptions; if (compositeEditorOptions) { - const typingDelay = this.gridOptions?.editorTypingDebounce ?? 500; - this._leftInput.addEventListener('keyup', (event: KeyboardEvent) => { - debounce(() => this.handleChangeOnCompositeEditor(event, compositeEditorOptions), typingDelay)(); - }); - this._rightInput.addEventListener('keyup', (event: KeyboardEvent) => { - debounce(() => this.handleChangeOnCompositeEditor(event, compositeEditorOptions), typingDelay)(); - }); + this._leftInput.addEventListener('input', this.handleChangeOnCompositeEditorDebounce.bind(this)); + this._rightInput.addEventListener('input', this.handleChangeOnCompositeEditorDebounce.bind(this)); } else { setTimeout(() => this._leftInput.select(), 50); } @@ -158,9 +153,13 @@ export class DualInputEditor implements Editor { this._eventHandler.unsubscribeAll(); const columnId = this.columnDef && this.columnDef.id; + const compositeEditorOptions = this.args?.compositeEditorOptions; const elements = document.querySelectorAll(`.dual-editor-text.editor-${columnId}`); if (elements.length > 0) { elements.forEach((elm) => elm.removeEventListener('focusout', this.handleFocusOut.bind(this))); + if (compositeEditorOptions) { + elements.forEach((elm) => elm.removeEventListener('input', this.handleChangeOnCompositeEditorDebounce.bind(this))); + } } } @@ -499,4 +498,12 @@ export class DualInputEditor implements Editor { } grid.onCompositeEditorChange.notify({ ...activeCell, item, grid, column, formValues: compositeEditorOptions.formValues, editors: compositeEditorOptions.editors }, { ...new Slick.EventData(), ...event }); } + + private handleChangeOnCompositeEditorDebounce(event: KeyboardEvent) { + const compositeEditorOptions = this.args?.compositeEditorOptions; + if (compositeEditorOptions) { + const typingDelay = this.gridOptions?.editorTypingDebounce ?? 500; + debounce(() => this.handleChangeOnCompositeEditor(event, compositeEditorOptions), typingDelay)(); + } + } } diff --git a/packages/common/src/editors/floatEditor.ts b/packages/common/src/editors/floatEditor.ts index ac6ee9f18..a12474fe7 100644 --- a/packages/common/src/editors/floatEditor.ts +++ b/packages/common/src/editors/floatEditor.ts @@ -95,9 +95,8 @@ export class FloatEditor implements Editor { } if (compositeEditorOptions) { - this._input.addEventListener('keyup', this.handleOnKeyUp.bind(this)); - this._input.addEventListener('change', this.handleOnChange.bind(this)); - this._input.addEventListener('wheel', this.handleOnChange.bind(this)); + this._input.addEventListener('input', this.handleOnInputChange.bind(this)); + this._input.addEventListener('wheel', this.handleOnMouseWheel.bind(this)); } } } @@ -105,9 +104,8 @@ export class FloatEditor implements Editor { destroy() { if (this._input) { this._input.removeEventListener('focusout', this.save); - this._input.removeEventListener('keyup', this.handleOnKeyUp); - this._input.removeEventListener('change', this.handleOnChange); - this._input.removeEventListener('wheel', this.handleOnChange.bind(this)); + this._input.removeEventListener('input', this.handleOnInputChange); + this._input.removeEventListener('wheel', this.handleOnMouseWheel.bind(this)); setTimeout(() => { if (this._input) { this._input.remove(); @@ -323,14 +321,14 @@ export class FloatEditor implements Editor { } /** When the input value changes (this will cover the input spinner arrows on the right) */ - private handleOnChange(event: KeyboardEvent) { + private handleOnMouseWheel(event: KeyboardEvent) { const compositeEditorOptions = this.args.compositeEditorOptions; if (compositeEditorOptions) { this.handleChangeOnCompositeEditor(event, compositeEditorOptions); } } - private handleOnKeyUp(event: KeyboardEvent) { + private handleOnInputChange(event: KeyboardEvent) { const compositeEditorOptions = this.args.compositeEditorOptions; if (compositeEditorOptions) { const typingDelay = this.gridOptions?.editorTypingDebounce ?? 500; diff --git a/packages/common/src/editors/integerEditor.ts b/packages/common/src/editors/integerEditor.ts index 588e3b744..0e229bb5f 100644 --- a/packages/common/src/editors/integerEditor.ts +++ b/packages/common/src/editors/integerEditor.ts @@ -93,9 +93,8 @@ export class IntegerEditor implements Editor { } if (compositeEditorOptions) { - this._input.addEventListener('keyup', this.handleOnKeyUp.bind(this)); - this._input.addEventListener('change', this.handleOnChange.bind(this)); - this._input.addEventListener('wheel', this.handleOnChange.bind(this)); + this._input.addEventListener('input', this.handleOnInputChange.bind(this)); + this._input.addEventListener('wheel', this.handleOnMouseWheel.bind(this)); } } } @@ -103,9 +102,8 @@ export class IntegerEditor implements Editor { destroy() { if (this._input) { this._input.removeEventListener('focusout', this.save); - this._input.removeEventListener('keyup', this.handleOnKeyUp); - this._input.removeEventListener('change', this.handleOnChange); - this._input.removeEventListener('wheel', this.handleOnChange); + this._input.removeEventListener('input', this.handleOnInputChange); + this._input.removeEventListener('wheel', this.handleOnMouseWheel); setTimeout(() => { if (this._input) { this._input.remove(); @@ -285,14 +283,14 @@ export class IntegerEditor implements Editor { } /** When the input value changes (this will cover the input spinner arrows on the right) */ - private handleOnChange(event: KeyboardEvent) { + private handleOnMouseWheel(event: KeyboardEvent) { const compositeEditorOptions = this.args.compositeEditorOptions; if (compositeEditorOptions) { this.handleChangeOnCompositeEditor(event, compositeEditorOptions); } } - private handleOnKeyUp(event: KeyboardEvent) { + private handleOnInputChange(event: KeyboardEvent) { const compositeEditorOptions = this.args.compositeEditorOptions; if (compositeEditorOptions) { const typingDelay = this.gridOptions?.editorTypingDebounce ?? 500; diff --git a/packages/common/src/editors/longTextEditor.ts b/packages/common/src/editors/longTextEditor.ts index 1dfa23c46..ca398222e 100644 --- a/packages/common/src/editors/longTextEditor.ts +++ b/packages/common/src/editors/longTextEditor.ts @@ -129,7 +129,7 @@ export class LongTextEditor implements Editor { editorFooterElm.appendTo(this._$wrapper); this._$textarea.on('keydown', this.handleKeyDown.bind(this)); - this._$textarea.on('keyup', this.handleKeyUp.bind(this)); + this._$textarea.on('input', this.handleOnInputChange.bind(this)); if (!compositeEditorOptions) { this.position(this.args && this.args.position); @@ -165,7 +165,7 @@ export class LongTextEditor implements Editor { destroy() { if (this._$textarea) { this._$textarea.off('keydown'); - this._$textarea.off('keyup'); + this._$textarea.off('input'); } if (this._$wrapper) { this._$wrapper.find('.btn-save').off('click'); @@ -341,8 +341,8 @@ export class LongTextEditor implements Editor { } } - /** On every keyup event, we'll update the current text length counter */ - private handleKeyUp(event: KeyboardEvent & { target: HTMLTextAreaElement }) { + /** On every input change event, we'll update the current text length counter */ + private handleOnInputChange(event: KeyboardEvent & { target: HTMLTextAreaElement }) { const compositeEditorOptions = this.args.compositeEditorOptions; const textLength = event.target.value.length; this._$currentLengthElm.text(textLength); diff --git a/packages/common/src/editors/textEditor.ts b/packages/common/src/editors/textEditor.ts index 54147fe9a..2a10bedaa 100644 --- a/packages/common/src/editors/textEditor.ts +++ b/packages/common/src/editors/textEditor.ts @@ -90,14 +90,18 @@ export class TextEditor implements Editor { } if (compositeEditorOptions) { - this._input.addEventListener('keyup', this.handleOnKeyUp.bind(this)); + this._input.addEventListener('input', this.handleOnInputChange.bind(this)); } } destroy() { + const compositeEditorOptions = this.args.compositeEditorOptions; + if (this._input) { this._input.removeEventListener('focusout', this.save); - this._input.removeEventListener('keyup', this.handleOnKeyUp); + if (compositeEditorOptions) { + this._input.removeEventListener('input', this.handleOnInputChange); + } setTimeout(() => { if (this._input) { this._input.remove(); @@ -271,7 +275,7 @@ export class TextEditor implements Editor { grid.onCompositeEditorChange.notify({ ...activeCell, item, grid, column, formValues: compositeEditorOptions.formValues, editors: compositeEditorOptions.editors }, { ...new Slick.EventData(), ...event }); } - private handleOnKeyUp(event: KeyboardEvent) { + private handleOnInputChange(event: KeyboardEvent) { const compositeEditorOptions = this.args.compositeEditorOptions; if (compositeEditorOptions) { const typingDelay = this.gridOptions?.editorTypingDebounce ?? 500; diff --git a/packages/common/src/extensions/cellExternalCopyManagerExtension.ts b/packages/common/src/extensions/cellExternalCopyManagerExtension.ts index e7c297d15..9b6639797 100644 --- a/packages/common/src/extensions/cellExternalCopyManagerExtension.ts +++ b/packages/common/src/extensions/cellExternalCopyManagerExtension.ts @@ -23,7 +23,7 @@ declare const Slick: SlickNamespace; export class CellExternalCopyManagerExtension implements Extension { private _addon: SlickCellExternalCopyManager | null; - private _addonOptions: ExcelCopyBufferOption; + private _addonOptions: ExcelCopyBufferOption | null; private _cellSelectionModel: SlickCellSelectionModel; private _eventHandler: SlickEventHandler; private _commandQueue: EditCommand[]; @@ -33,7 +33,7 @@ export class CellExternalCopyManagerExtension implements Extension { this._eventHandler = new Slick.EventHandler() as SlickEventHandler; } - get addonOptions(): ExcelCopyBufferOption { + get addonOptions(): ExcelCopyBufferOption | null { return this._addonOptions; } @@ -59,6 +59,8 @@ export class CellExternalCopyManagerExtension implements Extension { if (this._cellSelectionModel?.destroy) { this._cellSelectionModel.destroy(); } + this.extensionUtility.nullifyFunctionNameStartingWithOn(this._addonOptions); + this._addonOptions = null; document.removeEventListener('keydown', this.hookUndoShortcutKey.bind(this)); } @@ -85,29 +87,29 @@ export class CellExternalCopyManagerExtension implements Extension { } // hook to all possible events - if (this.sharedService.slickGrid && this.sharedService.gridOptions.excelCopyBufferOptions) { - if (this._addon && this.sharedService.gridOptions.excelCopyBufferOptions.onExtensionRegistered) { - this.sharedService.gridOptions.excelCopyBufferOptions.onExtensionRegistered(this._addon); + if (this.sharedService.slickGrid && this._addonOptions) { + if (this._addon && this._addonOptions.onExtensionRegistered) { + this._addonOptions.onExtensionRegistered(this._addon); } const onCopyCellsHandler = this._addon.onCopyCells; (this._eventHandler as SlickEventHandler>).subscribe(onCopyCellsHandler, (e, args) => { - if (this.sharedService.gridOptions.excelCopyBufferOptions && typeof this.sharedService.gridOptions.excelCopyBufferOptions.onCopyCells === 'function') { - this.sharedService.gridOptions.excelCopyBufferOptions.onCopyCells(e, args); + if (this._addonOptions && typeof this._addonOptions.onCopyCells === 'function') { + this._addonOptions.onCopyCells(e, args); } }); const onCopyCancelledHandler = this._addon.onCopyCancelled; (this._eventHandler as SlickEventHandler>).subscribe(onCopyCancelledHandler, (e, args) => { - if (this.sharedService.gridOptions.excelCopyBufferOptions && typeof this.sharedService.gridOptions.excelCopyBufferOptions.onCopyCancelled === 'function') { - this.sharedService.gridOptions.excelCopyBufferOptions.onCopyCancelled(e, args); + if (this._addonOptions && typeof this._addonOptions.onCopyCancelled === 'function') { + this._addonOptions.onCopyCancelled(e, args); } }); const onPasteCellsHandler = this._addon.onPasteCells; (this._eventHandler as SlickEventHandler>).subscribe(onPasteCellsHandler, (e, args) => { - if (this.sharedService.gridOptions.excelCopyBufferOptions && typeof this.sharedService.gridOptions.excelCopyBufferOptions.onPasteCells === 'function') { - this.sharedService.gridOptions.excelCopyBufferOptions.onPasteCells(e, args); + if (this._addonOptions && typeof this._addonOptions.onPasteCells === 'function') { + this._addonOptions.onPasteCells(e, args); } }); } diff --git a/packages/common/src/extensions/cellMenuExtension.ts b/packages/common/src/extensions/cellMenuExtension.ts index 1cdd455f6..636f9c3f7 100644 --- a/packages/common/src/extensions/cellMenuExtension.ts +++ b/packages/common/src/extensions/cellMenuExtension.ts @@ -1,6 +1,7 @@ import { Constants } from '../constants'; import { ExtensionName } from '../enums/extensionName.enum'; import { + CellMenu, CellMenuOption, Column, Extension, @@ -21,6 +22,7 @@ declare const Slick: SlickNamespace; export class CellMenuExtension implements Extension { private _addon: SlickCellMenu | null; + private _cellMenuOptions: CellMenu | null; private _eventHandler: SlickEventHandler; private _locales: Locale; @@ -42,8 +44,10 @@ export class CellMenuExtension implements Extension { if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } + this.extensionUtility.nullifyFunctionNameStartingWithOn(this._cellMenuOptions); + this._addon = null; + this._cellMenuOptions = null; } /** Get the instance of the SlickGrid addon (control or plugin). */ @@ -64,7 +68,8 @@ export class CellMenuExtension implements Extension { // dynamically import the SlickGrid plugin (addon) with RequireJS this.extensionUtility.loadExtensionDynamically(ExtensionName.cellMenu); - this.sharedService.gridOptions.cellMenu = { ...this.getDefaultCellMenuOptions(), ...this.sharedService.gridOptions.cellMenu }; + this._cellMenuOptions = { ...this.getDefaultCellMenuOptions(), ...this.sharedService.gridOptions.cellMenu }; + this.sharedService.gridOptions.cellMenu = this._cellMenuOptions; // translate the item keys when necessary if (this.sharedService.gridOptions.enableTranslate) { @@ -74,15 +79,15 @@ export class CellMenuExtension implements Extension { // sort all menu items by their position order when defined this.sortMenuItems(this.sharedService.allColumns); - this._addon = new Slick.Plugins.CellMenu(this.sharedService.gridOptions.cellMenu); + this._addon = new Slick.Plugins.CellMenu(this._cellMenuOptions); if (this._addon) { this.sharedService.slickGrid.registerPlugin(this._addon); } // hook all events - if (this.sharedService.slickGrid && this.sharedService.gridOptions.cellMenu) { - if (this._addon && this.sharedService.gridOptions.cellMenu.onExtensionRegistered) { - this.sharedService.gridOptions.cellMenu.onExtensionRegistered(this._addon); + if (this.sharedService.slickGrid && this._cellMenuOptions) { + if (this._addon && this._cellMenuOptions.onExtensionRegistered) { + this._cellMenuOptions.onExtensionRegistered(this._addon); } if (cellMenu && typeof cellMenu.onCommand === 'function') { const onCommandHandler = this._addon.onCommand; @@ -132,7 +137,7 @@ export class CellMenuExtension implements Extension { /** Translate the Cell Menu titles, we need to loop through all column definition to re-translate them */ translateCellMenu() { - if (this.sharedService.gridOptions && this.sharedService.gridOptions.cellMenu) { + if (this.sharedService.gridOptions?.cellMenu) { this.resetMenuTranslations(this.sharedService.allColumns); } } diff --git a/packages/common/src/extensions/checkboxSelectorExtension.ts b/packages/common/src/extensions/checkboxSelectorExtension.ts index a8ffc1fec..5f2b360d4 100644 --- a/packages/common/src/extensions/checkboxSelectorExtension.ts +++ b/packages/common/src/extensions/checkboxSelectorExtension.ts @@ -15,8 +15,8 @@ export class CheckboxSelectorExtension implements Extension { dispose() { if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } + this._addon = null; if (this._rowSelectionPlugin?.destroy) { this._rowSelectionPlugin.destroy(); } diff --git a/packages/common/src/extensions/columnPickerExtension.ts b/packages/common/src/extensions/columnPickerExtension.ts index bf7d0861c..ce5243fff 100644 --- a/packages/common/src/extensions/columnPickerExtension.ts +++ b/packages/common/src/extensions/columnPickerExtension.ts @@ -1,5 +1,5 @@ import { ExtensionName } from '../enums/extensionName.enum'; -import { Extension, GetSlickEventType, SlickColumnPicker, SlickEventHandler, SlickNamespace } from '../interfaces/index'; +import { ColumnPicker, Extension, GetSlickEventType, SlickColumnPicker, SlickEventHandler, SlickNamespace } from '../interfaces/index'; import { ExtensionUtility } from './extensionUtility'; import { SharedService } from '../services/shared.service'; @@ -9,6 +9,7 @@ declare const Slick: SlickNamespace; export class ColumnPickerExtension implements Extension { private _eventHandler: SlickEventHandler; private _addon: SlickColumnPicker | null; + private _columnPicker: ColumnPicker | null; constructor(private extensionUtility: ExtensionUtility, private sharedService: SharedService) { this._eventHandler = new Slick.EventHandler(); @@ -23,8 +24,9 @@ export class ColumnPickerExtension implements Extension { this._eventHandler.unsubscribeAll(); if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } + this.extensionUtility.nullifyFunctionNameStartingWithOn(this._columnPicker); + this._addon = null; } /** Get the instance of the SlickGrid addon (control or plugin). */ @@ -43,20 +45,21 @@ export class ColumnPickerExtension implements Extension { const forceFitTitle = this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'columnPicker'); const syncResizeTitle = this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'columnPicker'); - this.sharedService.gridOptions.columnPicker = this.sharedService.gridOptions.columnPicker || {}; - this.sharedService.gridOptions.columnPicker.columnTitle = this.sharedService.gridOptions.columnPicker.columnTitle || columnTitle; - this.sharedService.gridOptions.columnPicker.forceFitTitle = this.sharedService.gridOptions.columnPicker.forceFitTitle || forceFitTitle; - this.sharedService.gridOptions.columnPicker.syncResizeTitle = this.sharedService.gridOptions.columnPicker.syncResizeTitle || syncResizeTitle; + this._columnPicker = this.sharedService.gridOptions.columnPicker || {}; + this.sharedService.gridOptions.columnPicker = this._columnPicker; + this._columnPicker.columnTitle = this._columnPicker.columnTitle || columnTitle; + this._columnPicker.forceFitTitle = this._columnPicker.forceFitTitle || forceFitTitle; + this._columnPicker.syncResizeTitle = this._columnPicker.syncResizeTitle || syncResizeTitle; this._addon = new Slick.Controls.ColumnPicker(this.sharedService.allColumns, this.sharedService.slickGrid, this.sharedService.gridOptions); if (this.sharedService.slickGrid && this.sharedService.gridOptions.enableColumnPicker) { - if (this._addon && this.sharedService.gridOptions.columnPicker.onExtensionRegistered) { - this.sharedService.gridOptions.columnPicker.onExtensionRegistered(this._addon); + if (this._addon && this._columnPicker.onExtensionRegistered) { + this._columnPicker.onExtensionRegistered(this._addon); } const onColumnsChangedHandler = this._addon.onColumnsChanged; (this._eventHandler as SlickEventHandler>).subscribe(onColumnsChangedHandler, (e, args) => { - if (this.sharedService.gridOptions.columnPicker && typeof this.sharedService.gridOptions.columnPicker.onColumnsChanged === 'function') { - this.sharedService.gridOptions.columnPicker.onColumnsChanged(e, args); + if (this._columnPicker && typeof this._columnPicker.onColumnsChanged === 'function') { + this._columnPicker.onColumnsChanged(e, args); } if (args && Array.isArray(args.columns) && args.columns.length !== this.sharedService.visibleColumns.length) { this.sharedService.visibleColumns = args.columns; @@ -70,30 +73,28 @@ export class ColumnPickerExtension implements Extension { /** Translate the Column Picker headers and also the last 2 checkboxes */ translateColumnPicker() { - if (this.sharedService && this.sharedService.slickGrid && this.sharedService.gridOptions) { - // update the properties by pointers, that is the only way to get Column Picker Control to see the new values - if (this.sharedService.gridOptions.columnPicker) { - this.emptyColumnPickerTitles(); - this.sharedService.gridOptions.columnPicker.columnTitle = this.extensionUtility.getPickerTitleOutputString('columnTitle', 'columnPicker'); - this.sharedService.gridOptions.columnPicker.forceFitTitle = this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'columnPicker'); - this.sharedService.gridOptions.columnPicker.syncResizeTitle = this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'columnPicker'); - } + // update the properties by pointers, that is the only way to get Column Picker Control to see the new values + if (this._columnPicker) { + this.emptyColumnPickerTitles(); + this._columnPicker.columnTitle = this.extensionUtility.getPickerTitleOutputString('columnTitle', 'columnPicker'); + this._columnPicker.forceFitTitle = this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'columnPicker'); + this._columnPicker.syncResizeTitle = this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'columnPicker'); + } - // translate all columns (including hidden columns) - this.extensionUtility.translateItems(this.sharedService.allColumns, 'nameKey', 'name'); + // translate all columns (including hidden columns) + this.extensionUtility.translateItems(this.sharedService.allColumns, 'nameKey', 'name'); - // update the Titles of each sections (command, customTitle, ...) - if (this._addon?.updateAllTitles && this.sharedService?.gridOptions?.columnPicker) { - this._addon.updateAllTitles(this.sharedService.gridOptions.columnPicker); - } + // update the Titles of each sections (command, customTitle, ...) + if (this._addon?.updateAllTitles && this._columnPicker) { + this._addon.updateAllTitles(this._columnPicker); } } private emptyColumnPickerTitles() { - if (this.sharedService && this.sharedService.gridOptions && this.sharedService.gridOptions.columnPicker) { - this.sharedService.gridOptions.columnPicker.columnTitle = ''; - this.sharedService.gridOptions.columnPicker.forceFitTitle = ''; - this.sharedService.gridOptions.columnPicker.syncResizeTitle = ''; + if (this._columnPicker) { + this._columnPicker.columnTitle = ''; + this._columnPicker.forceFitTitle = ''; + this._columnPicker.syncResizeTitle = ''; } } } diff --git a/packages/common/src/extensions/contextMenuExtension.ts b/packages/common/src/extensions/contextMenuExtension.ts index 011b7ef54..33da61bca 100644 --- a/packages/common/src/extensions/contextMenuExtension.ts +++ b/packages/common/src/extensions/contextMenuExtension.ts @@ -22,8 +22,9 @@ declare const Slick: SlickNamespace; export class ContextMenuExtension implements Extension { private _addon: SlickContextMenu | null; + private _contextMenuOptions: ContextMenu | null; private _eventHandler: SlickEventHandler; - private _userOriginalContextMenu: ContextMenu; + private _userOriginalContextMenu: ContextMenu | undefined; constructor( private extensionUtility: ExtensionUtility, @@ -44,11 +45,14 @@ export class ContextMenuExtension implements Extension { if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } if (this.sharedService.gridOptions && this.sharedService.gridOptions.contextMenu && this.sharedService.gridOptions.contextMenu.commandItems) { this.sharedService.gridOptions.contextMenu = this._userOriginalContextMenu; } + + this.extensionUtility.nullifyFunctionNameStartingWithOn(this._contextMenuOptions); + this._addon = null; + this._contextMenuOptions = null; } /** Get the instance of the SlickGrid addon (control or plugin). */ @@ -63,23 +67,24 @@ export class ContextMenuExtension implements Extension { } if (this.sharedService && this.sharedService.slickGrid && this.sharedService.gridOptions && this.sharedService.gridOptions.contextMenu) { - const contextMenu = this.sharedService.gridOptions.contextMenu; + this._contextMenuOptions = this.sharedService.gridOptions.contextMenu; // keep original user context menu, useful when switching locale to translate - this._userOriginalContextMenu = { ...contextMenu }; + this._userOriginalContextMenu = { ...this._contextMenuOptions }; // dynamically import the SlickGrid plugin (addon) with RequireJS this.extensionUtility.loadExtensionDynamically(ExtensionName.contextMenu); // merge the original commands with the built-in internal commands const originalCommandItems = this._userOriginalContextMenu && Array.isArray(this._userOriginalContextMenu.commandItems) ? this._userOriginalContextMenu.commandItems : []; - contextMenu.commandItems = [...originalCommandItems, ...this.addMenuCustomCommands(originalCommandItems)]; - this.sharedService.gridOptions.contextMenu = { ...contextMenu }; + this._contextMenuOptions.commandItems = [...originalCommandItems, ...this.addMenuCustomCommands(originalCommandItems)]; + this._contextMenuOptions = { ...this._contextMenuOptions }; + this.sharedService.gridOptions.contextMenu = this._contextMenuOptions; // sort all menu items by their position order when defined - this.extensionUtility.sortItems(contextMenu.commandItems || [], 'positionOrder'); - this.extensionUtility.sortItems(contextMenu.optionItems || [], 'positionOrder'); + this.extensionUtility.sortItems(this._contextMenuOptions.commandItems || [], 'positionOrder'); + this.extensionUtility.sortItems(this._contextMenuOptions.optionItems || [], 'positionOrder'); - this._addon = new Slick.Plugins.ContextMenu(contextMenu); + this._addon = new Slick.Plugins.ContextMenu(this._contextMenuOptions); if (this._addon) { this.sharedService.slickGrid.registerPlugin(this._addon); } @@ -90,47 +95,47 @@ export class ContextMenuExtension implements Extension { } // hook all events - if (this.sharedService.slickGrid && contextMenu) { - if (this._addon && contextMenu.onExtensionRegistered) { - contextMenu.onExtensionRegistered(this._addon); + if (this.sharedService.slickGrid && this._contextMenuOptions) { + if (this._addon && this._contextMenuOptions.onExtensionRegistered) { + this._contextMenuOptions.onExtensionRegistered(this._addon); } - if (contextMenu && typeof contextMenu.onCommand === 'function') { + if (this._contextMenuOptions && typeof this._contextMenuOptions.onCommand === 'function') { const onCommandHandler = this._addon.onCommand; (this._eventHandler as SlickEventHandler>).subscribe(onCommandHandler, (event, args) => { - if (contextMenu.onCommand) { - contextMenu.onCommand(event, args); + if (this._contextMenuOptions?.onCommand) { + this._contextMenuOptions.onCommand(event, args); } }); } - if (contextMenu && typeof contextMenu.onOptionSelected === 'function') { + if (this._contextMenuOptions && typeof this._contextMenuOptions.onOptionSelected === 'function') { const onOptionSelectedHandler = this._addon.onOptionSelected; (this._eventHandler as SlickEventHandler>).subscribe(onOptionSelectedHandler, (event, args) => { - if (contextMenu.onOptionSelected) { - contextMenu.onOptionSelected(event, args); + if (this._contextMenuOptions?.onOptionSelected) { + this._contextMenuOptions.onOptionSelected(event, args); } }); } - if (contextMenu && typeof contextMenu.onBeforeMenuShow === 'function') { + if (this._contextMenuOptions && typeof this._contextMenuOptions.onBeforeMenuShow === 'function') { const onBeforeMenuShowHandler = this._addon.onBeforeMenuShow; (this._eventHandler as SlickEventHandler>).subscribe(onBeforeMenuShowHandler, (event, args) => { - if (contextMenu.onBeforeMenuShow) { - contextMenu.onBeforeMenuShow(event, args); + if (this._contextMenuOptions?.onBeforeMenuShow) { + this._contextMenuOptions.onBeforeMenuShow(event, args); } }); } - if (contextMenu && typeof contextMenu.onBeforeMenuClose === 'function') { + if (this._contextMenuOptions && typeof this._contextMenuOptions.onBeforeMenuClose === 'function') { const onBeforeMenuCloseHandler = this._addon.onBeforeMenuClose; (this._eventHandler as SlickEventHandler>).subscribe(onBeforeMenuCloseHandler, (event, args) => { - if (contextMenu.onBeforeMenuClose) { - contextMenu.onBeforeMenuClose(event, args); + if (this._contextMenuOptions?.onBeforeMenuClose) { + this._contextMenuOptions.onBeforeMenuClose(event, args); } }); } - if (contextMenu && typeof contextMenu.onAfterMenuShow === 'function') { + if (this._contextMenuOptions && typeof this._contextMenuOptions.onAfterMenuShow === 'function') { const onAfterMenuShowHandler = this._addon.onAfterMenuShow; (this._eventHandler as SlickEventHandler>).subscribe(onAfterMenuShowHandler, (event, args) => { - if (contextMenu.onAfterMenuShow) { - contextMenu.onAfterMenuShow(event, args); + if (this._contextMenuOptions?.onAfterMenuShow) { + this._contextMenuOptions.onAfterMenuShow(event, args); } }); } diff --git a/packages/common/src/extensions/draggableGroupingExtension.ts b/packages/common/src/extensions/draggableGroupingExtension.ts index 648411b16..1b7690fee 100644 --- a/packages/common/src/extensions/draggableGroupingExtension.ts +++ b/packages/common/src/extensions/draggableGroupingExtension.ts @@ -1,5 +1,5 @@ import { ExtensionName } from '../enums/index'; -import { Extension, GetSlickEventType, GridOption, SlickDraggableGrouping, SlickEventHandler, SlickNamespace } from '../interfaces/index'; +import { DraggableGrouping, Extension, GetSlickEventType, GridOption, SlickDraggableGrouping, SlickEventHandler, SlickNamespace } from '../interfaces/index'; import { ExtensionUtility } from './extensionUtility'; import { SharedService } from '../services/shared.service'; @@ -8,6 +8,7 @@ declare const Slick: SlickNamespace; export class DraggableGroupingExtension implements Extension { private _addon: SlickDraggableGrouping | null; + private _draggableGroupingOptions: DraggableGrouping | null; private _eventHandler: SlickEventHandler; constructor(private extensionUtility: ExtensionUtility, private sharedService: SharedService) { @@ -24,8 +25,10 @@ export class DraggableGroupingExtension implements Extension { if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } + this.extensionUtility.nullifyFunctionNameStartingWithOn(this._draggableGroupingOptions); + this._addon = null; + this._draggableGroupingOptions = null; } /** @@ -57,15 +60,16 @@ export class DraggableGroupingExtension implements Extension { // Events if (this.sharedService.slickGrid && this.sharedService.gridOptions.draggableGrouping) { - if (this._addon && this.sharedService.gridOptions.draggableGrouping.onExtensionRegistered) { - this.sharedService.gridOptions.draggableGrouping.onExtensionRegistered(this._addon); + this._draggableGroupingOptions = this.sharedService.gridOptions.draggableGrouping; + if (this._addon && this._draggableGroupingOptions.onExtensionRegistered) { + this._draggableGroupingOptions.onExtensionRegistered(this._addon); } if (this._addon && this._addon.onGroupChanged) { const onGroupChangedHandler = this._addon.onGroupChanged; (this._eventHandler as SlickEventHandler>).subscribe(onGroupChangedHandler, (e, args) => { - if (this.sharedService.gridOptions.draggableGrouping && typeof this.sharedService.gridOptions.draggableGrouping.onGroupChanged === 'function') { - this.sharedService.gridOptions.draggableGrouping.onGroupChanged(e, args); + if (this._draggableGroupingOptions && typeof this._draggableGroupingOptions.onGroupChanged === 'function') { + this._draggableGroupingOptions.onGroupChanged(e, args); } }); } diff --git a/packages/common/src/extensions/extensionUtility.ts b/packages/common/src/extensions/extensionUtility.ts index 95fe43830..f6a6b0af0 100644 --- a/packages/common/src/extensions/extensionUtility.ts +++ b/packages/common/src/extensions/extensionUtility.ts @@ -111,6 +111,20 @@ export class ExtensionUtility { return output; } + /** + * Loop through object provided and set to null any property found starting with "onX" + * @param {Object}: obj + */ + nullifyFunctionNameStartingWithOn(obj?: any) { + if (obj) { + for (const prop of Object.keys(obj)) { + if (prop.startsWith('on')) { + obj[prop] = null; + } + } + } + } + /** * Sort items (by pointers) in an array by a property name * @params items array diff --git a/packages/common/src/extensions/gridMenuExtension.ts b/packages/common/src/extensions/gridMenuExtension.ts index 6f4fca8bf..9465b4004 100644 --- a/packages/common/src/extensions/gridMenuExtension.ts +++ b/packages/common/src/extensions/gridMenuExtension.ts @@ -31,6 +31,7 @@ export class GridMenuExtension implements Extension { private _addon: SlickGridMenu | null; private _areVisibleColumnDifferent = false; private _eventHandler: SlickEventHandler; + private _gridMenuOptions: GridMenu | null; private _userOriginalGridMenu: GridMenu; constructor( @@ -52,11 +53,13 @@ export class GridMenuExtension implements Extension { this._eventHandler.unsubscribeAll(); if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && this.sharedService.gridOptions.gridMenu.customItems) { this.sharedService.gridOptions.gridMenu = this._userOriginalGridMenu; } + this.extensionUtility.nullifyFunctionNameStartingWithOn(this._gridMenuOptions); + this._addon = null; + this._gridMenuOptions = null; } /** Get the instance of the SlickGrid addon (control or plugin). */ @@ -76,40 +79,41 @@ export class GridMenuExtension implements Extension { // dynamically import the SlickGrid plugin (addon) with RequireJS this.extensionUtility.loadExtensionDynamically(ExtensionName.gridMenu); - this.sharedService.gridOptions.gridMenu = { ...this.getDefaultGridMenuOptions(), ...this.sharedService.gridOptions.gridMenu }; + this._gridMenuOptions = { ...this.getDefaultGridMenuOptions(), ...this.sharedService.gridOptions.gridMenu }; + this.sharedService.gridOptions.gridMenu = this._gridMenuOptions; // merge original user grid menu items with internal items // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) const originalCustomItems = this._userOriginalGridMenu && Array.isArray(this._userOriginalGridMenu.customItems) ? this._userOriginalGridMenu.customItems : []; - this.sharedService.gridOptions.gridMenu.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; - this.extensionUtility.translateItems(this.sharedService.gridOptions.gridMenu.customItems, 'titleKey', 'title'); - this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.customItems, 'positionOrder'); + this._gridMenuOptions.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; + this.extensionUtility.translateItems(this._gridMenuOptions.customItems, 'titleKey', 'title'); + this.extensionUtility.sortItems(this._gridMenuOptions.customItems, 'positionOrder'); this._addon = new Slick.Controls.GridMenu(this.sharedService.allColumns, this.sharedService.slickGrid, this.sharedService.gridOptions); // hook all events - if (this.sharedService.slickGrid && this.sharedService.gridOptions.gridMenu) { - if (this.sharedService.gridOptions.gridMenu.onExtensionRegistered) { - this.sharedService.gridOptions.gridMenu.onExtensionRegistered(this._addon); + if (this.sharedService.slickGrid && this._gridMenuOptions) { + if (this._gridMenuOptions.onExtensionRegistered) { + this._gridMenuOptions.onExtensionRegistered(this._addon); } - if (this.sharedService.gridOptions.gridMenu && typeof this.sharedService.gridOptions.gridMenu.onBeforeMenuShow === 'function') { + if (this._gridMenuOptions && typeof this._gridMenuOptions.onBeforeMenuShow === 'function') { const onBeforeMenuShowHandler = this._addon.onBeforeMenuShow; if (onBeforeMenuShowHandler) { (this._eventHandler as SlickEventHandler>).subscribe(onBeforeMenuShowHandler, (e, args) => { - if (this.sharedService.gridOptions.gridMenu && this.sharedService.gridOptions.gridMenu.onBeforeMenuShow) { - this.sharedService.gridOptions.gridMenu.onBeforeMenuShow(e, args); + if (this._gridMenuOptions && this._gridMenuOptions.onBeforeMenuShow) { + this._gridMenuOptions.onBeforeMenuShow(e, args); } }); } } - if (this.sharedService.gridOptions.gridMenu && typeof this.sharedService.gridOptions.gridMenu.onAfterMenuShow === 'function') { + if (this._gridMenuOptions && typeof this._gridMenuOptions.onAfterMenuShow === 'function') { const onAfterMenuShowHandler = this._addon.onAfterMenuShow; if (onAfterMenuShowHandler) { (this._eventHandler as SlickEventHandler>).subscribe(onAfterMenuShowHandler, (e, args) => { - if (this.sharedService.gridOptions.gridMenu && this.sharedService.gridOptions.gridMenu.onAfterMenuShow) { - this.sharedService.gridOptions.gridMenu.onAfterMenuShow(e, args); + if (this._gridMenuOptions && this._gridMenuOptions.onAfterMenuShow) { + this._gridMenuOptions.onAfterMenuShow(e, args); } }); } @@ -119,8 +123,8 @@ export class GridMenuExtension implements Extension { if (onColumnsChangedHandler) { (this._eventHandler as SlickEventHandler>).subscribe(onColumnsChangedHandler, (e, args) => { this._areVisibleColumnDifferent = true; - if (this.sharedService.gridOptions.gridMenu && typeof this.sharedService.gridOptions.gridMenu.onColumnsChanged === 'function') { - this.sharedService.gridOptions.gridMenu.onColumnsChanged(e, args); + if (this._gridMenuOptions && typeof this._gridMenuOptions.onColumnsChanged === 'function') { + this._gridMenuOptions.onColumnsChanged(e, args); } if (args && Array.isArray(args.columns) && args.columns.length > this.sharedService.visibleColumns.length) { this.sharedService.visibleColumns = args.columns; @@ -132,16 +136,16 @@ export class GridMenuExtension implements Extension { if (onCommandHandler) { (this._eventHandler as SlickEventHandler>).subscribe(onCommandHandler, (e, args) => { this.executeGridMenuInternalCustomCommands(e, args); - if (this.sharedService.gridOptions.gridMenu && typeof this.sharedService.gridOptions.gridMenu.onCommand === 'function') { - this.sharedService.gridOptions.gridMenu.onCommand(e, args); + if (this._gridMenuOptions && typeof this._gridMenuOptions.onCommand === 'function') { + this._gridMenuOptions.onCommand(e, args); } }); } const onMenuCloseHandler = this._addon.onMenuClose; if (onMenuCloseHandler) { (this._eventHandler as SlickEventHandler>).subscribe(onMenuCloseHandler, (e, args) => { - if (this.sharedService.gridOptions.gridMenu && typeof this.sharedService.gridOptions.gridMenu.onMenuClose === 'function') { - this.sharedService.gridOptions.gridMenu.onMenuClose(e, args); + if (this._gridMenuOptions && typeof this._gridMenuOptions.onMenuClose === 'function') { + this._gridMenuOptions.onMenuClose(e, args); } // we also want to resize the columns if the user decided to hide certain column(s) @@ -219,12 +223,12 @@ export class GridMenuExtension implements Extension { const translationPrefix = getTranslationPrefix(gridOptions); // show grid menu: Clear Frozen Columns - if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideClearFrozenColumnsCommand) { + if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideClearFrozenColumnsCommand) { const commandName = 'clear-frozen-columns'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconClearFrozenColumnsCommand || 'fa fa-times', + iconCssClass: this._gridMenuOptions.iconClearFrozenColumnsCommand || 'fa fa-times', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}CLEAR_FROZEN_COLUMNS`, 'TEXT_CLEAR_FROZEN_COLUMNS'), disabled: false, command: commandName, @@ -236,12 +240,12 @@ export class GridMenuExtension implements Extension { if (this.sharedService.gridOptions && (this.sharedService.gridOptions.enableFiltering && !this.sharedService.hideHeaderRowAfterPageLoad)) { // show grid menu: Clear all Filters - if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideClearAllFiltersCommand) { + if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideClearAllFiltersCommand) { const commandName = 'clear-filter'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconClearAllFiltersCommand || 'fa fa-filter text-danger', + iconCssClass: this._gridMenuOptions.iconClearAllFiltersCommand || 'fa fa-filter text-danger', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}CLEAR_ALL_FILTERS`, 'TEXT_CLEAR_ALL_FILTERS'), disabled: false, command: commandName, @@ -252,12 +256,12 @@ export class GridMenuExtension implements Extension { } // show grid menu: toggle filter row - if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideToggleFilterCommand) { + if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideToggleFilterCommand) { const commandName = 'toggle-filter'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconToggleFilterCommand || 'fa fa-random', + iconCssClass: this._gridMenuOptions.iconToggleFilterCommand || 'fa fa-random', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}TOGGLE_FILTER_ROW`, 'TEXT_TOGGLE_FILTER_ROW'), disabled: false, command: commandName, @@ -268,12 +272,12 @@ export class GridMenuExtension implements Extension { } // show grid menu: refresh dataset - if (backendApi && this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideRefreshDatasetCommand) { + if (backendApi && this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideRefreshDatasetCommand) { const commandName = 'refresh-dataset'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconRefreshDatasetCommand || 'fa fa-refresh', + iconCssClass: this._gridMenuOptions.iconRefreshDatasetCommand || 'fa fa-refresh', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}REFRESH_DATASET`, 'TEXT_REFRESH_DATASET'), disabled: false, command: commandName, @@ -286,12 +290,12 @@ export class GridMenuExtension implements Extension { if (this.sharedService.gridOptions.showPreHeaderPanel) { // show grid menu: toggle pre-header row - if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideTogglePreHeaderCommand) { + if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideTogglePreHeaderCommand) { const commandName = 'toggle-preheader'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconTogglePreHeaderCommand || 'fa fa-random', + iconCssClass: this._gridMenuOptions.iconTogglePreHeaderCommand || 'fa fa-random', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}TOGGLE_PRE_HEADER_ROW`, 'TEXT_TOGGLE_PRE_HEADER_ROW'), disabled: false, command: commandName, @@ -304,12 +308,12 @@ export class GridMenuExtension implements Extension { if (this.sharedService.gridOptions.enableSorting) { // show grid menu: Clear all Sorting - if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideClearAllSortingCommand) { + if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideClearAllSortingCommand) { const commandName = 'clear-sorting'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconClearAllSortingCommand || 'fa fa-unsorted text-danger', + iconCssClass: this._gridMenuOptions.iconClearAllSortingCommand || 'fa fa-unsorted text-danger', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}CLEAR_ALL_SORTING`, 'TEXT_CLEAR_ALL_SORTING'), disabled: false, command: commandName, @@ -321,12 +325,12 @@ export class GridMenuExtension implements Extension { } // show grid menu: Export to file - if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExport && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideExportCsvCommand) { + if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExport && this._gridMenuOptions && !this._gridMenuOptions.hideExportCsvCommand) { const commandName = 'export-csv'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconExportCsvCommand || 'fa fa-download', + iconCssClass: this._gridMenuOptions.iconExportCsvCommand || 'fa fa-download', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}EXPORT_TO_CSV`, 'TEXT_EXPORT_TO_CSV'), disabled: false, command: commandName, @@ -337,12 +341,12 @@ export class GridMenuExtension implements Extension { } // show grid menu: Export to Excel - if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExcelExport && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideExportExcelCommand) { + if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExcelExport && this._gridMenuOptions && !this._gridMenuOptions.hideExportExcelCommand) { const commandName = 'export-excel'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconExportExcelCommand || 'fa fa-file-excel-o text-success', + iconCssClass: this._gridMenuOptions.iconExportExcelCommand || 'fa fa-file-excel-o text-success', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}EXPORT_TO_EXCEL`, 'TEXT_EXPORT_TO_EXCEL'), disabled: false, command: commandName, @@ -353,12 +357,12 @@ export class GridMenuExtension implements Extension { } // show grid menu: export to text file as tab delimited - if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExport && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideExportTextDelimitedCommand) { + if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExport && this._gridMenuOptions && !this._gridMenuOptions.hideExportTextDelimitedCommand) { const commandName = 'export-text-delimited'; if (!originalCustomItems.find((item: GridMenuItem) => item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconExportTextDelimitedCommand || 'fa fa-download', + iconCssClass: this._gridMenuOptions.iconExportTextDelimitedCommand || 'fa fa-download', title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}EXPORT_TO_TAB_DELIMITED`, 'TEXT_EXPORT_TO_TAB_DELIMITED'), disabled: false, command: commandName, @@ -369,8 +373,8 @@ export class GridMenuExtension implements Extension { } // add the custom "Commands" title if there are any commands - if (this.sharedService && this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && (Array.isArray(gridMenuCustomItems) && gridMenuCustomItems.length > 0 || (Array.isArray(this.sharedService.gridOptions.gridMenu.customItems) && this.sharedService.gridOptions.gridMenu.customItems.length > 0))) { - this.sharedService.gridOptions.gridMenu.customTitle = this.sharedService.gridOptions.gridMenu.customTitle || this.extensionUtility.getPickerTitleOutputString('customTitle', 'gridMenu'); + if (this.sharedService && this.sharedService.gridOptions && this._gridMenuOptions && (Array.isArray(gridMenuCustomItems) && gridMenuCustomItems.length > 0 || (Array.isArray(this._gridMenuOptions.customItems) && this._gridMenuOptions.customItems.length > 0))) { + this._gridMenuOptions.customTitle = this._gridMenuOptions.customTitle || this.extensionUtility.getPickerTitleOutputString('customTitle', 'gridMenu'); } return gridMenuCustomItems; diff --git a/packages/common/src/extensions/groupItemMetaProviderExtension.ts b/packages/common/src/extensions/groupItemMetaProviderExtension.ts index 018fdb6a7..1c3686cc2 100644 --- a/packages/common/src/extensions/groupItemMetaProviderExtension.ts +++ b/packages/common/src/extensions/groupItemMetaProviderExtension.ts @@ -9,8 +9,8 @@ export class GroupItemMetaProviderExtension implements Extension { dispose() { if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } + this._addon = null; } /** Get the instance of the SlickGrid addon (control or plugin). */ diff --git a/packages/common/src/extensions/headerButtonExtension.ts b/packages/common/src/extensions/headerButtonExtension.ts index 5946d0150..754b00b80 100644 --- a/packages/common/src/extensions/headerButtonExtension.ts +++ b/packages/common/src/extensions/headerButtonExtension.ts @@ -1,5 +1,5 @@ import { ExtensionName } from '../enums/index'; -import { Extension, GetSlickEventType, SlickEventHandler, SlickNamespace, SlickHeaderButtons } from '../interfaces/index'; +import { Extension, GetSlickEventType, SlickEventHandler, SlickNamespace, SlickHeaderButtons, HeaderButton } from '../interfaces/index'; import { ExtensionUtility } from './extensionUtility'; import { SharedService } from '../services/shared.service'; @@ -9,6 +9,7 @@ declare const Slick: SlickNamespace; export class HeaderButtonExtension implements Extension { private _eventHandler: SlickEventHandler; private _addon: SlickHeaderButtons | null; + private _headerButtonOptions: HeaderButton | null; constructor(private extensionUtility: ExtensionUtility, private sharedService: SharedService) { this._eventHandler = new Slick.EventHandler(); @@ -24,8 +25,10 @@ export class HeaderButtonExtension implements Extension { if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } + this.extensionUtility.nullifyFunctionNameStartingWithOn(this._headerButtonOptions); + this._addon = null; + this._headerButtonOptions = null; } /** Get the instance of the SlickGrid addon (control or plugin). */ @@ -38,23 +41,23 @@ export class HeaderButtonExtension implements Extension { if (this.sharedService && this.sharedService.slickGrid && this.sharedService.gridOptions) { // dynamically import the SlickGrid plugin (addon) with RequireJS this.extensionUtility.loadExtensionDynamically(ExtensionName.headerButton); - - this._addon = new Slick.Plugins.HeaderButtons(this.sharedService.gridOptions.headerButton); + this._headerButtonOptions = this.sharedService.gridOptions.headerButton || {}; + this._addon = new Slick.Plugins.HeaderButtons(this._headerButtonOptions); if (this._addon) { this.sharedService.slickGrid.registerPlugin(this._addon); } // hook all events - if (this._addon && this.sharedService.slickGrid && this.sharedService.gridOptions.headerButton) { - if (this.sharedService.gridOptions.headerButton.onExtensionRegistered) { - this.sharedService.gridOptions.headerButton.onExtensionRegistered(this._addon); + if (this._addon && this.sharedService.slickGrid && this._headerButtonOptions) { + if (this._headerButtonOptions.onExtensionRegistered) { + this._headerButtonOptions.onExtensionRegistered(this._addon); } const onCommandHandler = this._addon.onCommand; if (onCommandHandler) { (this._eventHandler as SlickEventHandler>).subscribe(onCommandHandler, (e, args) => { - if (this.sharedService.gridOptions.headerButton && typeof this.sharedService.gridOptions.headerButton.onCommand === 'function') { - this.sharedService.gridOptions.headerButton.onCommand(e, args); + if (this._headerButtonOptions && typeof this._headerButtonOptions.onCommand === 'function') { + this._headerButtonOptions.onCommand(e, args); } }); } diff --git a/packages/common/src/extensions/headerMenuExtension.ts b/packages/common/src/extensions/headerMenuExtension.ts index f730f406e..1c21aebbd 100644 --- a/packages/common/src/extensions/headerMenuExtension.ts +++ b/packages/common/src/extensions/headerMenuExtension.ts @@ -51,8 +51,8 @@ export class HeaderMenuExtension implements Extension { if (this._addon && this._addon.destroy) { this._addon.destroy(); - this._addon = null; } + this._addon = null; } /** Get the instance of the SlickGrid addon (control or plugin). */ diff --git a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts index 2871d0bea..3f779367d 100644 --- a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts +++ b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts @@ -140,7 +140,7 @@ describe('AutoCompleteFilter', () => { filterElm.focus(); filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 109, bubbles: true, cancelable: true })); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 109, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 109, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); expect(filterFilledElms.length).toBe(1); @@ -158,7 +158,7 @@ describe('AutoCompleteFilter', () => { const filterElm = divContainer.querySelector('input.filter-gender'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); expect(filterFilledElms.length).toBe(1); @@ -176,7 +176,7 @@ describe('AutoCompleteFilter', () => { const filterElm = divContainer.querySelector('input.filter-gender'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); expect(filterFilledElms.length).toBe(1); @@ -192,7 +192,7 @@ describe('AutoCompleteFilter', () => { filterElm.focus(); filterElm.value = 'a'; - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); // expect(autocompleteListElms.length).toBe(2); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['a'], shouldTriggerQuery: true }); @@ -264,7 +264,7 @@ describe('AutoCompleteFilter', () => { filter.setValues('male'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); expect(autocompleteUlElms.length).toBe(1); @@ -288,7 +288,7 @@ describe('AutoCompleteFilter', () => { filter.setValues('male'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); expect(autocompleteUlElms.length).toBe(1); @@ -318,7 +318,7 @@ describe('AutoCompleteFilter', () => { filter.setValues('male'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); expect(autocompleteUlElms.length).toBe(1); diff --git a/packages/common/src/filters/__tests__/compoundInputFilter.spec.ts b/packages/common/src/filters/__tests__/compoundInputFilter.spec.ts index 9a703e8a2..563f5c151 100644 --- a/packages/common/src/filters/__tests__/compoundInputFilter.spec.ts +++ b/packages/common/src/filters/__tests__/compoundInputFilter.spec.ts @@ -85,13 +85,47 @@ describe('CompoundInputFilter', () => { const filterInputElm = divContainer.querySelector('.search-filter.filter-duration input'); filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterInputElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('.search-filter.filter-duration.filled'); expect(filterFilledElms.length).toBe(1); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true }); }); + it('should call "setValues" and expect that value to be in the callback when triggered by ENTER key', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues(['abc']); + const filterInputElm = divContainer.querySelector('.search-filter.filter-duration input'); + + filterInputElm.focus(); + const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true }); + event.key = 'Enter'; + filterInputElm.dispatchEvent(event); + const filterFilledElms = divContainer.querySelectorAll('.search-filter.filter-duration.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true }); + }); + + it('should call "setValues" and expect that value NOT to be in the callback when triggered by a keyup event that is NOT the ENTER key', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues(['abc']); + const filterInputElm = divContainer.querySelector('.search-filter.filter-duration input'); + + filterInputElm.focus(); + const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true }); + event.key = 'a'; + filterInputElm.dispatchEvent(event); + const filterFilledElms = divContainer.querySelectorAll('.search-filter.filter-duration.filled'); + + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).not.toHaveBeenCalled(); + }); + it('should call "setValues" with "operator" set in the filter arguments and expect that value to be in the callback when triggered', () => { mockColumn.type = FieldType.number; const filterArgs = { ...filterArguments, operator: '>' } as FilterArguments; @@ -102,7 +136,7 @@ describe('CompoundInputFilter', () => { const filterInputElm = divContainer.querySelector('.search-filter.filter-duration input'); filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterInputElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['9'], shouldTriggerQuery: true }); }); @@ -146,7 +180,7 @@ describe('CompoundInputFilter', () => { const filterInputElm = divContainer.querySelector('.search-filter.filter-duration input'); filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterInputElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['987'], shouldTriggerQuery: true }); }); @@ -163,7 +197,7 @@ describe('CompoundInputFilter', () => { const filterInputElm = divContainer.querySelector('.search-filter.filter-duration input'); filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterInputElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['987'], shouldTriggerQuery: true }); }); @@ -176,7 +210,7 @@ describe('CompoundInputFilter', () => { filterInputElm.focus(); filterInputElm.value = 'a'; - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterInputElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['a'], shouldTriggerQuery: true }); }); diff --git a/packages/common/src/filters/__tests__/inputFilter.spec.ts b/packages/common/src/filters/__tests__/inputFilter.spec.ts index b14d664ea..5afe3d779 100644 --- a/packages/common/src/filters/__tests__/inputFilter.spec.ts +++ b/packages/common/src/filters/__tests__/inputFilter.spec.ts @@ -77,13 +77,47 @@ describe('InputFilter', () => { const filterElm = divContainer.querySelector('input.filter-duration'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-duration.filled'); expect(filterFilledElms.length).toBe(1); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true }); }); + it('should call "setValues" and expect that value to be in the callback when triggered by ENTER key', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues('abc'); + const filterElm = divContainer.querySelector('input.filter-duration'); + + filterElm.focus(); + const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true }); + event.key = 'Enter'; + filterElm.dispatchEvent(event); + const filterFilledElms = divContainer.querySelectorAll('input.filter-duration.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true }); + }); + + it('should call "setValues" and expect that value NOT to be in the callback when triggered by a keyup event that is NOT the ENTER key', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues('abc'); + const filterElm = divContainer.querySelector('input.filter-duration'); + + filterElm.focus(); + const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true }); + event.key = 'a'; + filterElm.dispatchEvent(event); + const filterFilledElms = divContainer.querySelectorAll('input.filter-duration.filled'); + + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).not.toHaveBeenCalled(); + }); + it('should call "setValues" with an operator and with extra spaces at the beginning of the searchTerms and trim value when "enableFilterTrimWhiteSpace" is enabled in grid options', () => { gridOptionMock.enableFilterTrimWhiteSpace = true; const spyCallback = jest.spyOn(filterArguments, 'callback'); @@ -93,7 +127,7 @@ describe('InputFilter', () => { const filterElm = divContainer.querySelector('input.filter-duration'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-duration.filled'); expect(filterFilledElms.length).toBe(1); @@ -110,7 +144,7 @@ describe('InputFilter', () => { const filterElm = divContainer.querySelector('input.filter-duration'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-duration.filled'); expect(filterFilledElms.length).toBe(1); @@ -125,7 +159,7 @@ describe('InputFilter', () => { filterElm.focus(); filterElm.value = 'a'; - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).Event('input', { key: 'a', keyCode: 97, bubbles: true, cancelable: true })); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['a'], shouldTriggerQuery: true }); }); diff --git a/packages/common/src/filters/__tests__/inputMaskFilter.spec.ts b/packages/common/src/filters/__tests__/inputMaskFilter.spec.ts index 6658a27af..7bf1eb429 100644 --- a/packages/common/src/filters/__tests__/inputMaskFilter.spec.ts +++ b/packages/common/src/filters/__tests__/inputMaskFilter.spec.ts @@ -83,7 +83,7 @@ describe('InputMaskFilter', () => { filter.setValues('1234567890'); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['1234567890'], shouldTriggerQuery: true }); }); @@ -96,7 +96,7 @@ describe('InputMaskFilter', () => { filter.setValues('1234567890', 'EQ'); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(filterElm.value).toBe('(123) 456-7890'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['1234567890'], shouldTriggerQuery: true }); @@ -110,7 +110,7 @@ describe('InputMaskFilter', () => { filter.setValues('1234567890abc'); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(filterElm.value).toBe('(123) 456-7890'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['1234567890'], shouldTriggerQuery: true }); @@ -124,7 +124,7 @@ describe('InputMaskFilter', () => { filter.setValues('1234567890'); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(filterElm.value).toBe('(123) 456-7890'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['1234567890'], shouldTriggerQuery: true }); @@ -138,7 +138,7 @@ describe('InputMaskFilter', () => { filter.setValues('H1H1H1'); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(filterElm.value).toBe('H1H 1H1'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['H1H1H1'], shouldTriggerQuery: true }); @@ -153,7 +153,7 @@ describe('InputMaskFilter', () => { filter.setValues(' 1234567890 '); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(filterElm.value).toBe('(123) 456-7890'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['1234567890'], shouldTriggerQuery: true }); @@ -169,7 +169,7 @@ describe('InputMaskFilter', () => { filter.setValues(' 1234567890 '); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(filterElm.value).toBe('(123) 456-7890'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['1234567890'], shouldTriggerQuery: true }); @@ -183,7 +183,7 @@ describe('InputMaskFilter', () => { filter.setValues('abc'); const filterElm = divContainer.querySelector('input.filter-mask'); filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(filterElm.value).toBe('() -'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: [''], shouldTriggerQuery: true }); @@ -198,7 +198,7 @@ describe('InputMaskFilter', () => { filterElm.focus(); filterElm.value = '1'; - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['1'], shouldTriggerQuery: true }); }); diff --git a/packages/common/src/filters/autoCompleteFilter.ts b/packages/common/src/filters/autoCompleteFilter.ts index 3a0539dc4..f57e27afd 100644 --- a/packages/common/src/filters/autoCompleteFilter.ts +++ b/packages/common/src/filters/autoCompleteFilter.ts @@ -23,7 +23,7 @@ import { TranslaterService } from '../services/translater.service'; export class AutoCompleteFilter implements Filter { private _autoCompleteOptions: AutocompleteOption; private _clearFilterTriggered = false; - private _collection: any[]; + private _collection: any[] | null; private _shouldTriggerQuery = true; /** DOM Element Name, useful for auto-detecting positioning (dropup / dropdown) */ @@ -71,7 +71,7 @@ export class AutoCompleteFilter implements Filter { } /** Getter for the Collection Used by the Filter */ - get collection(): any[] { + get collection(): any[] | null { return this._collection; } @@ -177,7 +177,7 @@ export class AutoCompleteFilter implements Filter { this._shouldTriggerQuery = shouldTriggerQuery; this.searchTerms = []; this.$filterElm.val(''); - this.$filterElm.trigger('keyup'); + this.$filterElm.trigger('input'); } } @@ -187,9 +187,10 @@ export class AutoCompleteFilter implements Filter { destroy() { if (this.$filterElm) { this.$filterElm.autocomplete('destroy'); - this.$filterElm.off('keyup').remove(); - this.$filterElm = null; + this.$filterElm.off('input').remove(); } + this.$filterElm = null; + this._collection = null; } /** Set value(s) on the DOM element */ @@ -268,26 +269,9 @@ export class AutoCompleteFilter implements Filter { this._collection = newCollection; this.createDomElement(filterTemplate, newCollection, searchTerm); - // step 3, subscribe to the keyup event and run the callback when that happens + // step 3, subscribe to the input change event and run the callback when that happens // also add/remove "filled" class for styling purposes - this.$filterElm.on('keyup', (e: any) => { - let value = e && e.target && e.target.value || ''; - const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace; - if (typeof value === 'string' && enableWhiteSpaceTrim) { - value = value.trim(); - } - - if (this._clearFilterTriggered) { - this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); - this.$filterElm.removeClass('filled'); - } else { - value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('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; - }); + this.$filterElm.on('input', this.handleOnInputChange.bind(this)); } /** @@ -422,7 +406,27 @@ export class AutoCompleteFilter implements Filter { return false; } - protected renderCustomItem(ul: HTMLElement, item: any) { + private handleOnInputChange(e: any) { + let value = e && e.target && e.target.value || ''; + const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace; + if (typeof value === 'string' && enableWhiteSpaceTrim) { + value = value.trim(); + } + + if (this._clearFilterTriggered) { + this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); + this.$filterElm.removeClass('filled'); + } else { + value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('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; + } + + private renderCustomItem(ul: HTMLElement, item: any) { const templateString = this._autoCompleteOptions?.renderItem?.templateCallback(item) ?? ''; // sanitize any unauthorized html tags like script and others @@ -435,7 +439,7 @@ export class AutoCompleteFilter implements Filter { .appendTo(ul); } - protected renderCollectionItem(ul: any, item: any) { + private renderCollectionItem(ul: any, item: any) { const isRenderHtmlEnabled = this.columnFilter?.enableRenderHtml ?? false; const prefixText = item.labelPrefix || ''; const labelText = item.label || ''; @@ -453,7 +457,7 @@ export class AutoCompleteFilter implements Filter { .appendTo(ul); } - protected async renderOptionsAsync(collectionAsync: Promise): Promise { + private async renderOptionsAsync(collectionAsync: Promise): Promise { let awaitedCollection: any = null; if (collectionAsync) { diff --git a/packages/common/src/filters/compoundDateFilter.ts b/packages/common/src/filters/compoundDateFilter.ts index 74aa3b3e4..0191a7f2f 100644 --- a/packages/common/src/filters/compoundDateFilter.ts +++ b/packages/common/src/filters/compoundDateFilter.ts @@ -129,11 +129,11 @@ export class CompoundDateFilter implements Filter { } if (this.$filterElm) { this.$filterElm.off('keyup').remove(); - this.$filterElm = null; } if (this.$selectOperatorElm) { this.$selectOperatorElm.off('change').remove(); } + this.$filterElm = null; } hide() { @@ -325,6 +325,7 @@ export class CompoundDateFilter implements Filter { (this._currentValue) ? this.$filterElm.addClass('filled') : this.$filterElm.removeClass('filled'); this.callback(e, { columnDef: this.columnDef, searchTerms: (this._currentValue ? [this._currentValue] : null), operator: selectedOperator || '', shouldTriggerQuery: this._shouldTriggerQuery }); } + // reset both flags for next use this._clearFilterTriggered = false; this._shouldTriggerQuery = true; diff --git a/packages/common/src/filters/compoundInputFilter.ts b/packages/common/src/filters/compoundInputFilter.ts index 4926224c0..716268b77 100644 --- a/packages/common/src/filters/compoundInputFilter.ts +++ b/packages/common/src/filters/compoundInputFilter.ts @@ -90,14 +90,10 @@ export class CompoundInputFilter implements Filter { // and initialize it if searchTerm is filled this.$filterElm = this.createDomElement(searchTerm); - // step 3, subscribe to the keyup event and run the callback when that happens + // step 3, subscribe to the input change event and run the callback when that happens // also add/remove "filled" class for styling purposes - this.$filterInputElm.on('keyup input change', (e: any) => { - this.onTriggerEvent(e); - }); - this.$selectOperatorElm.on('change', (e: any) => { - this.onTriggerEvent(e); - }); + this.$filterInputElm.on('keyup input', this.onTriggerEvent.bind(this)); + this.$selectOperatorElm.on('change', this.onTriggerEvent.bind(this)); } /** @@ -119,11 +115,11 @@ export class CompoundInputFilter implements Filter { */ destroy() { if (this.$filterElm && this.$selectOperatorElm) { - this.$filterElm.off('keyup input change').remove(); + this.$filterElm.off('keyup input').remove(); this.$selectOperatorElm.off('change'); - this.$filterElm = null; - this.$selectOperatorElm = null; } + this.$filterElm = null; + this.$selectOperatorElm = null; } /** Set value(s) on the DOM element */ @@ -248,7 +244,12 @@ export class CompoundInputFilter implements Filter { } /** Event trigger, could be called by the Operator dropdown or the input itself */ - private onTriggerEvent(e: Event | undefined) { + private onTriggerEvent(e: KeyboardEvent | undefined) { + // we'll use the "input" event for everything (keyup, change, mousewheel & spinner) + // with 1 small exception, we need to use the keyup event to handle ENTER key, everything will be processed by the "input" event + if (e && e.type === 'keyup' && e.key !== 'Enter') { + return; + } if (this._clearFilterTriggered) { this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); this.$filterElm.removeClass('filled'); @@ -263,6 +264,7 @@ export class CompoundInputFilter implements Filter { (value !== null && value !== undefined && value !== '') ? this.$filterElm.addClass('filled') : this.$filterElm.removeClass('filled'); this.callback(e, { columnDef: this.columnDef, searchTerms: (value ? [value] : null), operator: selectedOperator || '', shouldTriggerQuery: this._shouldTriggerQuery }); } + // reset both flags for next use this._clearFilterTriggered = false; this._shouldTriggerQuery = true; diff --git a/packages/common/src/filters/compoundSliderFilter.ts b/packages/common/src/filters/compoundSliderFilter.ts index 064da4093..406573cda 100644 --- a/packages/common/src/filters/compoundSliderFilter.ts +++ b/packages/common/src/filters/compoundSliderFilter.ts @@ -100,7 +100,7 @@ export class CompoundSliderFilter implements Filter { // and initialize it if searchTerm is filled this.$filterElm = this.createDomElement(searchTerm); - // step 3, subscribe to the keyup event and run the callback when that happens + // step 3, subscribe to the input change event and run the callback when that happens // also add/remove "filled" class for styling purposes this.$filterInputElm.change((e: any) => { this.onTriggerEvent(e); @@ -151,10 +151,10 @@ export class CompoundSliderFilter implements Filter { if (this.$filterInputElm) { this.$filterInputElm.off('input change').remove(); this.$selectOperatorElm.off('change').remove(); - this.$filterInputElm = null; - this.$filterElm = null; - this.$selectOperatorElm = null; } + this.$filterInputElm = null; + this.$filterElm = null; + this.$selectOperatorElm = null; } /** @@ -307,6 +307,7 @@ export class CompoundSliderFilter implements Filter { const selectedOperator = this.$selectOperatorElm.find('option:selected').val(); this.callback(e, { columnDef: this.columnDef, searchTerms: (value ? [value || '0'] : null), operator: selectedOperator || '', shouldTriggerQuery: this._shouldTriggerQuery }); } + // reset both flags for next use this._clearFilterTriggered = false; this._shouldTriggerQuery = true; diff --git a/packages/common/src/filters/dateRangeFilter.ts b/packages/common/src/filters/dateRangeFilter.ts index bb16658e6..3894027b5 100644 --- a/packages/common/src/filters/dateRangeFilter.ts +++ b/packages/common/src/filters/dateRangeFilter.ts @@ -118,12 +118,12 @@ export class DateRangeFilter implements Filter { if (this.flatInstance.element) { destroyObjectDomElementProps(this.flatInstance); } - this.flatInstance = null; } if (this.$filterElm) { this.$filterElm.off('keyup').remove(); - this.$filterElm = null; } + this.flatInstance = null; + this.$filterElm = null; } hide() { @@ -295,6 +295,7 @@ export class DateRangeFilter implements Filter { (this._currentDateStrings) ? this.$filterElm.addClass('filled') : this.$filterElm.removeClass('filled'); this.callback(e, { columnDef: this.columnDef, searchTerms: (this._currentDateStrings ? this._currentDateStrings : [this._currentValue]), operator: this.operator || '', shouldTriggerQuery: this._shouldTriggerQuery }); } + // reset both flags for next use this._clearFilterTriggered = false; this._shouldTriggerQuery = true; diff --git a/packages/common/src/filters/inputFilter.ts b/packages/common/src/filters/inputFilter.ts index e73929126..620b726d4 100644 --- a/packages/common/src/filters/inputFilter.ts +++ b/packages/common/src/filters/inputFilter.ts @@ -79,26 +79,9 @@ export class InputFilter implements Filter { // step 2, create the DOM Element of the filter & initialize it if searchTerm is filled this.$filterElm = this.createDomElement(filterTemplate, searchTerm); - // step 3, subscribe to the keyup event and run the callback when that happens + // step 3, subscribe to the input event and run the callback when that happens // also add/remove "filled" class for styling purposes - this.$filterElm.on('keyup input change', (e: any) => { - let value = e && e.target && e.target.value || ''; - const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace; - if (typeof value === 'string' && enableWhiteSpaceTrim) { - value = value.trim(); - } - - if (this._clearFilterTriggered) { - this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); - this.$filterElm.removeClass('filled'); - } else { - value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('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; - }); + this.$filterElm.on('keyup input', this.handleInputChange.bind(this)); } /** @@ -110,7 +93,7 @@ export class InputFilter implements Filter { this._shouldTriggerQuery = shouldTriggerQuery; this.searchTerms = []; this.$filterElm.val(''); - this.$filterElm.trigger('keyup'); + this.$filterElm.trigger('input'); } } @@ -119,9 +102,9 @@ export class InputFilter implements Filter { */ destroy() { if (this.$filterElm) { - this.$filterElm.off('keyup input change').remove(); - this.$filterElm = null; + this.$filterElm.off('keyup input').remove(); } + this.$filterElm = null; } /** Set value(s) on the DOM element */ @@ -177,4 +160,29 @@ export class InputFilter implements Filter { return $filterElm; } + + protected handleInputChange(e: any) { + // we'll use the "input" event for everything (keyup, change, mousewheel & spinner) + // with 1 small exception, we need to use the keyup event to handle ENTER key, everything will be processed by the "input" event + if (e && e.type === 'keyup' && e.key !== 'Enter') { + return; + } + let value = e && e.target && e.target.value || ''; + const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace; + if (typeof value === 'string' && enableWhiteSpaceTrim) { + value = value.trim(); + } + + if (this._clearFilterTriggered) { + this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); + this.$filterElm.removeClass('filled'); + } else { + value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('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/filters/inputMaskFilter.ts b/packages/common/src/filters/inputMaskFilter.ts index e3f8bfe19..b58bd4258 100644 --- a/packages/common/src/filters/inputMaskFilter.ts +++ b/packages/common/src/filters/inputMaskFilter.ts @@ -48,10 +48,10 @@ export class InputMaskFilter extends InputFilter { // step 2, create the DOM Element of the filter & initialize it if searchTerm is filled this.$filterElm = this.createDomElement(filterTemplate, searchTerm); - // step 3, subscribe to the keyup event and run the callback when that happens + // step 3, subscribe to the input change event and run the callback when that happens // also add/remove "filled" class for styling purposes - this.$filterElm.on('keyup input change', (e: any) => { + this.$filterElm.on('input change', (e: any) => { let value = ''; if (e && e.target && e.target.value) { let targetValue = e.target.value; diff --git a/packages/common/src/filters/nativeSelectFilter.ts b/packages/common/src/filters/nativeSelectFilter.ts index a4140f880..af985ae6f 100644 --- a/packages/common/src/filters/nativeSelectFilter.ts +++ b/packages/common/src/filters/nativeSelectFilter.ts @@ -83,21 +83,7 @@ export class NativeSelectFilter implements Filter { // step 3, subscribe to the change event and run the callback when that happens // 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'); - } else { - value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('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; - }); + this.$filterElm.change(this.handleOnChange.bind(this)); } /** @@ -120,8 +106,8 @@ export class NativeSelectFilter implements Filter { destroy() { if (this.$filterElm) { this.$filterElm.off('change').remove(); - this.$filterElm = null; } + this.$filterElm = null; } /** @@ -208,4 +194,21 @@ export class NativeSelectFilter implements Filter { return $filterElm; } + + private 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.removeClass('filled'); + } else { + value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('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/filters/selectFilter.ts b/packages/common/src/filters/selectFilter.ts index 72359417f..f3d0ed99b 100644 --- a/packages/common/src/filters/selectFilter.ts +++ b/packages/common/src/filters/selectFilter.ts @@ -179,8 +179,8 @@ export class SelectFilter implements Filter { this.$filterElm.remove(); const elementClassName = this.elementName.toString().replace('.', '\\.'); // make sure to escape any dot "." from CSS class to avoid console error $(`[name=${elementClassName}].ms-drop`).remove(); - this.$filterElm = null; } + this.$filterElm = null; } /** diff --git a/packages/common/src/filters/sliderFilter.ts b/packages/common/src/filters/sliderFilter.ts index 330c95cab..9e32164e4 100644 --- a/packages/common/src/filters/sliderFilter.ts +++ b/packages/common/src/filters/sliderFilter.ts @@ -85,21 +85,7 @@ export class SliderFilter implements Filter { // step 3, subscribe to the change event and run the callback when that happens // also add/remove "filled" class for styling purposes - this.$filterInputElm.change((e: any) => { - const value = e && e.target && e.target.value; - this._currentValue = +value; - - if (this._clearFilterTriggered) { - this.$filterElm.removeClass('filled'); - this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, searchTerms: [], shouldTriggerQuery: this._shouldTriggerQuery }); - } else { - this.$filterElm.addClass('filled'); - this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: [value || '0'], shouldTriggerQuery: this._shouldTriggerQuery }); - } - // reset both flags for next use - this._clearFilterTriggered = false; - this._shouldTriggerQuery = true; - }); + this.$filterInputElm.change(this.handleOnChange.bind(this)); // if user chose to display the slider number on the right side, then update it every time it changes // we need to use both "input" and "change" event to be all cross-browser @@ -138,9 +124,9 @@ export class SliderFilter implements Filter { destroy() { if (this.$filterInputElm) { this.$filterInputElm.off('input change').remove(); - this.$filterInputElm = null; - this.$filterElm = null; } + this.$filterInputElm = null; + this.$filterElm = null; } /** @@ -243,4 +229,20 @@ export class SliderFilter implements Filter { return $filterElm; } + + private handleOnChange(e: any) { + const value = e && e.target && e.target.value; + this._currentValue = +value; + + if (this._clearFilterTriggered) { + this.$filterElm.removeClass('filled'); + this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, searchTerms: [], shouldTriggerQuery: this._shouldTriggerQuery }); + } else { + this.$filterElm.addClass('filled'); + this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: [value || '0'], shouldTriggerQuery: this._shouldTriggerQuery }); + } + // reset both flags for next use + this._clearFilterTriggered = false; + this._shouldTriggerQuery = true; + } } diff --git a/packages/common/src/filters/sliderRangeFilter.ts b/packages/common/src/filters/sliderRangeFilter.ts index 9b0c240a4..66b6177d7 100644 --- a/packages/common/src/filters/sliderRangeFilter.ts +++ b/packages/common/src/filters/sliderRangeFilter.ts @@ -119,9 +119,9 @@ export class SliderRangeFilter implements Filter { this.$filterElm.slider('destroy'); this.$filterElm.off('change').remove(); this.$filterContainerElm.remove(); - this.$filterElm = null; - this.$filterContainerElm = null; } + this.$filterElm = null; + this.$filterContainerElm = null; } /** diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index bd5c58543..71f1bf5ec 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -96,7 +96,7 @@ export const GlobalGridOptions: GridOption = { defaultSlickgridEventPrefix: '', editable: false, editorTypingDebounce: 450, - enableEmptyDataWarningMessage: true, + enableEmptyDataWarningMessage: false, emptyDataWarning: { class: 'slick-empty-data-warning', message: 'No data to display.', diff --git a/packages/common/src/interfaces/elementEventListener.interface.ts b/packages/common/src/interfaces/elementEventListener.interface.ts new file mode 100644 index 000000000..409a80691 --- /dev/null +++ b/packages/common/src/interfaces/elementEventListener.interface.ts @@ -0,0 +1,5 @@ +export interface ElementEventListener { + element: Element; + eventName: string; + listener: EventListenerOrEventListenerObject; +} diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 97a3efbd9..3adcf9d19 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -42,6 +42,7 @@ export * from './editorArguments.interface'; export * from './editorValidationResult.interface'; export * from './editorValidator.interface'; export * from './editUndoRedoBuffer.interface'; +export * from './elementEventListener.interface'; export * from './elementPosition.interface'; export * from './emptyWarning.interface'; export * from './excelCellFormat.interface'; diff --git a/packages/common/src/interfaces/slickDataView.interface.ts b/packages/common/src/interfaces/slickDataView.interface.ts index 80b494b1b..b339501da 100644 --- a/packages/common/src/interfaces/slickDataView.interface.ts +++ b/packages/common/src/interfaces/slickDataView.interface.ts @@ -13,6 +13,9 @@ export interface SlickDataView { /** Begin Data Update Transaction */ beginUpdate(): void; + /** Destroy (dispose) of Slick DataView */ + destroy(): void; + /** Collapse all Groups, optionally pass a level number to only collapse that level */ collapseAllGroups(level?: number): void; diff --git a/packages/common/src/interfaces/slickGrid.interface.ts b/packages/common/src/interfaces/slickGrid.interface.ts index 1bf3e2cd3..ad8dc954c 100644 --- a/packages/common/src/interfaces/slickGrid.interface.ts +++ b/packages/common/src/interfaces/slickGrid.interface.ts @@ -47,34 +47,37 @@ export interface SlickGrid { /** * Returns true if you can click on a given cell and make it the active focus. - * @param row A row index. - * @param col A column index. + * @param {number} row A row index. + * @param {number} col A column index. */ canCellBeActive(row: number, col: number): boolean; /** * Returns true if selecting the row causes this particular cell to have the selectedCellCssClass applied to it. A cell can be selected if it exists and if it isn't on an empty / "Add New" row and if it is not marked as "unselectable" in the column definition. - * @param row A row index. - * @param col A column index. + * @param {number} row A row index. + * @param {number} col A column index. */ canCellBeSelected(row: number, col: number): boolean; - /** Destroy (dispose) of SlickGrid */ - destroy(): void; + /** + * Destroy (dispose) of SlickGrid + * @param {boolean} shouldDestroyAllElements - do we want to destroy (nullify) all DOM elements as well? This help in avoiding mem leaks + */ + destroy(shouldDestroyAllElements?: boolean): void; /** * Attempts to switch the active cell into edit mode. Will throw an error if the cell is set to be not editable. Uses the specified editor, otherwise defaults to any default editor for that given cell. - * @param {object} editor A SlickGrid editor (see examples in slick.editors.js). - * @param {boolean} preClickModeOn Pre-Click Mode is Enabled? + * @param {object} editor - A SlickGrid editor (see examples in slick.editors.js). + * @param {boolean} preClickModeOn - Pre-Click Mode is Enabled? * @param {object} event */ editActiveCell(editor: Editor | SlickCompositeEditor, preClickModeOn?: boolean, event?: Event): void; /** * Flashes the cell twice by toggling the CSS class 4 times. - * @param row A row index. - * @param cell A column index. - * @param speed (optional) - The milliseconds delay between the toggling calls. Defaults to 100 ms. + * @param {number} row A row index. + * @param {number} cell A column index. + * @param {number} speed (optional) - The milliseconds delay between the toggling calls. Defaults to 100 ms. */ flashCell(row: number, cell: number, speed?: number): void; diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts index bf47abb08..dcc86bf14 100644 --- a/packages/common/src/services/__tests__/filter.service.spec.ts +++ b/packages/common/src/services/__tests__/filter.service.spec.ts @@ -317,7 +317,7 @@ describe('FilterService', () => { mockArgs = { grid: gridStub, column: mockColumn, node: document.getElementById(DOM_ELEMENT_ID) }; }); - it('should execute the search callback normally when a keyup event is triggered and searchTerms are defined', () => { + it('should execute the search callback normally when a input change event is triggered and searchTerms are defined', () => { const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'] }; const spySearchChange = jest.spyOn(service.onSearchChange, 'notify'); const spyEmit = jest.spyOn(service, 'emitFilterChanged'); @@ -325,7 +325,7 @@ describe('FilterService', () => { service.init(gridStub); service.bindLocalOnFilter(gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs, new Slick.EventData(), gridStub); - service.getFiltersMetadata()[0].callback(new CustomEvent(`keyup`), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true }); + service.getFiltersMetadata()[0].callback(new CustomEvent('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true }); expect(service.getColumnFilters()).toContainEntry(['firstName', expectationColumnFilter]); expect(spySearchChange).toHaveBeenCalledWith({ @@ -341,7 +341,7 @@ describe('FilterService', () => { expect(spyEmit).toHaveBeenCalledWith('local'); }); - it('should execute the callback normally when a keyup event is triggered and the searchTerm comes from this event.target', () => { + it('should execute the callback normally when a input change event is triggered and the searchTerm comes from this event.target', () => { const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'] }; const spySearchChange = jest.spyOn(service.onSearchChange, 'notify'); @@ -349,7 +349,7 @@ describe('FilterService', () => { service.bindLocalOnFilter(gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs, new Slick.EventData(), gridStub); - const mockEvent = new CustomEvent(`keyup`); + const mockEvent = new CustomEvent('input'); Object.defineProperty(mockEvent, 'target', { writable: true, configurable: true, value: { value: 'John' } }); service.getFiltersMetadata()[0].callback(mockEvent, { columnDef: mockColumn, operator: 'EQ', shouldTriggerQuery: true }); @@ -370,7 +370,7 @@ describe('FilterService', () => { service.init(gridStub); service.bindLocalOnFilter(gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs, new Slick.EventData(), gridStub); - service.getFiltersMetadata()[0].callback(new CustomEvent(`keyup`), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true }); + service.getFiltersMetadata()[0].callback(new CustomEvent('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true }); expect(service.getColumnFilters()).toEqual({}); }); @@ -428,9 +428,9 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs1, new Slick.EventData(), gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs2, new Slick.EventData(), gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs3, new Slick.EventData(), gridStub); - service.getFiltersMetadata()[1].callback(new CustomEvent(`keyup`), { columnDef: mockColumn3 }); - service.getFiltersMetadata()[0].callback(new CustomEvent(`keyup`), { columnDef: mockColumn1, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true }); - service.getFiltersMetadata()[1].callback(new CustomEvent(`keyup`), { columnDef: mockColumn2, operator: 'NE', searchTerms: ['Doe'], shouldTriggerQuery: true }); + 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 }); }); describe('clearFilterByColumnId method', () => { @@ -528,8 +528,8 @@ describe('FilterService', () => { service.bindLocalOnFilter(gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs1, new Slick.EventData(), gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs2, new Slick.EventData(), gridStub); - service.getFiltersMetadata()[0].callback(new CustomEvent(`keyup`), { columnDef: mockColumn1, operator: 'EQ', searchTerms: ['John'], shouldTriggerQuery: true }); - service.getFiltersMetadata()[1].callback(new CustomEvent(`keyup`), { columnDef: mockColumn2, operator: 'NE', searchTerms: ['Doe'], shouldTriggerQuery: true }); + 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 }); }); describe('clearFilterByColumnId method', () => { @@ -868,7 +868,7 @@ describe('FilterService', () => { const spy = jest.spyOn(gridOptionMock.backendServiceApi.service, 'processOnFilterChanged').mockReturnValue('backend query'); service.init(gridStub); - const mockEvent = new CustomEvent(`keyup`); + const mockEvent = new CustomEvent('input'); Object.defineProperty(mockEvent, 'target', { writable: true, configurable: true, value: { value: 'John' } }); // @ts-ignore diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts index 8d5c7d95e..eb9a8c5a3 100644 --- a/packages/common/src/services/filter.service.ts +++ b/packages/common/src/services/filter.service.ts @@ -59,7 +59,7 @@ export class FilterService { private _filtersMetadata: any[] = []; private _columnFilters: ColumnFilters = {}; private _grid: SlickGrid; - private _onSearchChange: SlickEvent; + private _onSearchChange: SlickEvent | null; private _tmpPreFilteredData: number[]; constructor(private filterFactory: FilterFactory, private pubSubService: PubSubService, private sharedService: SharedService) { @@ -78,7 +78,7 @@ export class FilterService { } /** Getter of the SlickGrid Event Handler */ - get onSearchChange(): SlickEvent { + get onSearchChange(): SlickEvent | null { return this._onSearchChange; } @@ -110,12 +110,12 @@ export class FilterService { } dispose() { - this.disposeColumnFilters(); - // unsubscribe all SlickGrid events if (this._eventHandler && this._eventHandler.unsubscribeAll) { this._eventHandler.unsubscribeAll(); } + this.disposeColumnFilters(); + this._onSearchChange = null; } /** @@ -170,8 +170,10 @@ export class FilterService { }); // subscribe to the SlickGrid event and call the backend execution - const onSearchChangeHandler = this._onSearchChange; - (this._eventHandler as SlickEventHandler>).subscribe(onSearchChangeHandler, this.onBackendFilterChange.bind(this)); + if (this._onSearchChange) { + const onSearchChangeHandler = this._onSearchChange; + (this._eventHandler as SlickEventHandler>).subscribe(onSearchChangeHandler, this.onBackendFilterChange.bind(this)); + } } /** @@ -185,28 +187,30 @@ export class FilterService { this._dataView.setFilterArgs({ columnFilters: this._columnFilters, grid: this._grid, dataView: this._dataView }); this._dataView.setFilter(this.customLocalFilter.bind(this)); - // bind any search filter change (e.g. input filter keyup event) - const onSearchChangeHandler = this._onSearchChange; - (this._eventHandler as SlickEventHandler>).subscribe(this._onSearchChange, (_e, args) => { - const isGridWithTreeData = this._gridOptions?.enableTreeData ?? false; - - // When using Tree Data, we need to do it in 2 steps - // step 1. we need to prefilter (search) the data prior, the result will be an array of IDs which are the node(s) and their parent nodes when necessary. - // step 2. calling the DataView.refresh() is what triggers the final filtering, with "customLocalFilter()" which will decide which rows should persist - if (isGridWithTreeData) { - this._tmpPreFilteredData = this.preFilterTreeData(this._dataView.getItems(), this._columnFilters); - } + // bind any search filter change (e.g. input filter input change event) + if (this._onSearchChange) { + const onSearchChangeHandler = this._onSearchChange; + (this._eventHandler as SlickEventHandler>).subscribe(this._onSearchChange, (_e, args) => { + const isGridWithTreeData = this._gridOptions?.enableTreeData ?? false; + + // When using Tree Data, we need to do it in 2 steps + // step 1. we need to prefilter (search) the data prior, the result will be an array of IDs which are the node(s) and their parent nodes when necessary. + // step 2. calling the DataView.refresh() is what triggers the final filtering, with "customLocalFilter()" which will decide which rows should persist + if (isGridWithTreeData) { + this._tmpPreFilteredData = this.preFilterTreeData(this._dataView.getItems(), this._columnFilters); + } - const columnId = args.columnId; - if (columnId !== null) { - this._dataView.refresh(); - } + const columnId = args.columnId; + if (columnId !== null) { + this._dataView.refresh(); + } - // emit an onFilterChanged event when it's not called by a clear filter - if (args && !args.clearFilterTriggered) { - this.emitFilterChanged(EmitterType.local); - } - }); + // emit an onFilterChanged event when it's not called by a clear filter + if (args && !args.clearFilterTriggered) { + this.emitFilterChanged(EmitterType.local); + } + }); + } // subscribe to SlickGrid onHeaderRowCellRendered event to create filter template const onHeaderRowCellRenderedHandler = grid.onHeaderRowCellRendered; @@ -807,7 +811,7 @@ export class FilterService { /** * Callback method that is called and executed by the individual Filter (DOM element), - * for example when user type in a word to search (which uses InputFilter), this Filter will execute the callback from a keyup event. + * for example when user type in a word to search (which uses InputFilter), this Filter will execute the callback from an input change event. */ private callbackSearchEvent(event: SlickEventData | undefined, args: FilterCallbackArg) { if (args) { @@ -844,8 +848,9 @@ export class FilterService { const eventData = (event && typeof event.isPropagationStopped !== 'function') ? $.extend({}, new Slick.EventData(), event) : event; // trigger an event only if Filters changed or if ENTER key was pressed - const eventKeyCode = event && event.keyCode; - if (eventKeyCode === KeyCode.ENTER || !isequal(oldColumnFilters, this._columnFilters)) { + const eventKey = event?.key; + const eventKeyCode = event?.keyCode; + if (this._onSearchChange && (eventKey === 'Enter' || eventKeyCode === KeyCode.ENTER || !isequal(oldColumnFilters, this._columnFilters))) { this._onSearchChange.notify({ clearFilterTriggered: args.clearFilterTriggered, shouldTriggerQuery: args.shouldTriggerQuery, diff --git a/packages/common/src/services/gridEvent.service.ts b/packages/common/src/services/gridEvent.service.ts index 4ae2e5fee..41073de33 100644 --- a/packages/common/src/services/gridEvent.service.ts +++ b/packages/common/src/services/gridEvent.service.ts @@ -1,12 +1,12 @@ import { Column, - SlickDataView, + GetSlickEventType, GridOption, OnEventArgs, + SlickDataView, SlickEventHandler, SlickGrid, SlickNamespace, - GetSlickEventType, } from './../interfaces/index'; // using external non-typed js libraries @@ -23,6 +23,10 @@ export class GridEventService { this._eventHandler = new Slick.EventHandler(); } + dispose() { + this._eventHandler.unsubscribeAll(); + } + /* OnCellChange Event */ bindOnBeforeEditCell(grid: SlickGrid) { const dataView = grid?.getData && grid.getData() as SlickDataView; @@ -118,8 +122,4 @@ export class GridEventService { } }); } - - dispose() { - this._eventHandler.unsubscribeAll(); - } } diff --git a/packages/common/src/services/shared.service.ts b/packages/common/src/services/shared.service.ts index 57473c2d5..75cb5baaa 100644 --- a/packages/common/src/services/shared.service.ts +++ b/packages/common/src/services/shared.service.ts @@ -10,7 +10,7 @@ export class SharedService { private _currentPagination: CurrentPagination; private _visibleColumns: Column[]; private _hideHeaderRowAfterPageLoad = false; - private _hierarchicalDataset: any[]; + private _hierarchicalDataset: any[] | undefined; private _internalPubSubService: PubSubService; private _externalRegisteredServices: any[]; @@ -115,12 +115,12 @@ export class SharedService { } /** Getter for the Hierarchical Tree Data dataset when the feature is enabled */ - get hierarchicalDataset(): any[] { + get hierarchicalDataset(): any[] | undefined { return this._hierarchicalDataset; } /** Getter for the Hierarchical Tree Data dataset when the feature is enabled */ - set hierarchicalDataset(hierarchicalDataset: any[]) { + set hierarchicalDataset(hierarchicalDataset: any[] | undefined) { this._hierarchicalDataset = hierarchicalDataset; } } diff --git a/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip b/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip index 97a33d710..2094b2bf6 100644 Binary files a/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip and b/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip differ diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-composite-editor.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-composite-editor.spec.ts index 363b56ac4..374b23e02 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-composite-editor.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-composite-editor.spec.ts @@ -98,7 +98,7 @@ const rowSelectionModelStub = { } as SlickRowSelectionModel; function createNewColumDefinitions(count) { - const columnsMock = []; + const columnsMock: Column[] = []; for (let i = 0; i < count; i++) { columnsMock.push({ id: `field${i}`, field: `field${i}`, name: `Field ${i}`, editor: { model: Editors.text, massUpdate: true }, width: 75 }); } @@ -148,6 +148,7 @@ describe('CompositeEditorService', () => { const newGridOptions = { ...gridOptionsMock, enableTranslate: true }; jest.spyOn(gridStub, 'getOptions').mockReturnValue(newGridOptions); + // @ts-ignore translateService = undefined; component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub, translateService); } catch (e) { @@ -225,6 +226,7 @@ describe('CompositeEditorService', () => { }); it('should throw an error when there are no rows or active cell selected', (done) => { + // @ts-ignore jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(null); component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); @@ -282,7 +284,7 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -296,17 +298,17 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); - const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel'); - const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; + const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -332,13 +334,13 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeContentElm = compositeContainerElm.querySelector('.slick-editor-modal-content.split-view'); - const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-6'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeContentElm = compositeContainerElm.querySelector('.slick-editor-modal-content.split-view') as HTMLSelectElement; + const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-6') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -361,13 +363,13 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details', viewColumnLayout: 'auto' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeContentElm = compositeContainerElm.querySelector('.slick-editor-modal-content.triple-split-view'); - const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-6'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeContentElm = compositeContainerElm.querySelector('.slick-editor-modal-content.triple-split-view') as HTMLSelectElement; + const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-6') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -389,7 +391,7 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(setActiveSpy).toHaveBeenCalledTimes(2); expect(setActiveSpy).toHaveBeenCalledWith(4, 0, false); @@ -404,7 +406,7 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -412,7 +414,7 @@ describe('CompositeEditorService', () => { component.dispose(); - const compositeContainerElm2 = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm2 = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(compositeContainerElm2).toBeFalsy(); }); @@ -422,14 +424,14 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details', backdrop: null }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); expect(compositeContainerElm).toBeTruthy(); compositeContainerElm.click(); - const compositeContainerElm2 = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm2 = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(compositeContainerElm2).toBeFalsy(); }); @@ -446,15 +448,15 @@ describe('CompositeEditorService', () => { component.openDetails(mockModalOptions); gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct, formValues: { field1: 'test' }, editors: {}, grid: gridStub }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); - const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field1DetailCellElm = field1DetailContainerElm.querySelector('.slick-cell'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; + const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field1DetailCellElm = field1DetailContainerElm.querySelector('.slick-cell') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; compositeFooterCancelBtnElm.click(); setTimeout(() => { @@ -486,15 +488,15 @@ describe('CompositeEditorService', () => { component.openDetails(mockModalOptions); gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct, formValues: { fieldX: 'test' }, editors: {}, grid: gridStub }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); - const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field1DetailCellElm = field1DetailContainerElm.querySelector('.slick-cell'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; + const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field1DetailCellElm = field1DetailContainerElm.querySelector('.slick-cell') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; compositeFooterCancelBtnElm.click(); setTimeout(() => { @@ -521,8 +523,8 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Editing ({{id}}) - {{product.name}}' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeTitleElm = compositeContainerElm.querySelector('.slick-editor-modal-title'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeTitleElm = compositeContainerElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -530,7 +532,7 @@ describe('CompositeEditorService', () => { component.dispose(); - const compositeContainerElm2 = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm2 = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(compositeContainerElm2).toBeFalsy(); expect(compositeTitleElm.textContent).toBe('Editing (222) - Product ABC'); }); @@ -542,7 +544,7 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; compositeContainerElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { code: 'Escape', bubbles: true @@ -563,7 +565,7 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; compositeContainerElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { code: 'Tab', bubbles: true @@ -583,8 +585,8 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterCancelBtnElm = compositeContainerElm.querySelector('.btn-cancel'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterCancelBtnElm = compositeContainerElm.querySelector('.btn-cancel') as HTMLSelectElement; compositeFooterCancelBtnElm.click(); expect(component).toBeTruthy(); @@ -602,8 +604,8 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Some Details', showCloseButtonOutside: true }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterCloseBtnElm = compositeContainerElm.querySelector('.close'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterCloseBtnElm = compositeContainerElm.querySelector('.close') as HTMLSelectElement; compositeFooterCloseBtnElm.click(); expect(component).toBeTruthy(); @@ -621,8 +623,8 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save') as HTMLSelectElement; compositeFooterSaveBtnElm.click(); expect(component).toBeTruthy(); @@ -633,7 +635,7 @@ describe('CompositeEditorService', () => { expect(saveSpy).toHaveBeenCalled(); }); - it('should execute "onAddNewRow" callback when triggered by a new item', (done) => { + it('should execute "onAddNewRow" callback when triggered by a new item', () => { const newGridOptions = { ...gridOptionsMock, enableAddRow: true }; const mockProduct1 = { id: 222, address: { zip: 123456 }, product: { name: 'Product ABC', price: 12.55 } }; const mockProduct2 = { address: { zip: 345678 }, product: { name: 'Product DEF', price: 22.33 } }; @@ -646,34 +648,31 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); - const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field1DetailCellElm = field1DetailContainerElm.querySelector('.slick-cell'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field1DetailCellElm = field1DetailContainerElm.querySelector('.slick-cell') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct2, formValues: { field1: 'test' }, editors: {}, grid: gridStub }); - gridStub.onAddNewRow.notify({ grid: gridStub, item: mockProduct2, column: columnsMock[0] }); compositeFooterSaveBtnElm.click(); + gridStub.onAddNewRow.notify({ grid: gridStub, item: mockProduct2, column: columnsMock[0] }); - setTimeout(() => { - expect(component).toBeTruthy(); - expect(component.constructor).toBeDefined(); - expect(compositeContainerElm).toBeTruthy(); - expect(compositeHeaderElm).toBeTruthy(); - expect(compositeTitleElm).toBeTruthy(); - expect(compositeTitleElm.textContent).toBe('Details'); - expect(field1LabelElm.textContent).toBe('Field 1'); - expect(field1DetailCellElm.classList.contains('modified')).toBe(true); - expect(gridSrvAddItemSpy).toHaveBeenCalledWith({ ...mockProduct2, id: 2 }, undefined); - expect(saveSpy).toHaveBeenCalled(); - done(); - }, 5); + expect(component).toBeTruthy(); + expect(component.constructor).toBeDefined(); + expect(compositeContainerElm).toBeTruthy(); + expect(compositeHeaderElm).toBeTruthy(); + expect(compositeTitleElm).toBeTruthy(); + expect(compositeTitleElm.textContent).toBe('Details'); + expect(field1LabelElm.textContent).toBe('Field 1'); + expect(field1DetailCellElm.classList.contains('modified')).toBe(true); + expect(gridSrvAddItemSpy).toHaveBeenCalledWith({ ...mockProduct2, id: 2 }, undefined); + expect(saveSpy).toHaveBeenCalled(); }); describe('Form Logics', () => { @@ -685,13 +684,13 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const field3DetailCellElm = field3DetailContainerElm.querySelector('.slick-cell'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const field3DetailCellElm = field3DetailContainerElm.querySelector('.slick-cell') as HTMLSelectElement; component.editors = { field3: mockEditor }; component.changeFormInputValue('field3', 'Field 3 different text'); @@ -715,13 +714,13 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const field3DetailCellElm = field3DetailContainerElm.querySelector('.slick-cell'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const field3DetailCellElm = field3DetailContainerElm.querySelector('.slick-cell') as HTMLSelectElement; component.editors = { field3: mockEditor }; component.changeFormInputValue('field3', 'Field 3 different text'); @@ -766,12 +765,12 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; component.editors = { field3: mockEditor }; component.disableFormInput('field3'); @@ -798,10 +797,10 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; component.editors = { field3: mockEditor }; component.changeFormEditorOption('field3', 'minDate', 'today'); @@ -845,12 +844,12 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: 'Details' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeBodyElm = compositeContainerElm.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; component.editors = { field3: mockEditor }; component.disableFormInput('field3', false); @@ -913,9 +912,9 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: '', modalType: 'auto-mass', headerTitleMassUpdate: 'Mass Update', headerTitleMassSelection: 'Mass Selection' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; expect(component).toBeTruthy(); expect(compositeContainerElm).toBeTruthy(); @@ -930,9 +929,9 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: '', modalType: 'auto-mass', headerTitleMassUpdate: 'Mass Update', headerTitleMassSelection: 'Mass Selection' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeHeaderElm = compositeContainerElm.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; expect(component).toBeTruthy(); expect(compositeContainerElm).toBeTruthy(); @@ -948,7 +947,7 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails({ headerTitle: '', modalType: 'auto-mass', headerTitleMassUpdate: 'Mass Update', headerTitleMassSelection: 'Mass Selection' }); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; expect(setActiveSpy).toHaveBeenCalledTimes(2); expect(setActiveSpy).toHaveBeenCalledWith(4, 2, false); @@ -975,14 +974,14 @@ describe('CompositeEditorService', () => { component.openDetails(mockModalOptions); const spyOnError = jest.spyOn(mockModalOptions, 'onError'); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary') as HTMLSelectElement; gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct, formValues: {}, editors: {}, grid: gridStub }); @@ -1024,14 +1023,14 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary') as HTMLSelectElement; gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct, formValues: { field3: 'test' }, editors: {}, grid: gridStub }); @@ -1088,14 +1087,14 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary') as HTMLSelectElement; gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct1, formValues: { field3: 'test' }, editors: {}, grid: gridStub }); @@ -1138,14 +1137,14 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary') as HTMLSelectElement; gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct1, formValues: { field3: 'test' }, editors: {}, grid: gridStub }); @@ -1186,14 +1185,14 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary') as HTMLSelectElement; gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct1, formValues: { field3: 'test' }, editors: {}, grid: gridStub }); @@ -1243,15 +1242,15 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub, translateService); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12'); - const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); - const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel'); - const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field1DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field1.slick-col-medium-12') as HTMLSelectElement; + const field1LabelElm = field1DetailContainerElm.querySelector('.item-details-label.editor-field1') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; + const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -1281,15 +1280,15 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub, translateService); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); - const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel'); - const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; + const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save') as HTMLSelectElement; gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct, formValues: { field3: 'test' }, editors: {}, grid: gridStub }); @@ -1326,15 +1325,15 @@ describe('CompositeEditorService', () => { component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub, translateService); component.openDetails(mockModalOptions); - const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456'); - const compositeHeaderElm = document.querySelector('.slick-editor-modal-header'); - const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title'); - const compositeBodyElm = document.querySelector('.slick-editor-modal-body'); - const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12'); - const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3'); - const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer'); - const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel'); - const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save'); + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement; + const compositeFooterCancelBtnElm = compositeFooterElm.querySelector('.btn-cancel') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeFooterElm.querySelector('.btn-save') as HTMLSelectElement; setTimeout(() => { expect(component).toBeTruthy(); diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts index 5270145a9..2f121d210 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts @@ -1,4 +1,4 @@ -import { GridOption, SlickGrid } from '@slickgrid-universal/common'; +import { CustomFooterOption, GridOption, SlickGrid } from '@slickgrid-universal/common'; import { SlickFooterComponent } from '../slick-footer.component'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; @@ -48,7 +48,7 @@ describe('Slick-Footer Component', () => { }); it('should make sure Slick-Footer is being created and rendered', () => { - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); @@ -59,12 +59,12 @@ describe('Slick-Footer Component', () => { }); it('should create a the Slick-Footer component in the DOM', () => { - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); - const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); - const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer'); - const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics'); + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; expect(translateService.getCurrentLanguage()).toBe('en'); expect(footerContainerElm).toBeTruthy(); @@ -73,16 +73,16 @@ describe('Slick-Footer Component', () => { }); it('should create a the Slick-Footer component with only left side content when everything else is defined as hidden', () => { - mockGridOptions.customFooterOptions.hideLastUpdateTimestamp = true; - mockGridOptions.customFooterOptions.hideMetrics = true; + (mockGridOptions.customFooterOptions as CustomFooterOption).hideLastUpdateTimestamp = true; + (mockGridOptions.customFooterOptions as CustomFooterOption).hideMetrics = true; - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; - const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); - const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer'); - const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics'); + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; expect(footerContainerElm).toBeTruthy(); expect(leftFooterElm).toBeTruthy(); @@ -92,13 +92,13 @@ describe('Slick-Footer Component', () => { }); it('should create a the Slick-Footer component in the DOM with only right side with last update timestamp & items count', () => { - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; - const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); - const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer'); - const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics'); + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; expect(footerContainerElm).toBeTruthy(); expect(leftFooterElm).toBeTruthy(); @@ -112,16 +112,16 @@ describe('Slick-Footer Component', () => { }); it('should create a the Slick-Footer component in the DOM with metrics but without timestamp when hidding it', () => { - mockGridOptions.customFooterOptions.hideMetrics = false; - mockGridOptions.customFooterOptions.hideLastUpdateTimestamp = true; + (mockGridOptions.customFooterOptions as CustomFooterOption).hideMetrics = false; + (mockGridOptions.customFooterOptions as CustomFooterOption).hideLastUpdateTimestamp = true; - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; - const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); - const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer'); - const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics'); + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; expect(footerContainerElm).toBeTruthy(); expect(leftFooterElm).toBeTruthy(); @@ -135,17 +135,17 @@ describe('Slick-Footer Component', () => { }); it('should create a the Slick-Footer component in the DOM with metrics but without timestamp neither totalCount when hidding it', () => { - mockGridOptions.customFooterOptions.hideMetrics = false; - mockGridOptions.customFooterOptions.hideLastUpdateTimestamp = true; - mockGridOptions.customFooterOptions.hideTotalItemCount = true; + (mockGridOptions.customFooterOptions as CustomFooterOption).hideMetrics = false; + (mockGridOptions.customFooterOptions as CustomFooterOption).hideLastUpdateTimestamp = true; + (mockGridOptions.customFooterOptions as CustomFooterOption).hideTotalItemCount = true; - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; - const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); - const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer'); - const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics'); + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; expect(footerContainerElm).toBeTruthy(); expect(leftFooterElm).toBeTruthy(); @@ -159,15 +159,15 @@ describe('Slick-Footer Component', () => { }); it('should create a the Slick-Footer component in the DOM and expect to use default English locale when none of the metricsText are defined', () => { - mockGridOptions.customFooterOptions.metricTexts = { items: '', lastUpdate: '', of: '' }; + (mockGridOptions.customFooterOptions as CustomFooterOption).metricTexts = { items: '', lastUpdate: '', of: '' }; - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; - const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); - const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer'); - const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics'); + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; expect(footerContainerElm).toBeTruthy(); expect(leftFooterElm).toBeTruthy(); @@ -181,17 +181,17 @@ describe('Slick-Footer Component', () => { }); it('should create a the Slick-Footer component in the DOM and use different locale when enableTranslate is enabled', () => { - mockGridOptions.customFooterOptions.metricTexts = { itemsKey: 'ITEMS', lastUpdateKey: 'LAST_UPDATE', ofKey: 'OF' }; + (mockGridOptions.customFooterOptions as CustomFooterOption).metricTexts = { itemsKey: 'ITEMS', lastUpdateKey: 'LAST_UPDATE', ofKey: 'OF' }; mockGridOptions.enableTranslate = true; translateService.use('fr'); - component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions, translateService); + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); component.renderFooter(div); component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; - const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456'); - const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer'); - const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics'); + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; expect(footerContainerElm).toBeTruthy(); expect(leftFooterElm).toBeTruthy(); diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-pagination-without-i18n.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-pagination-without-i18n.spec.ts index 60810ddfd..9056b5881 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-pagination-without-i18n.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-pagination-without-i18n.spec.ts @@ -83,6 +83,7 @@ describe('Slick-Pagination Component', () => { it('should throw an error when "enableTranslate" is set and I18N Service is not provided', (done) => { try { mockGridOptions.enableTranslate = true; + // @ts-ignore translateService = null; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(mockGridOptions); @@ -101,8 +102,8 @@ describe('Slick-Pagination Component', () => { component = new SlickPaginationComponent(paginationServiceStub, eventPubSubService, sharedService, translateService); component.renderPagination(div); - const pageInfoFromTo = document.querySelector('.page-info-from-to'); - const pageInfoTotalItems = document.querySelector('.page-info-total-items'); + const pageInfoFromTo = document.querySelector('.page-info-from-to') as HTMLSpanElement; + const pageInfoTotalItems = document.querySelector('.page-info-total-items') as HTMLSpanElement; expect(translateService.getCurrentLanguage()).toBe('en'); expect(removeExtraSpaces(pageInfoFromTo.innerHTML)).toBe('10-15of'); diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-pagination.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-pagination.spec.ts index 22f8a9910..cb93adc53 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-pagination.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-pagination.spec.ts @@ -82,7 +82,7 @@ describe('Slick-Pagination Component', () => { }); it('should make sure Slick-Pagination is defined', () => { - const paginationElm = document.querySelector('div.pager.slickgrid_123456'); + const paginationElm = document.querySelector('div.pager.slickgrid_123456') as HTMLSelectElement; expect(component).toBeTruthy(); expect(component.constructor).toBeDefined(); @@ -90,9 +90,9 @@ describe('Slick-Pagination Component', () => { }); it('should create a the Slick-Pagination component in the DOM', () => { - const pageInfoFromTo = document.querySelector('.page-info-from-to'); - const pageInfoTotalItems = document.querySelector('.page-info-total-items'); - const itemsPerPage = document.querySelector('.items-per-page'); + const pageInfoFromTo = document.querySelector('.page-info-from-to') as HTMLSpanElement; + const pageInfoTotalItems = document.querySelector('.page-info-total-items') as HTMLSpanElement; + const itemsPerPage = document.querySelector('.items-per-page') as HTMLSelectElement; expect(translateService.getCurrentLanguage()).toBe('en'); expect(removeExtraSpaces(pageInfoFromTo.innerHTML)).toBe('10-15of'); @@ -103,7 +103,7 @@ describe('Slick-Pagination Component', () => { it('should call changeToFirstPage() from the View and expect the pagination service to be called with correct method', () => { const spy = jest.spyOn(paginationServiceStub, 'goToFirstPage'); - const button = document.querySelector('.icon-seek-first'); + const button = document.querySelector('.icon-seek-first') as HTMLAnchorElement; button.click(); mockFullPagination.pageNumber = 1; mockFullPagination.dataFrom = 1; @@ -111,9 +111,9 @@ describe('Slick-Pagination Component', () => { jest.spyOn(paginationServiceStub, 'dataFrom', 'get').mockReturnValue(mockFullPagination.dataFrom); jest.spyOn(paginationServiceStub, 'dataTo', 'get').mockReturnValue(mockFullPagination.dataTo); - const input = document.querySelector('input.form-control'); - const itemFrom = document.querySelector('.item-from'); - const itemTo = document.querySelector('.item-to'); + const input = document.querySelector('input.form-control') as HTMLInputElement; + const itemFrom = document.querySelector('.item-from') as HTMLInputElement; + const itemTo = document.querySelector('.item-to') as HTMLInputElement; expect(spy).toHaveBeenCalled(); expect(input.value).toBe('1'); @@ -127,7 +127,7 @@ describe('Slick-Pagination Component', () => { it('should call changeToNextPage() from the View and expect the pagination service to be called with correct method', () => { const spy = jest.spyOn(paginationServiceStub, 'goToNextPage'); - const button = document.querySelector('.icon-seek-next'); + const button = document.querySelector('.icon-seek-next') as HTMLAnchorElement; button.click(); expect(spy).toHaveBeenCalled(); @@ -137,7 +137,7 @@ describe('Slick-Pagination Component', () => { mockFullPagination.pageNumber = 2; const spy = jest.spyOn(paginationServiceStub, 'goToPreviousPage'); - const button = document.querySelector('.icon-seek-prev'); + const button = document.querySelector('.icon-seek-prev') as HTMLAnchorElement; button.click(); expect(spy).toHaveBeenCalled(); @@ -146,7 +146,7 @@ describe('Slick-Pagination Component', () => { it('should call changeToLastPage() from the View and expect the pagination service to be called with correct method', () => { const spy = jest.spyOn(paginationServiceStub, 'goToLastPage'); - const button = document.querySelector('.icon-seek-end'); + const button = document.querySelector('.icon-seek-end') as HTMLAnchorElement; button.click(); expect(spy).toHaveBeenCalled(); @@ -156,7 +156,7 @@ describe('Slick-Pagination Component', () => { const spy = jest.spyOn(paginationServiceStub, 'goToPageNumber'); const newPageNumber = 3; - const input = document.querySelector('input.form-control'); + const input = document.querySelector('input.form-control') as HTMLInputElement; input.value = `${newPageNumber}`; const mockEvent = new CustomEvent('change', { bubbles: true, detail: { target: { value: newPageNumber } } }); input.dispatchEvent(mockEvent); @@ -169,7 +169,7 @@ describe('Slick-Pagination Component', () => { const spy = jest.spyOn(paginationServiceStub, 'changeItemPerPage'); const newItemsPerPage = 10; - const select = document.querySelector('select'); + const select = document.querySelector('select') as HTMLSelectElement; select.value = `${newItemsPerPage}`; const mockEvent = new CustomEvent('change', { bubbles: true, detail: { target: { value: newItemsPerPage } } }); select.dispatchEvent(mockEvent); @@ -182,7 +182,7 @@ describe('Slick-Pagination Component', () => { mockFullPagination.totalItems = 100; component.pageNumber = 1; eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); - const pageFromToElm = document.querySelector('span.page-info-from-to'); + const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; expect(component.firstButtonClasses).toBe('page-item seek-first disabled'); expect(component.prevButtonClasses).toBe('page-item seek-prev disabled'); @@ -197,7 +197,7 @@ describe('Slick-Pagination Component', () => { mockFullPagination.totalItems = 100; component.pageNumber = 10; eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); - const pageFromToElm = document.querySelector('span.page-info-from-to'); + const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; expect(component.firstButtonClasses).toBe('page-item seek-first'); expect(component.prevButtonClasses).toBe('page-item seek-prev'); @@ -211,7 +211,7 @@ describe('Slick-Pagination Component', () => { mockFullPagination.totalItems = 0; component.pageNumber = 0; eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); - const pageFromToElm = document.querySelector('span.page-info-from-to'); + const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; expect(component.firstButtonClasses).toBe('page-item seek-first disabled'); expect(component.prevButtonClasses).toBe('page-item seek-prev disabled'); @@ -258,8 +258,8 @@ describe('with different i18n locale', () => { eventPubSubService.publish('onLanguageChange', 'fr'); setTimeout(() => { - const pageInfoFromTo = document.querySelector('.page-info-from-to'); - const pageInfoTotalItems = document.querySelector('.page-info-total-items'); + const pageInfoFromTo = document.querySelector('.page-info-from-to') as HTMLSpanElement; + const pageInfoTotalItems = document.querySelector('.page-info-total-items') as HTMLSpanElement; expect(translateService.getCurrentLanguage()).toBe('fr'); expect(removeExtraSpaces(pageInfoFromTo.innerHTML)).toBe(`10-15de`); expect(removeExtraSpaces(pageInfoTotalItems.innerHTML)).toBe(`95éléments`); diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts index 03bace715..0fc8678c0 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts @@ -185,6 +185,7 @@ const mockDataView = { getItem: jest.fn(), getItems: jest.fn(), getItemMetadata: jest.fn(), + getLength: jest.fn(), getPagingInfo: jest.fn(), mapIdsToRows: jest.fn(), mapRowsToIds: jest.fn(), @@ -232,6 +233,7 @@ const mockGrid = { getEditorLock: () => mockGetEditorLock, getUID: () => 'slickgrid_12345', getContainerNode: jest.fn(), + getGridPosition: jest.fn(), getOptions: jest.fn(), getSelectionModel: jest.fn(), getScrollbarDimensions: jest.fn(), @@ -294,7 +296,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () minWidth: 300, rightPadding: 0, }, - } as GridOption; + } as unknown as GridOption; sharedService = new SharedService(); translateService = new TranslateServiceStub(); eventPubSubService = new EventPubSubService(divContainer); @@ -306,7 +308,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () columnDefinitions, gridOptions, dataset, - null, + undefined, { collectionService: collectionServiceStub, eventPubSubService, @@ -458,6 +460,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const autosizeSpy = jest.spyOn(mockGrid, 'autosizeColumns'); const refreshSpy = jest.spyOn(component, 'refreshGridData'); const mockData = [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' }]; + jest.spyOn(mockDataView, 'getLength').mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValueOnce(mockData.length); component.gridOptions = { autoFitColumnsOnFirstLoad: true }; component.initialization(divContainer, slickEventHandler); @@ -471,6 +474,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const autosizeSpy = jest.spyOn(mockGrid, 'autosizeColumns'); const refreshSpy = jest.spyOn(component, 'refreshGridData'); const mockData = [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' }]; + jest.spyOn(mockDataView, 'getLength').mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValueOnce(mockData.length); component.gridOptions = { autoFitColumnsOnFirstLoad: false }; component.initialization(divContainer, slickEventHandler); @@ -715,7 +719,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const globalEaSpy = jest.spyOn(eventPubSubService, 'publish'); const sortSpy = jest.spyOn(sortServiceStub, 'loadGridSorters'); - component.gridOptions = { presets: { sorters: [{ columnId: 'field1', direction: 'DESC' }] } } as GridOption; + component.gridOptions = { presets: { sorters: [{ columnId: 'field1', direction: 'DESC' }] } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(globalEaSpy).toHaveBeenNthCalledWith(3, 'onGridCreated', expect.any(Object)); @@ -726,7 +730,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () jest.spyOn(mockGrid, 'getSelectionModel').mockReturnValue(true); const syncSpy = jest.spyOn(mockDataView, 'syncGridSelection'); - component.gridOptions = { dataView: { syncGridSelection: true }, enableRowSelection: true } as GridOption; + component.gridOptions = { dataView: { syncGridSelection: true }, enableRowSelection: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(syncSpy).toHaveBeenCalledWith(component.slickGrid, true); @@ -736,7 +740,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () jest.spyOn(mockGrid, 'getSelectionModel').mockReturnValue(true); const syncSpy = jest.spyOn(mockDataView, 'syncGridSelection'); - component.gridOptions = { dataView: { syncGridSelection: false }, enableRowSelection: true } as GridOption; + component.gridOptions = { dataView: { syncGridSelection: false }, enableRowSelection: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(syncSpy).toHaveBeenCalledWith(component.slickGrid, false); @@ -749,7 +753,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () component.gridOptions = { dataView: { syncGridSelection: { preserveHidden: true, preserveHiddenOnSelectionChange: false } }, enableRowSelection: true - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(syncSpy).toHaveBeenCalledWith(component.slickGrid, true, false); @@ -766,7 +770,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }, dataView: { syncGridSelection: true, syncGridSelectionWithBackendService: true }, enableRowSelection: true - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(syncSpy).toHaveBeenCalledWith(component.slickGrid, true); @@ -783,7 +787,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }, dataView: { syncGridSelection: false, syncGridSelectionWithBackendService: true }, enableRowSelection: true - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(syncSpy).toHaveBeenCalledWith(component.slickGrid, false); @@ -800,7 +804,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }, dataView: { syncGridSelection: true, syncGridSelectionWithBackendService: false }, enableRowSelection: true - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(syncSpy).toHaveBeenCalledWith(component.slickGrid, false); @@ -817,7 +821,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should call "showHeaderRow" method with false when its flag is disabled', () => { const gridSpy = jest.spyOn(mockGrid, 'setHeaderRowVisibility'); - component.gridOptions = { showHeaderRow: false } as GridOption; + component.gridOptions = { showHeaderRow: false } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(gridSpy).toHaveBeenCalledWith(false, false); @@ -826,7 +830,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should initialize groupingAndColspanService when "createPreHeaderPanel" grid option is enabled and "enableDraggableGrouping" is disabled', () => { const spy = jest.spyOn(groupingAndColspanServiceStub, 'init'); - component.gridOptions = { createPreHeaderPanel: true, enableDraggableGrouping: false } as GridOption; + component.gridOptions = { createPreHeaderPanel: true, enableDraggableGrouping: false } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(spy).toHaveBeenCalledWith(mockGrid, sharedService); @@ -835,7 +839,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should not initialize groupingAndColspanService when "createPreHeaderPanel" grid option is enabled and "enableDraggableGrouping" is also enabled', () => { const spy = jest.spyOn(groupingAndColspanServiceStub, 'init'); - component.gridOptions = { createPreHeaderPanel: true, enableDraggableGrouping: true } as GridOption; + component.gridOptions = { createPreHeaderPanel: true, enableDraggableGrouping: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(spy).not.toHaveBeenCalled(); @@ -844,21 +848,21 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should call "translateColumnHeaders" from ExtensionService when "enableTranslate" is set', () => { const spy = jest.spyOn(extensionServiceStub, 'translateColumnHeaders'); - component.gridOptions = { enableTranslate: true } as GridOption; + component.gridOptions = { enableTranslate: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(spy).toHaveBeenCalled(); }); it('should initialize SlickCompositeEditorComponent when "enableCompositeEditor" is set', () => { - component.gridOptions = { enableCompositeEditor: true } as GridOption; + component.gridOptions = { enableCompositeEditor: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(component.slickCompositeEditor instanceof SlickCompositeEditorComponent).toBeTrue(); }); it('should initialize ExportService when "enableExport" is set when using Salesforce', () => { - component.gridOptions = { enableExport: true, useSalesforceDefaultGridOptions: true } as GridOption; + component.gridOptions = { enableExport: true, useSalesforceDefaultGridOptions: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(FileExportService).toHaveBeenCalled(); @@ -878,7 +882,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should bind local filter when "enableFiltering" is set', () => { const bindLocalSpy = jest.spyOn(filterServiceStub, 'bindLocalOnFilter'); - component.gridOptions = { enableFiltering: true } as GridOption; + component.gridOptions = { enableFiltering: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid); @@ -887,7 +891,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should bind local sort when "enableSorting" is set', () => { const bindLocalSpy = jest.spyOn(sortServiceStub, 'bindLocalOnSort'); - component.gridOptions = { enableSorting: true } as GridOption; + component.gridOptions = { enableSorting: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid); @@ -1162,7 +1166,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const transGroupingColSpanSpy = jest.spyOn(groupingAndColspanServiceStub, 'translateGroupingAndColSpan'); const setHeaderRowSpy = jest.spyOn(mockGrid, 'setHeaderRowVisibility'); - component.gridOptions = { enableTranslate: true, createPreHeaderPanel: false, enableDraggableGrouping: false } as GridOption; + component.gridOptions = { enableTranslate: true, createPreHeaderPanel: false, enableDraggableGrouping: false } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); eventPubSubService.publish('onLanguageChange', { language: 'fr' }); @@ -1190,7 +1194,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const transHeaderMenuSpy = jest.spyOn(extensionServiceStub, 'translateHeaderMenu'); const transGroupingColSpanSpy = jest.spyOn(groupingAndColspanServiceStub, 'translateGroupingAndColSpan'); - component.gridOptions = { enableTranslate: true, createPreHeaderPanel: true, enableDraggableGrouping: false } as GridOption; + component.gridOptions = { enableTranslate: true, createPreHeaderPanel: true, enableDraggableGrouping: false } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); eventPubSubService.publish('onLanguageChange', {}); @@ -1210,7 +1214,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should call "translateGroupingAndColSpan" translate methods when locale changes and Column Grouping PreHeader are enabled', (done) => { const groupColSpanSpy = jest.spyOn(groupingAndColspanServiceStub, 'translateGroupingAndColSpan'); - component.gridOptions = { enableTranslate: true, createPreHeaderPanel: true, enableDraggableGrouping: false } as GridOption; + component.gridOptions = { enableTranslate: true, createPreHeaderPanel: true, enableDraggableGrouping: false } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); eventPubSubService.publish('onLanguageChange', {}); @@ -1227,7 +1231,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const getAssocColSpy = jest.spyOn(gridStateServiceStub, 'getAssociatedGridColumns').mockReturnValue(mockCols); const setColSpy = jest.spyOn(mockGrid, 'setColumns'); - component.gridOptions = { presets: { columns: mockColsPresets } } as GridOption; + component.gridOptions = { presets: { columns: mockColsPresets } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(getAssocColSpy).toHaveBeenCalledWith(mockGrid, mockColsPresets); @@ -1242,7 +1246,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const setColSpy = jest.spyOn(mockGrid, 'setColumns'); component.columnDefinitions = mockCols; - component.gridOptions = { enableCheckboxSelector: true, presets: { columns: mockColsPresets } } as GridOption; + component.gridOptions = { enableCheckboxSelector: true, presets: { columns: mockColsPresets } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(getAssocColSpy).toHaveBeenCalledWith(mockGrid, mockColsPresets); @@ -1266,7 +1270,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () process: () => new Promise(resolve => resolve({ data: { users: { nodes: [], totalCount: 100 } } })), } as GraphqlServiceApi, pagination: mockPagination, - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(bindBackendSpy).toHaveBeenCalledWith(mockGrid); @@ -1283,7 +1287,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () preProcess: () => jest.fn(), process: () => new Promise(resolve => resolve('process resolved')), } - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(bindBackendSpy).toHaveBeenCalledWith(mockGrid); @@ -1300,7 +1304,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () preProcess: () => jest.fn(), process: () => new Promise(resolve => resolve('process resolved')), } - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid); @@ -1311,7 +1315,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const bindLocalSpy = jest.spyOn(filterServiceStub, 'bindLocalOnFilter'); const populateSpy = jest.spyOn(filterServiceStub, 'populateColumnFilterSearchTermPresets'); - component.gridOptions = { enableFiltering: true } as GridOption; + component.gridOptions = { enableFiltering: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(initSpy).toHaveBeenCalledWith(mockGrid); @@ -1330,7 +1334,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () preProcess: () => jest.fn(), process: () => new Promise(resolve => resolve('process resolved')), } - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(bindLocalSpy).toHaveBeenCalledWith(mockGrid); @@ -1348,7 +1352,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () preProcess: () => jest.fn(), process: () => new Promise(resolve => resolve('process resolved')), } - } as GridOption; + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(initSpy).toHaveBeenCalledWith(mockGrid); @@ -1361,7 +1365,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const initSpy = jest.spyOn(filterServiceStub, 'init'); const populateSpy = jest.spyOn(filterServiceStub, 'populateColumnFilterSearchTermPresets'); - component.gridOptions = { enableFiltering: true, presets: { filters: mockPresetFilters } } as GridOption; + component.gridOptions = { enableFiltering: true, presets: { filters: mockPresetFilters } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(initSpy).toHaveBeenCalledWith(mockGrid); @@ -1371,7 +1375,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () it('should return null when "getItemMetadata" is called without a colspan callback defined', () => { const itemSpy = jest.spyOn(mockDataView, 'getItem'); - component.gridOptions = { colspanCallback: undefined } as GridOption; + component.gridOptions = { colspanCallback: undefined } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); mockDataView.getItemMetadata(2); @@ -1383,7 +1387,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const mockItem = { firstName: 'John', lastName: 'Doe' }; const itemSpy = jest.spyOn(mockDataView, 'getItem').mockReturnValue(mockItem); - component.gridOptions = { colspanCallback: mockCallback } as GridOption; + component.gridOptions = { colspanCallback: mockCallback } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); mockDataView.getItemMetadata(2); @@ -1495,7 +1499,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const setRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); jest.spyOn(gridStateServiceStub, 'getCurrentGridState').mockReturnValue({ columns: [], pagination: mockPagination } as GridState); - component.gridOptions = { enableRowSelection: true } as GridOption; + component.gridOptions = { enableRowSelection: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); component.paginationChanged(mockPagination); @@ -1512,7 +1516,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const setRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); jest.spyOn(gridStateServiceStub, 'getCurrentGridState').mockReturnValue({ columns: [], pagination: mockPagination } as GridState); - component.gridOptions = { enableCheckboxSelector: true } as GridOption; + component.gridOptions = { enableCheckboxSelector: true } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); component.paginationChanged(mockPagination); @@ -1524,6 +1528,30 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }); }); + describe('Empty Warning Message', () => { + it('should display an Empty Warning Message when "enableEmptyDataWarningMessage" is enabled and the dataset is empty', (done) => { + const mockColDefs = [{ id: 'name', field: 'name', editor: undefined, internalColumnEditor: {} }]; + const mockGridOptions = { enableTranslate: true, enableEmptyDataWarningMessage: true, }; + jest.spyOn(mockGrid, 'getOptions').mockReturnValue(mockGridOptions); + jest.spyOn(mockGrid, 'getGridPosition').mockReturnValue({ top: 10, left: 20 }); + + component.gridOptions = mockGridOptions; + component.initialization(divContainer, slickEventHandler); + const emptySpy = jest.spyOn(component.slickEmptyWarning, 'showEmptyDataMessage'); + component.columnDefinitions = mockColDefs; + component.refreshGridData([]); + mockDataView.onRowCountChanged.notify({ first: 'John' }); + + setTimeout(() => { + expect(component.columnDefinitions).toEqual(mockColDefs); + expect(component.gridOptions.enableEmptyDataWarningMessage).toBeTrue(); + expect(component.slickEmptyWarning).toBeTruthy(); + expect(emptySpy).toHaveBeenCalledTimes(2); + done(); + }); + }); + }); + describe('Custom Footer', () => { it('should have a Custom Footer when "showCustomFooter" is enabled and there are no Pagination used', (done) => { const mockColDefs = [{ id: 'name', field: 'name', editor: undefined, internalColumnEditor: {} }]; @@ -1661,6 +1689,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const mockData = [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' }]; const selectRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); jest.spyOn(mockGrid, 'getSelectionModel').mockReturnValue(true); + jest.spyOn(mockDataView, 'getLength').mockReturnValue(mockData.length); component.gridOptions.enableRowSelection = true; component.gridOptions.presets = { rowSelection: { gridRowIndexes: selectedGridRows } }; @@ -1680,6 +1709,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const mockData = [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' }]; const selectRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); jest.spyOn(mockGrid, 'getSelectionModel').mockReturnValue(true); + jest.spyOn(mockDataView, 'getLength').mockReturnValue(mockData.length); component.gridOptions = { enableRowSelection: true, @@ -1741,7 +1771,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () describe('Tree Data View', () => { it('should throw an error when enableTreeData is enabled without passing a "columnId"', (done) => { try { - component.gridOptions = { enableTreeData: true, treeDataOptions: {} } as GridOption; + component.gridOptions = { enableTreeData: true, treeDataOptions: {} } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); } catch (e) { @@ -1756,7 +1786,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }]; const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set'); - component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' } } as GridOption; + component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); component.dataset = mockFlatDataset; @@ -1770,7 +1800,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const setItemsSpy = jest.spyOn(mockDataView, 'setItems'); const processSpy = jest.spyOn(sortServiceStub, 'processTreeDataInitialSort'); - component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as GridOption; + component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); component.datasetHierarchical = mockHierarchical; @@ -1841,7 +1871,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with minWidth: 300, rightPadding: 0, }, - } as GridOption; + } as unknown as GridOption; sharedService = new SharedService(); translateService = new TranslateServiceStub(); eventPubSubService = new EventPubSubService(divContainer); @@ -1879,7 +1909,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with it('should provide hierarchical dataset by the constructor and expect processTreeDataInitialSort being called with other methods', () => { const mockHierarchical = [{ file: 'documents', files: [{ file: 'vacation.txt' }] }]; - component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as GridOption; + component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical); @@ -1913,7 +1943,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with dataset = []; columnDefinitions = [{ id: 'name', field: 'name' }]; - gridOptions = {} as GridOption; + gridOptions = {} as unknown as GridOption; sharedService = new SharedService(); translateService = new TranslateServiceStub(); eventPubSubService = new EventPubSubService(divContainer); diff --git a/packages/vanilla-bundle/src/components/slick-composite-editor.component.ts b/packages/vanilla-bundle/src/components/slick-composite-editor.component.ts index 8ee3e8f57..1b8da1426 100644 --- a/packages/vanilla-bundle/src/components/slick-composite-editor.component.ts +++ b/packages/vanilla-bundle/src/components/slick-composite-editor.component.ts @@ -8,6 +8,7 @@ import { CurrentRowSelection, Editor, EditorValidationResult, + ElementEventListener, getDescendantProperty, GetSlickEventType, GridOption, @@ -32,6 +33,7 @@ const DEFAULT_ON_ERROR = (error: OnErrorOption) => console.log(error.message); export class SlickCompositeEditorComponent { private _eventHandler: SlickEventHandler; + private _boundedEventWithListeners: ElementEventListener[] = []; private _modalElm: HTMLDivElement; private _options: CompositeEditorOpenDetailOption; private _lastActiveRowNumber: number; @@ -81,9 +83,10 @@ export class SlickCompositeEditorComponent { /** Dispose of the Component & unsubscribe all events */ dispose() { + this.unbindAll(); + this._eventHandler.unsubscribeAll(); this._formValues = undefined; this.disposeComponent(); - this._eventHandler.unsubscribeAll(); } /** Dispose of the Component without unsubscribing any events */ @@ -93,10 +96,20 @@ export class SlickCompositeEditorComponent { // remove the body backdrop click listener, every other listeners will be dropped automatically since we destroy the component document.body.classList.remove('slick-modal-open'); - document.body.removeEventListener('click', this.handleBodyClicked); } } + /** Unbind All (remove) bounded elements with listeners */ + unbindAll() { + for (const boundedEvent of this._boundedEventWithListeners) { + const { element, eventName, listener } = boundedEvent; + if (element?.removeEventListener) { + element.removeEventListener(eventName, listener); + } + } + this._boundedEventWithListeners = []; + } + /** * Dynamically change value of an input from the Composite Editor form * @param {String} columnId - column id @@ -369,7 +382,7 @@ export class SlickCompositeEditorComponent { document.body.appendChild(this._modalElm); document.body.classList.add('slick-modal-open'); // add backdrop to body - document.body.addEventListener('click', this.handleBodyClicked.bind(this)); + this.addElementEventListener(document.body, 'click', this.handleBodyClicked.bind(this)); this._editors = {}; this._editorContainers = modalColumns.map(col => modalBodyElm.querySelector(`[data-editorid=${col.id}]`)) || []; @@ -381,12 +394,12 @@ export class SlickCompositeEditorComponent { // Add a few Event Handlers // keyboard, blur & button event handlers - modalCloseButtonElm.addEventListener('click', this.cancelEditing.bind(this)); - modalCancelButtonElm.addEventListener('click', this.cancelEditing.bind(this)); - this._modalSaveButtonElm.addEventListener('click', this.handleSaveClicked.bind(this)); - this._modalElm.addEventListener('keydown', this.handleKeyDown.bind(this)); - this._modalElm.addEventListener('focusout', this.validateCurrentEditor.bind(this)); - this._modalElm.addEventListener('blur', this.validateCurrentEditor.bind(this)); + this.addElementEventListener(modalCloseButtonElm, 'click', this.cancelEditing.bind(this)); + this.addElementEventListener(modalCancelButtonElm, 'click', this.cancelEditing.bind(this)); + this.addElementEventListener(this._modalSaveButtonElm, 'click', this.handleSaveClicked.bind(this)); + this.addElementEventListener(this._modalElm, 'keydown', this.handleKeyDown.bind(this)); + this.addElementEventListener(this._modalElm, 'focusout', this.validateCurrentEditor.bind(this)); + this.addElementEventListener(this._modalElm, 'blur', this.validateCurrentEditor.bind(this)); // when any of the input of the composite editor form changes, we'll add/remove a "modified" CSS className for styling purposes const onCompositeEditorChangeHandler = this.grid.onCompositeEditorChange; @@ -409,6 +422,11 @@ export class SlickCompositeEditorComponent { } } + addElementEventListener(element: Element, eventName: string, listener: EventListenerOrEventListenerObject) { + element.addEventListener(eventName, listener); + this._boundedEventWithListeners.push({ element, eventName, listener }); + } + /** Apply Mass Update Changes from a the form changes */ applySaveMassUpdateChanges(formValues: any) { const data = this.dataView.getItems(); diff --git a/packages/vanilla-bundle/src/components/slick-pagination.component.ts b/packages/vanilla-bundle/src/components/slick-pagination.component.ts index 65454885b..b8321f688 100644 --- a/packages/vanilla-bundle/src/components/slick-pagination.component.ts +++ b/packages/vanilla-bundle/src/components/slick-pagination.component.ts @@ -121,12 +121,12 @@ export class SlickPaginationComponent { } dispose() { - this.paginationService.dispose(); - this._bindingHelper.dispose(); - this._paginationElement.remove(); - // also dispose of all Subscriptions this.pubSubService.unsubscribeAll(this._subscriptions); + + this._bindingHelper.dispose(); + this.paginationService.dispose(); + this._paginationElement.remove(); } renderPagination(gridParentContainerElm: HTMLElement) { diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index 1fb1f2052..f52d07cab 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -87,7 +87,6 @@ export class SlickVanillaGridBundle { protected _eventPubSubService: EventPubSubService; private _columnDefinitions: Column[]; private _gridOptions: GridOption; - private _dataset: any[]; private _gridContainerElm: HTMLElement; private _gridParentContainerElm: HTMLElement; private _hideHeaderRowAfterPageLoad = false; @@ -156,14 +155,13 @@ export class SlickVanillaGridBundle { } get dataset(): any[] { - return this._dataset; + return this.dataView?.getItems() ?? []; } set dataset(newDataset: any[]) { - const prevDatasetLn = Array.isArray(this._dataset) ? this._dataset.length : 0; + const prevDatasetLn = this.dataView.getLength(); const isDeepCopyDataOnPageLoadEnabled = !!(this._gridOptions && this._gridOptions.enableDeepCopyDatasetOnPageLoad); const data = isDeepCopyDataOnPageLoadEnabled ? $.extend(true, [], newDataset) : newDataset; - this._dataset = data || []; - this.refreshGridData(this._dataset); + this.refreshGridData(data || []); // expand/autofit columns on first page load // we can assume that if the prevDataset was empty then we are on first load @@ -172,19 +170,19 @@ export class SlickVanillaGridBundle { } } - get datasetHierarchical(): any[] { + get datasetHierarchical(): any[] | undefined { return this.sharedService.hierarchicalDataset; } - set datasetHierarchical(hierarchicalDataset: any[]) { - this.sharedService.hierarchicalDataset = hierarchicalDataset; + set datasetHierarchical(newHierarchicalDataset: any[] | undefined) { + this.sharedService.hierarchicalDataset = newHierarchicalDataset; - if (this.filterService && this.filterService.clearFilters) { + if (newHierarchicalDataset && this.columnDefinitions && this.filterService && this.filterService.clearFilters) { this.filterService.clearFilters(); } // when a hierarchical dataset is set afterward, we can reset the flat dataset and call a tree data sort that will overwrite the flat dataset - if (this.sortService && this.sortService.processTreeDataInitialSort) { + if (newHierarchicalDataset && this.sortService && this.sortService.processTreeDataInitialSort) { this.dataView.setItems([], this._gridOptions.datasetIdPropertyName); this.sortService.processTreeDataInitialSort(); } @@ -292,7 +290,6 @@ export class SlickVanillaGridBundle { this._gridContainerElm.classList.add('slickgrid-container'); gridParentContainerElm.appendChild(this._gridContainerElm); - this._dataset = []; this._columnDefinitions = columnDefs || []; this._gridOptions = this.mergeGridOptions(options || {}); const isDeepCopyDataOnPageLoadEnabled = !!(this._gridOptions && this._gridOptions.enableDeepCopyDatasetOnPageLoad); @@ -396,10 +393,29 @@ export class SlickVanillaGridBundle { this._eventPubSubService?.unsubscribeAll(); this.dataView?.setItems([]); - this.slickGrid?.destroy(); + if (this.dataView?.destroy) { + this.dataView?.destroy(); + } + this.slickGrid?.destroy(true); + emptyElement(this._gridContainerElm); emptyElement(this._gridParentContainerElm); + if (this.backendServiceApi) { + for (const prop of Object.keys(this.backendServiceApi)) { + this.backendServiceApi[prop] = null; + } + this.backendServiceApi = undefined; + } + for (const prop of Object.keys(this.columnDefinitions)) { + this.columnDefinitions[prop] = null; + } + for (const prop of Object.keys(this.sharedService)) { + this.sharedService[prop] = null; + } + this.datasetHierarchical = undefined; + this._columnDefinitions = []; + // we could optionally also empty the content of the grid container DOM element if (shouldEmptyDomElementContainer) { this.emptyGridContainerElm(); @@ -508,7 +524,7 @@ export class SlickVanillaGridBundle { this.slickGrid.invalidate(); - if (Array.isArray(this._dataset) && this._dataset.length > 0) { + if (this.dataView.getLength() > 0) { if (!this._isDatasetInitialized && (this._gridOptions.enableCheckboxSelector || this._gridOptions.enableRowSelection)) { this.loadRowSelectionPresetWhenExists(); } @@ -677,13 +693,12 @@ export class SlickVanillaGridBundle { // internalPostProcess only works (for now) with a GraphQL Service, so make sure it is of that type if (/* backendApiService instanceof GraphqlService || */ typeof backendApiService.getDatasetName === 'function') { backendApi.internalPostProcess = (processResult: any) => { - this._dataset = []; const datasetName = (backendApi && backendApiService && typeof backendApiService.getDatasetName === 'function') ? backendApiService.getDatasetName() : ''; if (processResult && processResult.data && processResult.data[datasetName]) { - this._dataset = processResult.data[datasetName].hasOwnProperty('nodes') ? (processResult as any).data[datasetName].nodes : (processResult as any).data[datasetName]; + const data = processResult.data[datasetName].hasOwnProperty('nodes') ? (processResult as any).data[datasetName].nodes : (processResult as any).data[datasetName]; const totalCount = processResult.data[datasetName].hasOwnProperty('totalCount') ? (processResult as any).data[datasetName].totalCount : (processResult as any).data[datasetName].length; this._isDatasetProvided = true; - this.refreshGridData(this._dataset, totalCount || 0); + this.refreshGridData(data, totalCount || 0); } }; } diff --git a/packages/vanilla-bundle/src/salesforce-global-grid-options.ts b/packages/vanilla-bundle/src/salesforce-global-grid-options.ts index 07d7a3a10..476880b47 100644 --- a/packages/vanilla-bundle/src/salesforce-global-grid-options.ts +++ b/packages/vanilla-bundle/src/salesforce-global-grid-options.ts @@ -12,6 +12,7 @@ export const SalesforceGlobalGridOptions = { }, datasetIdPropertyName: 'Id', defaultFilterPlaceholder: '', + enableEmptyDataWarningMessage: false, emptyDataWarning: { class: 'slick-empty-data-warning', message: ` No data to display.`, diff --git a/packages/vanilla-bundle/src/services/__tests__/binding.service.spec.ts b/packages/vanilla-bundle/src/services/__tests__/binding.service.spec.ts index 96178f993..5864662d5 100644 --- a/packages/vanilla-bundle/src/services/__tests__/binding.service.spec.ts +++ b/packages/vanilla-bundle/src/services/__tests__/binding.service.spec.ts @@ -75,4 +75,18 @@ describe('Binding Service', () => { expect(removeEventSpy).toHaveBeenCalledWith('click', mockCallback, false); }); + + it('should call unbindAll and expect a single unbind element being called', () => { + const mockElm = { addEventListener: jest.fn(), removeEventListener: jest.fn() } as unknown as HTMLElement; + const removeEventSpy = jest.spyOn(mockElm, 'removeEventListener'); + const mockObj = { name: 'John', age: 20 }; + const elm = document.createElement('input'); + div.appendChild(elm); + + service = new BindingService({ variable: mockObj, property: 'name' }); + service.bind(mockElm, 'value', 'keyup'); + service.unbindAll(); + + expect(removeEventSpy).toHaveBeenCalledWith('keyup', expect.toBeFunction(), undefined); + }); }); diff --git a/packages/vanilla-bundle/src/services/binding.helper.ts b/packages/vanilla-bundle/src/services/binding.helper.ts index e39c7d84e..4ba479287 100644 --- a/packages/vanilla-bundle/src/services/binding.helper.ts +++ b/packages/vanilla-bundle/src/services/binding.helper.ts @@ -1,4 +1,4 @@ -import { BindingService, ElementBindingWithListener } from './binding.service'; +import { BindingService } from './binding.service'; export class BindingHelper { private _observers: BindingService[] = []; @@ -19,13 +19,9 @@ export class BindingHelper { dispose() { for (const observer of this._observers) { - const elementBindings = observer.elementBindings as Array; - for (const binding of elementBindings) { - if (binding?.event && binding?.listener) { - observer.unbind(binding.element, binding.event, binding.listener); - } - } + observer.dispose(); } + this._observers = []; } addElementBinding(variable: any, property: string, selector: string, attribute: string, events?: string | string[], callback?: (val: any) => void) { diff --git a/packages/vanilla-bundle/src/services/binding.service.ts b/packages/vanilla-bundle/src/services/binding.service.ts index a6da4e3e7..c90d5f6b3 100644 --- a/packages/vanilla-bundle/src/services/binding.service.ts +++ b/packages/vanilla-bundle/src/services/binding.service.ts @@ -24,6 +24,7 @@ export interface ElementBindingWithListener extends ElementBinding { export class BindingService { _value: any = null; _binding: Binding; + _boundedEventWithListeners: { element: Element; eventName: string; listener: EventListenerOrEventListenerObject; }[] = []; _property: string; elementBindings: Array = []; @@ -49,6 +50,12 @@ export class BindingService { return this._property; } + dispose() { + this.unbindAll(); + this._boundedEventWithListeners = []; + this.elementBindings = []; + } + valueGetter() { return this._value; } @@ -82,13 +89,22 @@ export class BindingService { return this; } - /** Unbind (remove) an event from an element */ + /** Unbind (remove) an element event listener */ unbind(element: Element | null, eventName: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions) { if (element) { element.removeEventListener(eventName, listener, options); } } + /** Unbind All (remove) bounded elements with listeners */ + unbindAll() { + for (const boundedEvent of this._boundedEventWithListeners) { + const { element, eventName, listener } = boundedEvent; + this.unbind(element, eventName, listener); + } + this._boundedEventWithListeners = []; + } + /** * Add binding to a single element by an object attribute and optionally on an event, we can do it in couple ways * 1- if there's no event provided, it will simply replace the DOM elemnt (by an attribute), for example an innerHTML @@ -114,6 +130,7 @@ export class BindingService { (binding as ElementBindingWithListener).event = eventName; (binding as ElementBindingWithListener).listener = listener; element.addEventListener(eventName, listener); + this._boundedEventWithListeners.push({ element, eventName, listener }); } this.elementBindings.push(binding); element[attribute] = typeof this._value === 'string' ? this.sanitizeText(this._value) : this._value;