angular-slickgrid
Version:
Slickgrid components made available in Angular
884 lines • 256 kB
JavaScript
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