UNPKG

angular-slickgrid

Version:

Slickgrid components made available in Angular

884 lines 256 kB
import { ApplicationRef, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Inject, Input, Optional, Output, TemplateRef, } from '@angular/core'; import { isColumnDateType, SlickDataView, SlickEventHandler, SlickGrid, } from '@slickgrid-universal/common'; import { ExtensionName, ExtensionUtility, SlickGroupItemMetadataProvider, // services BackendUtilityService, CollectionService, EventNamingStyle, ExtensionService, FilterFactory, FilterService, GridEventService, GridService, GridStateService, HeaderGroupingService, PaginationService, ResizerService, SharedService, SlickgridConfig, SortService, TreeDataService, // utilities autoAddEditorFormatterToColumnsWithEditor, emptyElement, GridStateType, unsubscribeAll, } from '@slickgrid-universal/common'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; import { SlickFooterComponent } from '@slickgrid-universal/custom-footer-component'; import { SlickPaginationComponent } from '@slickgrid-universal/pagination-component'; import { RxJsResource } from '@slickgrid-universal/rxjs-observable'; import { extend } from '@slickgrid-universal/utils'; import { TranslateService } from '@ngx-translate/core'; import { dequal } from 'dequal/lite'; import { Observable } from 'rxjs'; import { Constants } from '../constants'; import { GlobalGridOptions } from './../global-grid-options'; import { TranslaterService } from '../services/translater.service'; // Services import { AngularUtilService } from '../services/angularUtil.service'; import { SlickRowDetailView } from '../extensions/slickRowDetailView'; import { ContainerService } from '../services/container.service'; import * as i0 from "@angular/core"; import * as i1 from "../services/angularUtil.service"; import * as i2 from "../services/container.service"; import * as i3 from "@ngx-translate/core"; import * as i4 from "../services/translater.service"; import * as i5 from "@angular/common"; const WARN_NO_PREPARSE_DATE_SIZE = 10000; // data size to warn user when pre-parse isn't enabled export class AngularSlickgridComponent { angularUtilService; appRef; cd; containerService; elm; translate; translaterService; forRootConfig; _dataset; _columnDefinitions; _currentDatasetLength = 0; _darkMode = false; _eventHandler = new SlickEventHandler(); _eventPubSubService; _angularGridInstances; _hideHeaderRowAfterPageLoad = false; _isAutosizeColsCalled = false; _isGridInitialized = false; _isDatasetInitialized = false; _isDatasetHierarchicalInitialized = false; _isPaginationInitialized = false; _isLocalGrid = true; _paginationOptions; _registeredResources = []; _scrollEndCalled = false; dataView; slickGrid; groupingDefinition = {}; groupItemMetadataProvider; backendServiceApi; locales; metrics; showPagination = false; serviceList = []; totalItems = 0; paginationData; subscriptions = []; // components / plugins slickEmptyWarning; slickFooter; slickPagination; paginationComponent; slickRowDetailView; // services backendUtilityService; collectionService; extensionService; extensionUtility; filterFactory; filterService; gridEventService; gridService; gridStateService; headerGroupingService; paginationService; resizerService; rxjs; sharedService; sortService; treeDataService; customDataView; gridId = ''; gridOptions = {}; get paginationOptions() { return this._paginationOptions; } set paginationOptions(newPaginationOptions) { if (newPaginationOptions && this._paginationOptions) { this._paginationOptions = { ...this.gridOptions.pagination, ...this._paginationOptions, ...newPaginationOptions }; } else { this._paginationOptions = newPaginationOptions; } this.gridOptions.pagination = this._paginationOptions ?? this.gridOptions.pagination; this.paginationService.updateTotalItems(this.gridOptions.pagination?.totalItems ?? 0, true); } get columnDefinitions() { return this._columnDefinitions; } set columnDefinitions(columnDefinitions) { this._columnDefinitions = columnDefinitions; if (this._isGridInitialized) { this.updateColumnDefinitionsList(columnDefinitions); } if (columnDefinitions.length > 0) { this.copyColumnWidthsReference(columnDefinitions); } } // make the columnDefinitions a 2-way binding so that plugin adding cols // are synched on user's side as well (RowMove, RowDetail, RowSelections) columnDefinitionsChange = new EventEmitter(true); get dataset() { return (this.customDataView ? this.slickGrid?.getData?.() : this.dataView?.getItems()) || []; } set dataset(newDataset) { const prevDatasetLn = this._currentDatasetLength; const isDatasetEqual = dequal(newDataset, this._dataset || []); let data = newDataset; // when Tree Data is enabled and we don't yet have the hierarchical dataset filled, we can force a convert+sort of the array if (this.slickGrid && this.gridOptions?.enableTreeData && Array.isArray(newDataset) && (newDataset.length > 0 || newDataset.length !== prevDatasetLn || !isDatasetEqual)) { this._isDatasetHierarchicalInitialized = false; data = this.sortTreeDataset(newDataset, !isDatasetEqual); // if dataset changed, then force a refresh anyway } this._dataset = data; this.refreshGridData(data || []); this._currentDatasetLength = (newDataset || []).length; // expand/autofit columns on first page load // we can assume that if the prevDataset was empty then we are on first load if (this.slickGrid && this.gridOptions?.autoFitColumnsOnFirstLoad && prevDatasetLn === 0 && !this._isAutosizeColsCalled) { this.slickGrid.autosizeColumns(); this._isAutosizeColsCalled = true; } this.suggestDateParsingWhenHelpful(); } get datasetHierarchical() { return this.sharedService.hierarchicalDataset; } set datasetHierarchical(newHierarchicalDataset) { const isDatasetEqual = dequal(newHierarchicalDataset, this.sharedService?.hierarchicalDataset ?? []); const prevFlatDatasetLn = this._currentDatasetLength; this.sharedService.hierarchicalDataset = newHierarchicalDataset; if (newHierarchicalDataset && this.columnDefinitions && 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 (newHierarchicalDataset && this.slickGrid && this.sortService?.processTreeDataInitialSort) { this.sortService.processTreeDataInitialSort(); // we also need to reset/refresh the Tree Data filters because if we inserted new item(s) then it might not show up without doing this refresh // however we need to queue our process until the flat dataset is ready, so we can queue a microtask to execute the DataView refresh only after everything is ready queueMicrotask(() => { const flatDatasetLn = this.dataView.getItemCount(); if (flatDatasetLn > 0 && (flatDatasetLn !== prevFlatDatasetLn || !isDatasetEqual)) { this.filterService.refreshTreeDataFilters(); } }); this._isDatasetHierarchicalInitialized = true; } } get elementRef() { return this.elm; } get backendService() { return this.gridOptions?.backendServiceApi?.service; } get eventHandler() { return this._eventHandler; } get gridContainerElement() { return document.querySelector(`#${this.gridOptions.gridContainerId || ''}`); } /** GETTER to know if dataset was initialized or not */ get isDatasetInitialized() { return this._isDatasetInitialized; } /** SETTER to change if dataset was initialized or not (stringly used for unit testing purposes) */ set isDatasetInitialized(isInitialized) { this._isDatasetInitialized = isInitialized; } set isDatasetHierarchicalInitialized(isInitialized) { this._isDatasetHierarchicalInitialized = isInitialized; } get registeredResources() { return this._registeredResources; } slickgridHeader; slickgridFooter; constructor(angularUtilService, appRef, cd, containerService, elm, translate, translaterService, forRootConfig, externalServices) { this.angularUtilService = angularUtilService; this.appRef = appRef; this.cd = cd; this.containerService = containerService; this.elm = elm; this.translate = translate; this.translaterService = translaterService; this.forRootConfig = forRootConfig; const slickgridConfig = new SlickgridConfig(); // initialize and assign all Service Dependencies this._eventPubSubService = externalServices?.eventPubSubService ?? new EventPubSubService(this.elm.nativeElement); this._eventPubSubService.eventNamingStyle = EventNamingStyle.camelCase; this.backendUtilityService = externalServices?.backendUtilityService ?? new BackendUtilityService(); this.gridEventService = externalServices?.gridEventService ?? new GridEventService(); this.sharedService = externalServices?.sharedService ?? new SharedService(); this.collectionService = externalServices?.collectionService ?? new CollectionService(this.translaterService); // prettier-ignore this.extensionUtility = externalServices?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.backendUtilityService, this.translaterService); this.filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService); // prettier-ignore this.filterService = externalServices?.filterService ?? new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService); this.resizerService = externalServices?.resizerService ?? new ResizerService(this._eventPubSubService); // prettier-ignore this.sortService = externalServices?.sortService ?? new SortService(this.collectionService, this.sharedService, this._eventPubSubService, this.backendUtilityService); this.treeDataService = externalServices?.treeDataService ?? new TreeDataService(this._eventPubSubService, this.sharedService, this.sortService); // prettier-ignore this.paginationService = externalServices?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService); this.extensionService = externalServices?.extensionService ?? new ExtensionService(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService, this.translaterService, () => this.gridService); // prettier-ignore /* v8 ignore next 8 */ this.gridStateService = externalServices?.gridStateService ?? new GridStateService(this.extensionService, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService); // prettier-ignore /* v8 ignore next 9 */ this.gridService = externalServices?.gridService ?? new GridService(this.gridStateService, this.filterService, this._eventPubSubService, this.paginationService, this.sharedService, this.sortService, this.treeDataService); this.headerGroupingService = externalServices?.headerGroupingService ?? new HeaderGroupingService(this.extensionUtility); this.serviceList = [ this.containerService, this.extensionService, this.filterService, this.gridEventService, this.gridService, this.gridStateService, this.headerGroupingService, this.paginationService, this.resizerService, this.sortService, this.treeDataService, ]; // register all Service instances in the container this.containerService.registerInstance('ExtensionUtility', this.extensionUtility); this.containerService.registerInstance('FilterService', this.filterService); this.containerService.registerInstance('CollectionService', this.collectionService); this.containerService.registerInstance('ExtensionService', this.extensionService); this.containerService.registerInstance('GridEventService', this.gridEventService); this.containerService.registerInstance('GridService', this.gridService); this.containerService.registerInstance('GridStateService', this.gridStateService); this.containerService.registerInstance('HeaderGroupingService', this.headerGroupingService); this.containerService.registerInstance('PaginationService', this.paginationService); this.containerService.registerInstance('ResizerService', this.resizerService); this.containerService.registerInstance('SharedService', this.sharedService); this.containerService.registerInstance('SortService', this.sortService); this.containerService.registerInstance('EventPubSubService', this._eventPubSubService); this.containerService.registerInstance('PubSubService', this._eventPubSubService); this.containerService.registerInstance('TranslaterService', this.translaterService); this.containerService.registerInstance('TreeDataService', this.treeDataService); } ngAfterViewInit() { if (!this.columnDefinitions) { throw new Error('Using `<angular-slickgrid>` requires [columnDefinitions], it seems that you might have forgot to provide the missing bindable input.'); } this.initialization(this._eventHandler); this._isGridInitialized = true; // recheck the empty warning message after grid is shown so that it works in every use case if (this.gridOptions?.enableEmptyDataWarningMessage && Array.isArray(this.dataset)) { const finalTotalCount = this.dataset.length; this.displayEmptyDataWarning(finalTotalCount < 1); } // add dark mode CSS class when enabled if (this.gridOptions.darkMode) { this.setDarkMode(true); } this.suggestDateParsingWhenHelpful(); } ngOnDestroy() { this._eventPubSubService.publish('onBeforeGridDestroy', this.slickGrid); this.destroy(); this._eventPubSubService.publish('onAfterGridDestroyed', true); } destroy(shouldEmptyDomElementContainer = false) { // dispose of all Services this.serviceList.forEach((service) => { if (typeof service?.dispose === 'function') { service.dispose(); } }); this.serviceList = []; // dispose backend service when defined and a dispose method exists this.backendService?.dispose?.(); // dispose all registered external resources this.disposeExternalResources(); // dispose the Components this.slickEmptyWarning?.dispose(); this.slickFooter?.dispose(); this.slickPagination?.dispose(); if (this._eventHandler?.unsubscribeAll) { this._eventHandler.unsubscribeAll(); } this._eventPubSubService?.unsubscribeAll(); if (this.dataView) { this.dataView.setItems([]); this.dataView.destroy(); } if (this.slickGrid?.destroy) { this.slickGrid.destroy(shouldEmptyDomElementContainer); } if (this.backendServiceApi) { for (const prop of Object.keys(this.backendServiceApi)) { delete this.backendServiceApi[prop]; } this.backendServiceApi = undefined; } if (this.columnDefinitions) { for (const prop of Object.keys(this.columnDefinitions)) { this.columnDefinitions[prop] = null; } } for (const prop of Object.keys(this.sharedService)) { this.sharedService[prop] = null; } // we could optionally also empty the content of the grid container DOM element if (shouldEmptyDomElementContainer) { this.emptyGridContainerElm(); } // also unsubscribe all RxJS subscriptions this.subscriptions = unsubscribeAll(this.subscriptions); this._dataset = null; this.datasetHierarchical = undefined; this._columnDefinitions = []; this._angularGridInstances = undefined; this.slickGrid = undefined; } disposeExternalResources() { if (Array.isArray(this._registeredResources)) { while (this._registeredResources.length > 0) { const res = this._registeredResources.pop(); if (res?.dispose) { res.dispose(); } } } this._registeredResources = []; } emptyGridContainerElm() { const gridContainerId = this.gridOptions?.gridContainerId || 'grid1'; const gridContainerElm = document.querySelector(`#${gridContainerId}`); emptyElement(gridContainerElm); } /** * Define our internal Post Process callback, it will execute internally after we get back result from the Process backend call * Currently ONLY available with the GraphQL Backend Service. * The behavior is to refresh the Dataset & Pagination without requiring the user to create his own PostProcess every time */ createBackendApiInternalPostProcessCallback(gridOptions) { const backendApi = gridOptions?.backendServiceApi; if (backendApi?.service) { const backendApiService = backendApi.service; // internalPostProcess only works (for now) with a GraphQL Service, so make sure it is of that type if (typeof backendApiService.getDatasetName === 'function') { backendApi.internalPostProcess = (processResult) => { // prettier-ignore const datasetName = backendApi && backendApiService && typeof backendApiService.getDatasetName === 'function' ? backendApiService.getDatasetName() : ''; if (processResult?.data[datasetName]) { const data = 'nodes' in processResult.data[datasetName] ? processResult.data[datasetName].nodes : processResult.data[datasetName]; const totalCount = 'totalCount' in processResult.data[datasetName] ? processResult.data[datasetName].totalCount : processResult.data[datasetName].length; this.refreshGridData(data, totalCount || 0); } }; } } } initialization(eventHandler) { this.gridOptions.translater = this.translaterService; this._eventHandler = eventHandler; this._isAutosizeColsCalled = false; // when detecting a frozen grid, we'll automatically enable the mousewheel scroll handler so that we can scroll from both left/right frozen containers if (this.gridOptions && ((this.gridOptions.frozenRow !== undefined && this.gridOptions.frozenRow >= 0) || (this.gridOptions.frozenColumn !== undefined && this.gridOptions.frozenColumn >= 0)) && this.gridOptions.enableMouseWheelScrollHandler === undefined) { this.gridOptions.enableMouseWheelScrollHandler = true; } this._eventPubSubService.eventNamingStyle = this.gridOptions?.eventNamingStyle ?? EventNamingStyle.camelCase; this._eventPubSubService.publish('onBeforeGridCreate', true); // make sure the dataset is initialized (if not it will throw an error that it cannot getLength of null) this._dataset ||= []; this.gridOptions = this.mergeGridOptions(this.gridOptions); this._paginationOptions = this.gridOptions?.pagination; this.locales = this.gridOptions?.locales ?? Constants.locales; this.backendServiceApi = this.gridOptions?.backendServiceApi; this._isLocalGrid = !this.backendServiceApi; // considered a local grid if it doesn't have a backend service set // unless specified, we'll create an internal postProcess callback (currently only available for GraphQL) if (this.gridOptions.backendServiceApi && !this.gridOptions.backendServiceApi?.disableInternalPostProcess) { this.createBackendApiInternalPostProcessCallback(this.gridOptions); } if (!this.customDataView) { const dataviewInlineFilters = this.gridOptions?.dataView?.inlineFilters ?? false; let dataViewOptions = { ...this.gridOptions.dataView, inlineFilters: dataviewInlineFilters }; if (this.gridOptions.draggableGrouping || this.gridOptions.enableGrouping) { this.groupItemMetadataProvider = new SlickGroupItemMetadataProvider(); this.sharedService.groupItemMetadataProvider = this.groupItemMetadataProvider; dataViewOptions = { ...dataViewOptions, groupItemMetadataProvider: this.groupItemMetadataProvider }; } this.dataView = new SlickDataView(dataViewOptions, this._eventPubSubService); this._eventPubSubService.publish('onDataviewCreated', this.dataView); } // get any possible Services that user want to register which don't require SlickGrid to be instantiated // RxJS Resource is in this lot because it has to be registered before anything else and doesn't require SlickGrid to be initialized this.preRegisterResources(); // prepare and load all SlickGrid editors, if an async editor is found then we'll also execute it. this._columnDefinitions = this.loadSlickGridEditors(this._columnDefinitions || []); // if the user wants to automatically add a Custom Editor Formatter, we need to call the auto add function again if (this.gridOptions.autoAddCustomEditorFormatter) { autoAddEditorFormatterToColumnsWithEditor(this._columnDefinitions, this.gridOptions.autoAddCustomEditorFormatter); } // save reference for all columns before they optionally become hidden/visible this.sharedService.allColumns = this._columnDefinitions; this.sharedService.visibleColumns = this._columnDefinitions; // before certain extentions/plugins potentially adds extra columns not created by the user itself (RowMove, RowDetail, RowSelections) // we'll subscribe to the event and push back the change to the user so they always use full column defs array including extra cols this.subscriptions.push(this._eventPubSubService.subscribe('onPluginColumnsChanged', (data) => { this._columnDefinitions = data.columns; this.columnDefinitionsChange.emit(this._columnDefinitions); })); // after subscribing to potential columns changed, we are ready to create these optional extensions // when we did find some to create (RowMove, RowDetail, RowSelections), it will automatically modify column definitions (by previous subscribe) this.extensionService.createExtensionsBeforeGridCreation(this._columnDefinitions, this.gridOptions); // if user entered some Pinning/Frozen "presets", we need to apply them in the grid options if (this.gridOptions.presets?.pinning) { this.gridOptions = { ...this.gridOptions, ...this.gridOptions.presets.pinning }; } // build SlickGrid Grid, also user might optionally pass a custom dataview (e.g. remote model) this.slickGrid = new SlickGrid(`#${this.gridId}`, this.customDataView || this.dataView, this._columnDefinitions, this.gridOptions, this._eventPubSubService); this.sharedService.dataView = this.dataView; this.sharedService.slickGrid = this.slickGrid; this.sharedService.gridContainerElement = this.elm.nativeElement; if (this.groupItemMetadataProvider) { this.slickGrid.registerPlugin(this.groupItemMetadataProvider); // register GroupItemMetadataProvider when Grouping is enabled } this.extensionService.bindDifferentExtensions(); this.bindDifferentHooks(this.slickGrid, this.gridOptions, this.dataView); // when it's a frozen grid, we need to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward const frozenColumnIndex = this.gridOptions.frozenColumn !== undefined ? this.gridOptions.frozenColumn : -1; if (frozenColumnIndex >= 0 && frozenColumnIndex <= this._columnDefinitions.length) { this.sharedService.frozenVisibleColumnId = this._columnDefinitions[frozenColumnIndex].id || ''; } // get any possible Services that user want to register this.registerResources(); // initialize the SlickGrid grid this.slickGrid.init(); // initialized the resizer service only after SlickGrid is initialized // if we don't we end up binding our resize to a grid element that doesn't yet exist in the DOM and the resizer service will fail silently (because it has a try/catch that unbinds the resize without throwing back) if (this.gridContainerElement) { this.resizerService.init(this.slickGrid, this.gridContainerElement); } // user could show a custom footer with the data metrics (dataset length and last updated timestamp) if (!this.gridOptions.enablePagination && this.gridOptions.showCustomFooter && this.gridOptions.customFooterOptions && this.gridContainerElement) { this.slickFooter = new SlickFooterComponent(this.slickGrid, this.gridOptions.customFooterOptions, this._eventPubSubService, this.translaterService); this.slickFooter.renderFooter(this.gridContainerElement); } if (!this.customDataView && this.dataView) { // load the data in the DataView (unless it's a hierarchical dataset, if so it will be loaded after the initial tree sort) const initialDataset = this.gridOptions?.enableTreeData ? this.sortTreeDataset(this._dataset) : this._dataset; this.dataView.beginUpdate(); this.dataView.setItems(initialDataset || [], this.gridOptions.datasetIdPropertyName ?? 'id'); this.dataView.endUpdate(); // if you don't want the items that are not visible (due to being filtered out or being on a different page) // to stay selected, pass 'false' to the second arg if (this.slickGrid?.getSelectionModel() && this.gridOptions?.dataView && 'syncGridSelection' in this.gridOptions.dataView) { // if we are using a Backend Service, we will do an extra flag check, the reason is because it might have some unintended behaviors // with the BackendServiceApi because technically the data in the page changes the DataView on every page change. let preservedRowSelectionWithBackend = false; if (this.gridOptions.backendServiceApi && 'syncGridSelectionWithBackendService' in this.gridOptions.dataView) { preservedRowSelectionWithBackend = this.gridOptions.dataView.syncGridSelectionWithBackendService; } const syncGridSelection = this.gridOptions.dataView.syncGridSelection; if (typeof syncGridSelection === 'boolean') { let preservedRowSelection = syncGridSelection; if (!this._isLocalGrid) { // when using BackendServiceApi, we'll be using the "syncGridSelectionWithBackendService" flag BUT "syncGridSelection" must also be set to True preservedRowSelection = syncGridSelection && preservedRowSelectionWithBackend; } this.dataView.syncGridSelection(this.slickGrid, preservedRowSelection); } else if (typeof syncGridSelection === 'object') { this.dataView.syncGridSelection(this.slickGrid, syncGridSelection.preserveHidden, syncGridSelection.preserveHiddenOnSelectionChange); } } const datasetLn = this.dataView.getLength() || this._dataset?.length || 0; if (datasetLn > 0) { if (!this._isDatasetInitialized && (this.gridOptions.enableCheckboxSelector || this.gridOptions.enableRowSelection)) { this.loadRowSelectionPresetWhenExists(); } this.loadFilterPresetsWhenDatasetInitialized(); this._isDatasetInitialized = true; } } // user might want to hide the header row on page load but still have `enableFiltering: true` // if that is the case, we need to hide the headerRow ONLY AFTER all filters got created & dataView exist if (this._hideHeaderRowAfterPageLoad) { this.showHeaderRow(false); this.sharedService.hideHeaderRowAfterPageLoad = this._hideHeaderRowAfterPageLoad; } // publish & dispatch certain events this._eventPubSubService.publish('onGridCreated', this.slickGrid); // after the DataView is created & updated execute some processes if (!this.customDataView) { this.executeAfterDataviewCreated(this.slickGrid, this.gridOptions); } // bind resize ONLY after the dataView is ready this.bindResizeHook(this.slickGrid, this.gridOptions); // bind the Backend Service API callback functions only after the grid is initialized // because the preProcess() and onInit() might get triggered if (this.gridOptions?.backendServiceApi) { this.bindBackendCallbackFunctions(this.gridOptions); } // local grid, check if we need to show the Pagination // if so then also check if there's any presets and finally initialize the PaginationService // a local grid with Pagination presets will potentially have a different total of items, we'll need to get it from the DataView and update our total if (this.gridOptions?.enablePagination && this._isLocalGrid) { this.showPagination = true; this.loadLocalGridPagination(this.dataset); } this._angularGridInstances = { // Slick Grid & DataView objects dataView: this.dataView, slickGrid: this.slickGrid, extensions: this.extensionService?.extensionList, // public methods destroy: this.destroy.bind(this), // return all available Services (non-singleton) backendService: this.backendService, eventPubSubService: this._eventPubSubService, filterService: this.filterService, gridEventService: this.gridEventService, gridStateService: this.gridStateService, gridService: this.gridService, groupingService: this.headerGroupingService, headerGroupingService: this.headerGroupingService, extensionService: this.extensionService, paginationComponent: this.slickPagination, paginationService: this.paginationService, resizerService: this.resizerService, sortService: this.sortService, treeDataService: this.treeDataService, }; // all instances (SlickGrid, DataView & all Services) this._eventPubSubService.publish('onAngularGridCreated', this._angularGridInstances); } /** * On a Pagination changed, we will trigger a Grid State changed with the new pagination info * Also if we use Row Selection or the Checkbox Selector with a Backend Service (Odata, GraphQL), we need to reset any selection */ paginationChanged(pagination) { const isSyncGridSelectionEnabled = this.gridStateService?.needToPreserveRowSelection() ?? false; if (this.slickGrid && !isSyncGridSelectionEnabled && this.gridOptions?.backendServiceApi && (this.gridOptions.enableRowSelection || this.gridOptions.enableCheckboxSelector)) { this.slickGrid.setSelectedRows([]); } const { pageNumber, pageSize } = pagination; if (this.sharedService) { if (pageSize !== undefined && pageNumber !== undefined) { this.sharedService.currentPagination = { pageNumber, pageSize }; } } this._eventPubSubService.publish('onGridStateChanged', { change: { newValues: { pageNumber, pageSize }, type: GridStateType.pagination }, gridState: this.gridStateService.getCurrentGridState(), }); this.cd.markForCheck(); } /** * When dataset changes, we need to refresh the entire grid UI & possibly resize it as well * @param dataset */ refreshGridData(dataset, totalCount) { if (this.gridOptions?.enableEmptyDataWarningMessage && Array.isArray(dataset)) { const finalTotalCount = totalCount || dataset.length; this.displayEmptyDataWarning(finalTotalCount < 1); } if (Array.isArray(dataset) && this.slickGrid && this.dataView?.setItems) { this.dataView.setItems(dataset, this.gridOptions.datasetIdPropertyName ?? 'id'); if (!this.gridOptions.backendServiceApi && !this.gridOptions.enableTreeData) { this.dataView.reSort(); } if (dataset.length > 0) { if (!this._isDatasetInitialized) { this.loadFilterPresetsWhenDatasetInitialized(); if (this.gridOptions.enableCheckboxSelector) { this.loadRowSelectionPresetWhenExists(); } } this._isDatasetInitialized = true; } if (dataset) { this.slickGrid.invalidate(); } // display the Pagination component only after calling this refresh data first, we call it here so that if we preset pagination page number it will be shown correctly this.showPagination = !!(this.gridOptions && (this.gridOptions.enablePagination || (this.gridOptions.backendServiceApi && this.gridOptions.enablePagination === undefined))); if (this._paginationOptions && this.gridOptions?.pagination && this.gridOptions?.backendServiceApi) { const paginationOptions = this.setPaginationOptionsWhenPresetDefined(this.gridOptions, this._paginationOptions); // when we have a totalCount use it, else we'll take it from the pagination object // only update the total items if it's different to avoid refreshing the UI const totalRecords = totalCount !== undefined ? totalCount : this.gridOptions?.pagination?.totalItems; if (totalRecords !== undefined && totalRecords !== this.totalItems) { this.totalItems = +totalRecords; } // initialize the Pagination Service with new pagination options (which might have presets) if (!this._isPaginationInitialized) { this.initializePaginationService(paginationOptions); } else { // update the pagination service with the new total this.paginationService.updateTotalItems(this.totalItems); } } // resize the grid inside a slight timeout, in case other DOM element changed prior to the resize (like a filter/pagination changed) if (this.slickGrid && this.gridOptions.enableAutoResize) { const delay = this.gridOptions.autoResize && this.gridOptions.autoResize.delay; this.resizerService.resizeGrid(delay || 10); } } } setData(data, shouldAutosizeColumns = false) { if (shouldAutosizeColumns) { this._isAutosizeColsCalled = false; this._currentDatasetLength = 0; } this.dataset = data || []; } /** * Check if there's any Pagination Presets defined in the Grid Options, * if there are then load them in the paginationOptions object */ setPaginationOptionsWhenPresetDefined(gridOptions, paginationOptions) { if (gridOptions.presets?.pagination && paginationOptions && !this._isPaginationInitialized) { if (this.hasBackendInfiniteScroll()) { console.warn('[Angular-Slickgrid] `presets.pagination` is not supported with Infinite Scroll, reverting to first page.'); } else { paginationOptions.pageSize = gridOptions.presets.pagination.pageSize; paginationOptions.pageNumber = gridOptions.presets.pagination.pageNumber; } } return paginationOptions; } setDarkMode(dark = false) { if (dark) { this.sharedService.gridContainerElement?.classList.add('slick-dark-mode'); } else { this.sharedService.gridContainerElement?.classList.remove('slick-dark-mode'); } } /** * Dynamically change or update the column definitions list. * We will re-render the grid so that the new header and data shows up correctly. * If using i18n, we also need to trigger a re-translate of the column headers */ updateColumnDefinitionsList(newColumnDefinitions) { // map the Editor model to editorClass and load editor collectionAsync newColumnDefinitions = this.loadSlickGridEditors(newColumnDefinitions); if (this.gridOptions.enableTranslate) { this.extensionService.translateColumnHeaders(undefined, newColumnDefinitions); } else { this.extensionService.renderColumnHeaders(newColumnDefinitions, true); } if (this.gridOptions?.enableAutoSizeColumns) { this.slickGrid.autosizeColumns(); } else if (this.gridOptions?.enableAutoResizeColumnsByCellContent && this.resizerService?.resizeColumnsByCellContent) { this.resizerService.resizeColumnsByCellContent(); } } /** * Show the filter row displayed on first row, we can optionally pass false to hide it. * @param showing */ showHeaderRow(showing = true) { this.slickGrid.setHeaderRowVisibility(showing); if (showing === true && this._isGridInitialized) { this.slickGrid.setColumns(this.columnDefinitions); } return showing; } /** * Toggle the empty data warning message visibility. * @param showWarning */ displayEmptyDataWarning(showWarning = true) { this.slickEmptyWarning?.showEmptyDataMessage(showWarning); } // // protected functions // ------------------ /** * Loop through all column definitions and copy the original optional `width` properties optionally provided by the user. * We will use this when doing a resize by cell content, if user provided a `width` it won't override it. */ copyColumnWidthsReference(columnDefinitions) { columnDefinitions.forEach((col) => (col.originalWidth = col.width)); } bindDifferentHooks(grid, gridOptions, dataView) { // on locale change, we have to manually translate the Headers, GridMenu if (this.translate?.onLangChange) { // translate some of them on first load, then on each language change if (gridOptions.enableTranslate) { this.extensionService.translateAllExtensions(); } this.subscriptions.push(this.translate.onLangChange.subscribe(({ lang }) => { // publish event of the same name that Slickgrid-Universal uses on a language change event this._eventPubSubService.publish('onLanguageChange'); if (gridOptions.enableTranslate) { this.extensionService.translateAllExtensions(lang); if ((gridOptions.createPreHeaderPanel && gridOptions.createTopHeaderPanel) || (gridOptions.createPreHeaderPanel && !gridOptions.enableDraggableGrouping)) { this.headerGroupingService.translateHeaderGrouping(); } } })); } // if user set an onInit Backend, we'll run it right away (and if so, we also need to run preProcess, internalPostProcess & postProcess) if (gridOptions.backendServiceApi) { const backendApi = gridOptions.backendServiceApi; if (backendApi?.service?.init) { backendApi.service.init(backendApi.options, gridOptions.pagination, this.slickGrid, this.sharedService); } } if (dataView && grid) { // on cell click, mainly used with the columnDef.action callback this.gridEventService.bindOnCellChange(grid); this.gridEventService.bindOnClick(grid); // bind external sorting (backend) when available or default onSort (dataView) if (gridOptions.enableSorting) { // bind external sorting (backend) unless specified to use the local one if (gridOptions.backendServiceApi && !gridOptions.backendServiceApi.useLocalSorting) { this.sortService.bindBackendOnSort(grid); } else { this.sortService.bindLocalOnSort(grid); } } // bind external filter (backend) when available or default onFilter (dataView) if (gridOptions.enableFiltering) { this.filterService.init(grid); // bind external filter (backend) unless specified to use the local one if (gridOptions.backendServiceApi && !gridOptions.backendServiceApi.useLocalFiltering) { this.filterService.bindBackendOnFilter(grid); } else { this.filterService.bindLocalOnFilter(grid); } } // when column are reordered, we need to update the visibleColumn array this._eventHandler.subscribe(grid.onColumnsReordered, (_e, args) => { this.sharedService.hasColumnsReordered = true; this.sharedService.visibleColumns = args.impactedColumns; }); this._eventHandler.subscribe(grid.onSetOptions, (_e, args) => { // add/remove dark mode CSS class when enabled if (args.optionsBefore.darkMode !== args.optionsAfter.darkMode && this.gridContainerElement) { this.setDarkMode(args.optionsAfter.darkMode); } }); // load any presets if any (after dataset is initialized) this.loadColumnPresetsWhenDatasetInitialized(); this.loadFilterPresetsWhenDatasetInitialized(); // When data changes in the DataView, we need to refresh the metrics and/or display a warning if the dataset is empty this._eventHandler.subscribe(dataView.onRowCountChanged, (_e, args) => { if (!gridOptions.enableRowDetailView || !Array.isArray(args.changedRows) || args.changedRows.length === args.itemCount) { grid.invalidate(); } else { grid.invalidateRows(args.changedRows); grid.render(); } this.handleOnItemCountChanged(dataView.getFilteredItemCount() || 0, dataView.getItemCount() || 0); }); this._eventHandler.subscribe(dataView.onSetItemsCalled, (_e, args) => { this.sharedService.isItemsDateParsed = false; this.handleOnItemCountChanged(dataView.getFilteredItemCount() || 0, args.itemCount); // when user has resize by content enabled, we'll force a full width calculation since we change our entire dataset if (args.itemCount > 0 && (this.gridOptions.autosizeColumnsByCellContentOnFirstLoad || this.gridOptions.enableAutoResizeColumnsByCellContent)) { this.resizerService.resizeColumnsByCellContent(!this.gridOptions?.resizeByContentOnlyOnFirstLoad); } }); if (gridOptions?.enableFiltering && !gridOptions.enableRowDetailView) { this._eventHandler.subscribe(dataView.onRowsChanged, (_e, { calledOnRowCountChanged, rows }) => { // filtering data with local dataset will not always show correctly unless we call this updateRow/render // also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row // see commit: https://github.com/ghiscoding/aurelia-slickgrid/commit/8c503a4d45fba11cbd8d8cc467fae8d177cc4f60 if (!calledOnRowCountChanged && Array.isArray(rows)) { const ranges = grid.getRenderedRange(); rows.filter((row) => row >= ranges.top && row <= ranges.bottom).forEach((row) => grid.updateRow(row)); grid.render(); } }); } } // @deprecated @user `dataview.globalItemMetadataProvider.getRowMetadata` // did the user add a colspan callback? If so, hook it into the DataView getItemMetadata if (gridOptions?.colspanCallback && dataView && dataView.getItem && dataView.getItemMetadata) { dataView.getItemMetadata = (rowNumber) => { let callbackResult = null; if (gridOptions.colspanCallback) { callbackResult = gridOptions.colspanCallback(dataView.getItem(rowNumber)); } return callbackResult; }; } } bindBackendCallbackFunctions(gridOptions) { const backendApi = gridOptions.backendServiceApi; const backendApiService = backendApi?.service; const serviceOptions = backendApiService?.options ?? {}; // prettier-ignore const isExecuteCommandOnInit = (!serviceOptions) ? false : ((serviceOptions && 'executeProcessCommandOnInit' in serviceOptions) ? serviceOptions['executeProcessCommandOnInit'] : true); if (backendApiService) { // update backend filters (if need be) BEFORE the query runs (via the onInit command a few lines below) // if user entered some any "presets", we need to reflect them all in the grid if (gridOptions?.presets) { // Filters "presets" if (backendApiService.updateFilters && Array.isArray(gridOptions.presets.filters) && gridOptions.presets.filters.length > 0) { backendApiService.updateFilters(gridOptions.presets.filters, true); } // Sorters "presets" if (backendApiService.updateSorters && Array.isArray(gridOptions.presets.sorters) && gridOptions.presets.sorters.length > 0) { // when using multi-column sort, we can have multiple but on single sort then only grab the first sort provided const sortColumns = this.gridOptions.multiColumnSort ? gridOptions.presets.sorters : gridOptions.presets.sorters.slice(0, 1); backendApiService.updateSorters(undefined, sortColumns); } // Pagination "presets" if (backendApiService.updatePagination && gridOptions.presets.pagination && !this.hasBackendInfiniteScroll()) { const { pageNumber, pageSize } = gridOptions.presets.pagination; backendApiService.updatePagination(pageNumber, pageSize); } } else { const columnFilters = this.filterService.getColumnFilters(); if (columnFilters && backendApiService.updateFilters) { backendApiService.updateFilters(columnFilters, false); } } // execute onInit command when necessary if (backendApi && backendApiService && (backendApi.onInit || isExecuteCommandOnInit)) { const query = typeof backendApiService.buildQuery === 'function' ? backendApiService.buildQuery() : ''; // prettier-ignore const process = (isExecuteCommandOnInit) ? (backendApi.process && backendApi.process(query) || null) : (backendApi.onInit && backendApi.onInit(query) || null); // wrap this inside a microtask to be executed at the end of the task and avoid timing issue since the gridOptions needs to be ready before running this onInit queueMicrotask(() => { const backendUtilityService = this.backendUtilityService; // keep start time & end timestamps & return i