slickgrid-react
Version:
Slickgrid components made available in React
864 lines • 81.2 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { autoAddEditorFormatterToColumnsWithEditor, BackendUtilityService, collectionObserver, CollectionService, emptyElement, EventNamingStyle, ExtensionService, ExtensionUtility, FilterFactory, FilterService, GridEventService, GridService, GridStateService, HeaderGroupingService, isColumnDateType, PaginationService, ResizerService, SharedService, SlickDataView, SlickEventHandler, SlickGrid, SlickgridConfig, SlickGroupItemMetadataProvider, SortService, TreeDataService, } from '@slickgrid-universal/common';
import { SlickFooterComponent } from '@slickgrid-universal/custom-footer-component';
import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component';
import { EventPubSubService } from '@slickgrid-universal/event-pub-sub';
import { SlickPaginationComponent } from '@slickgrid-universal/pagination-component';
import { deepCopy, extend } from '@slickgrid-universal/utils';
import { dequal } from 'dequal/lite';
import React from 'react';
import { I18nextContext } from '../contexts/i18nextContext.js';
import { GlobalGridOptions } from '../global-grid-options.js';
import { loadReactComponentDynamically } from '../services/reactUtils.js';
import { GlobalContainerService } from '../services/singletons.js';
import { TranslaterI18NextService } from '../services/translaterI18Next.service.js';
import { disposeAllSubscriptions } from '../services/utilities.js';
const WARN_NO_PREPARSE_DATE_SIZE = 10000; // data size to warn user when pre-parse isn't enabled
export class SlickgridReact extends React.Component {
props;
// i18next has to be provided by the external user through our `I18nextProvider`
static contextType = I18nextContext;
_mounted = false;
setStateValue(key, value, callback) {
if (this.state?.[key] === value) {
return;
}
if (!this._mounted) {
this.state = this.state || {};
this.state[key] = value;
return;
}
this.setState(() => {
const result = {};
result[key] = value;
return result;
}, callback);
}
_columns = [];
_currentDatasetLength = 0;
_dataset = null;
_options = {};
_elm;
_collectionObservers = [];
_eventHandler;
_eventPubSubService;
_hideHeaderRowAfterPageLoad = false;
_i18next;
_isAutosizeColsCalled = false;
_isGridInitialized = false;
_isDatasetInitialized = false;
_isDatasetHierarchicalInitialized = false;
_isPaginationInitialized = false;
_isLocalGrid = true;
_paginationOptions;
_registeredResources = [];
_scrollEndCalled = false;
get options() {
return this._options || {};
}
set options(options) {
let mergedOptions;
// if we already have grid options, when grid was already initialized, we'll merge with those options
// else we'll merge with global grid options
if (this.grid?.getOptions) {
mergedOptions = extend(true, {}, this.grid.getOptions(), options);
}
else {
mergedOptions = this.mergeGridOptions(options);
}
if (this.sharedService?.gridOptions && this.grid?.setOptions) {
this.sharedService.gridOptions = mergedOptions;
this.grid.setOptions(mergedOptions, false, true); // make sure to supressColumnCheck (3rd arg) to avoid problem with changeColumnsArrangement() and custom grid view
this.grid.reRenderColumns(true); // then call a re-render since we did supressColumnCheck on previous setOptions
}
this._options = mergedOptions;
}
groupItemMetadataProvider;
backendServiceApi;
metrics;
showPagination = false;
serviceList = [];
subscriptions = [];
// components
slickEmptyWarning;
slickFooter;
slickPagination;
// services
backendUtilityService;
collectionService;
extensionService;
extensionUtility;
filterFactory;
filterService;
gridEventService;
gridService;
gridStateService;
groupingService;
headerGroupingService;
resizerService;
rxjs;
sharedService;
sortService;
treeDataService;
dataView;
grid;
totalItems = 0;
extensions;
instances = null;
static defaultProps = {
containerService: GlobalContainerService,
translaterService: new TranslaterI18NextService(),
gridId: '',
dataset: [],
columns: [],
options: {},
};
get dataset() {
return this.dataView?.getItems() || [];
}
set dataset(newDataset) {
const prevDatasetLn = this._currentDatasetLength;
const isDatasetEqual = dequal(newDataset, this.dataset || []);
let data = this._options?.enableDeepCopyDatasetOnPageLoad ? deepCopy(newDataset) : 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.grid &&
this.options?.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.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.grid && this.options.autoFitColumnsOnFirstLoad && prevDatasetLn === 0 && !this._isAutosizeColsCalled) {
this.grid.autosizeColumns();
this._isAutosizeColsCalled = true;
}
}
get datasetHierarchical() {
return this.sharedService.hierarchicalDataset;
}
set datasetHierarchical(newHierarchicalDataset) {
const isDatasetEqual = dequal(newHierarchicalDataset, this.sharedService?.hierarchicalDataset ?? []);
const prevFlatDatasetLn = this._currentDatasetLength;
if (this.sharedService) {
this.sharedService.hierarchicalDataset = newHierarchicalDataset;
}
if (newHierarchicalDataset && this.props.columns && 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.dataView && newHierarchicalDataset && this.grid && this.sortService?.processTreeDataInitialSort) {
this.dataView.setItems([], this._options?.datasetIdPropertyName ?? 'id');
this.sortService.processTreeDataInitialSort();
this.treeDataService.initHierarchicalTree();
// 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() ?? 0;
if (flatDatasetLn > 0 && (flatDatasetLn !== prevFlatDatasetLn || !isDatasetEqual)) {
this.filterService.refreshTreeDataFilters();
}
});
}
this._isDatasetHierarchicalInitialized = true;
}
get paginationService() {
return this.state?.paginationService;
}
set paginationService(value) {
this.setStateValue('paginationService', value);
}
constructor(props) {
super(props);
this.props = props;
const slickgridConfig = new SlickgridConfig();
this._eventHandler = new SlickEventHandler();
this.showPagination = false;
// check if the user wants to hide the header row from the start
// we only want to do this check once in the constructor
this._hideHeaderRowAfterPageLoad = props.options?.showHeaderRow === false;
this._options = this.mergeGridOptions(props.options || {});
// initialize and assign all Service Dependencies
this._eventPubSubService = new EventPubSubService();
this._eventPubSubService.eventNamingStyle = EventNamingStyle.camelCase;
this.backendUtilityService = new BackendUtilityService();
this.gridEventService = new GridEventService();
this.sharedService = new SharedService();
this.collectionService = new CollectionService(this.props.translaterService);
this.extensionUtility = new ExtensionUtility(this.sharedService, this.backendUtilityService, this.props.translaterService);
this.filterFactory = new FilterFactory(slickgridConfig, this.props.translaterService, this.collectionService);
this.filterService = new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService);
this.resizerService = new ResizerService(this._eventPubSubService);
this.sortService = new SortService(this.collectionService, this.sharedService, this._eventPubSubService, this.backendUtilityService);
this.treeDataService = new TreeDataService(this._eventPubSubService, this.filterService, this.sharedService, this.sortService);
this.paginationService = new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService);
this.extensionService = new ExtensionService(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService, this.props.translaterService, () => this.gridService);
this.gridStateService = new GridStateService(this.extensionService, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService);
this.gridService = new GridService(this.gridStateService, this.filterService, this._eventPubSubService, this.paginationService, this.sharedService, this.sortService, this.treeDataService);
this.headerGroupingService = new HeaderGroupingService(this.extensionUtility);
this.serviceList = [
this.extensionService,
this.filterService,
this.gridEventService,
this.gridService,
this.gridStateService,
this.headerGroupingService,
this.paginationService,
this.resizerService,
this.sortService,
this.treeDataService,
];
if (this.props.datasetHierarchical) {
this.sharedService.hierarchicalDataset = this.props.datasetHierarchical || [];
}
// register all Service instances in the container
this.props.containerService.registerInstance('PubSubService', this._eventPubSubService);
this.props.containerService.registerInstance('EventPubSubService', this._eventPubSubService);
this.props.containerService.registerInstance('ExtensionUtility', this.extensionUtility);
this.props.containerService.registerInstance('FilterService', this.filterService);
this.props.containerService.registerInstance('CollectionService', this.collectionService);
this.props.containerService.registerInstance('ExtensionService', this.extensionService);
this.props.containerService.registerInstance('GridEventService', this.gridEventService);
this.props.containerService.registerInstance('GridService', this.gridService);
this.props.containerService.registerInstance('GridStateService', this.gridStateService);
this.props.containerService.registerInstance('HeaderGroupingService', this.headerGroupingService);
this.props.containerService.registerInstance('PaginationService', this.paginationService);
this.props.containerService.registerInstance('ResizerService', this.resizerService);
this.props.containerService.registerInstance('SharedService', this.sharedService);
this.props.containerService.registerInstance('SortService', this.sortService);
this.props.containerService.registerInstance('TranslaterService', this.props.translaterService);
this.props.containerService.registerInstance('TreeDataService', this.treeDataService);
}
get backendService() {
return this.options.backendServiceApi?.service;
}
get eventHandler() {
return this._eventHandler;
}
get isDatasetInitialized() {
return this._isDatasetInitialized;
}
set isDatasetInitialized(isInitialized) {
this._isDatasetInitialized = isInitialized;
}
set isDatasetHierarchicalInitialized(isInitialized) {
this._isDatasetHierarchicalInitialized = isInitialized;
}
get registeredResources() {
return this._registeredResources;
}
componentDidMount() {
this._mounted = true;
if (this._elm && this._eventPubSubService instanceof EventPubSubService) {
this._eventPubSubService.elementSource = this._elm;
// React doesn't play well with Custom Events & also the render is called after the constructor which brings a second problem
// to fix both issues, we need to do the following:
// loop through all component props and subscribe to the ones that startsWith "on", we'll assume that it's the custom events
// we'll then call the assigned listener(s) when events are dispatching
for (const prop in this.props) {
if (prop.startsWith('on')) {
const eventCallback = this.props[prop];
if (typeof eventCallback === 'function') {
this.subscriptions.push(this._eventPubSubService.subscribe(prop, (data) => {
const gridEventName = this._eventPubSubService.getEventNameByNamingConvention(prop, '');
eventCallback.call(null, new CustomEvent(gridEventName, { detail: data }));
}));
}
}
}
}
// save resource refs to register before the grid options are merged and possibly deep copied
// since a deep copy of grid options would lose original resource refs but we want to keep them as singleton
this._registeredResources = this.options?.externalResources || [];
this.initialization(this._eventHandler);
this._isGridInitialized = true;
// if we have a backendServiceApi and the enablePagination is undefined, we'll assume that we do want to see it, else get that defined value
if (!this.hasBackendInfiniteScroll()) {
this.options.enablePagination = !!(this.options.backendServiceApi && this.options.enablePagination === undefined
? true
: this.options.enablePagination);
}
if (!this._isPaginationInitialized && !this.props.datasetHierarchical && this._options?.enablePagination && this._isLocalGrid) {
this.showPagination = true;
this.loadLocalGridPagination(this.dataset);
}
// recheck the empty warning message after grid is shown so that it works in every use case
if (this._options?.enableEmptyDataWarningMessage) {
const dataset = this.props.dataset || [];
if (Array.isArray(dataset)) {
const finalTotalCount = dataset.length;
this.displayEmptyDataWarning(finalTotalCount < 1);
}
}
// add dark mode CSS class when enabled
if (this.options.darkMode) {
this.setDarkMode(true);
}
this.suggestDateParsingWhenHelpful();
}
initialization(eventHandler) {
if (!this._columns) {
throw new Error('Using `<SlickgridReact>` requires `columns` and it seems that you might have forgot to provide this missing bindable model.');
}
this._options.translater = this.props.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._options &&
((this._options.frozenRow !== undefined && this._options.frozenRow >= 0) ||
(this._options.frozenColumn !== undefined && this._options.frozenColumn >= 0)) &&
this._options.enableMouseWheelScrollHandler === undefined) {
this._options.enableMouseWheelScrollHandler = true;
}
this._eventPubSubService.eventNamingStyle = this._options?.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.props.dataset || [];
this._currentDatasetLength = this._dataset.length;
this._options = this.mergeGridOptions(this._options);
this._paginationOptions = this._options?.pagination;
this.backendServiceApi = this._options?.backendServiceApi;
this._isLocalGrid = !this.backendServiceApi; // considered a local grid if it doesn't have a backend service set
// inject the I18Next instance when translation is enabled
if (this._options?.enableTranslate || this._options?.i18n) {
const importErrorMsg = '[Slickgrid-React] Enabling translation requires you to install I18Next in your App and use `I18nextProvider` to provide it. ' +
'Please make sure to first install it via "npm install i18next react-i18next" and then ' +
'use `<I18nextProvider value={i18n}><App/></I18nextProvider>` in your main index.tsx file. ' +
'Visit https://ghiscoding.gitbook.io/slickgrid-react/localization/localization for more info.';
this._i18next = this.context; // Access the context directly
if (this.props.translaterService && this._i18next) {
this.props.translaterService.i18nInstance = this._i18next;
}
else {
throw new Error(importErrorMsg);
}
}
// unless specified, we'll create an internal postProcess callback (currently only available for GraphQL)
if (this.options.backendServiceApi && !this.options.backendServiceApi?.disableInternalPostProcess) {
this.createBackendApiInternalPostProcessCallback(this._options);
}
if (!this.props.customDataView) {
const dataviewInlineFilters = (this._options.dataView && this._options.dataView.inlineFilters) || false;
let dataViewOptions = { ...this._options.dataView, inlineFilters: dataviewInlineFilters };
if (this._options.draggableGrouping || this._options.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._columns = this.loadSlickGridEditors(this.props.columns);
// if the user wants to automatically add a Custom Editor Formatter, we need to call the auto add function again
if (this._options.autoAddCustomEditorFormatter) {
autoAddEditorFormatterToColumnsWithEditor(this._columns, this._options.autoAddCustomEditorFormatter);
}
// save reference for all columns before they optionally become hidden/visible
this.sharedService.allColumns = this._columns;
this.sharedService.visibleColumns = this._columns;
// 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._columns, this._options);
// if user entered some Pinning/Frozen "presets", we need to apply them in the grid options
if (this.options.presets?.pinning) {
this.options = { ...this.options, ...this.options.presets.pinning };
}
// build SlickGrid Grid, also user might optionally pass a custom dataview (e.g. remote model)
this.grid = new SlickGrid(`#${this.props.gridId}`, this.props.customDataView || this.dataView, this._columns, this._options, this._eventPubSubService);
this.sharedService.dataView = this.dataView;
this.sharedService.slickGrid = this.grid;
this.sharedService.gridContainerElement = this._elm;
if (this.groupItemMetadataProvider) {
this.grid.registerPlugin(this.groupItemMetadataProvider); // register GroupItemMetadataProvider when Grouping is enabled
}
this.extensionService.bindDifferentExtensions();
this.bindDifferentHooks(this.grid, this._options, 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
this.sharedService.frozenVisibleColumnId = this.grid.getFrozenColumnId();
// get any possible Services that user want to register
this.registerResources();
// initialize the SlickGrid grid
this.grid.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)
const gridContainerElm = this._elm;
if (gridContainerElm) {
this.resizerService.init(this.grid, gridContainerElm);
}
// user could show a custom footer with the data metrics (dataset length and last updated timestamp)
if (!this._options.enablePagination && this._options.showCustomFooter && this._options.customFooterOptions && gridContainerElm) {
this.slickFooter = new SlickFooterComponent(this.grid, this._options.customFooterOptions, this._eventPubSubService, this.props.translaterService);
this.slickFooter.renderFooter(gridContainerElm);
}
if (!this.props.customDataView && this.dataView) {
const initialDataset = this._options?.enableTreeData ? this.sortTreeDataset(this.props.dataset) : this.props.dataset;
if (Array.isArray(initialDataset)) {
this.dataView.setItems(initialDataset, this._options.datasetIdPropertyName ?? 'id');
}
// 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.grid?.getSelectionModel() && this._options?.dataView && this._options.dataView.hasOwnProperty('syncGridSelection')) {
// 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._options.backendServiceApi && this._options.dataView.hasOwnProperty('syncGridSelectionWithBackendService')) {
preservedRowSelectionWithBackend = this._options.dataView.syncGridSelectionWithBackendService;
}
const syncGridSelection = this._options.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.grid, preservedRowSelection);
}
else if (typeof syncGridSelection === 'object') {
this.dataView.syncGridSelection(this.grid, syncGridSelection.preserveHidden, syncGridSelection.preserveHiddenOnSelectionChange);
}
}
if (this._dataset.length > 0) {
if (!this._isDatasetInitialized &&
(this._options.enableCheckboxSelector || this._options.enableRowSelection || this._options.enableHybridSelection)) {
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.grid);
// after the DataView is created & updated execute some processes & dispatch some events
if (!this.props.customDataView) {
this.executeAfterDataviewCreated(this.grid, this._options);
}
// bind resize ONLY after the dataView is ready
this.bindResizeHook(this.grid, this._options);
// bind the Backend Service API callback functions only after the grid is initialized
// because the preProcess() and onInit() might get triggered
if (this._options?.backendServiceApi) {
this.bindBackendCallbackFunctions(this._options);
}
// create the React Grid Instance with reference to all Services
const reactElementInstance = {
element: this._elm,
// Slick Grid & DataView objects
dataView: this.dataView,
slickGrid: this.grid,
// public methods
dispose: this.dispose.bind(this),
// return all available Services (non-singleton)
backendService: this.backendService,
eventPubSubService: this._eventPubSubService,
extensionService: this.extensionService,
filterService: this.filterService,
gridEventService: this.gridEventService,
gridStateService: this.gridStateService,
gridService: this.gridService,
headerGroupingService: this.headerGroupingService,
paginationService: this.paginationService,
resizerService: this.resizerService,
sortService: this.sortService,
treeDataService: this.treeDataService,
};
// addons (SlickGrid extra plugins/controls)
this.extensions = this.extensionService?.extensionList;
// all instances (SlickGrid, DataView & all Services)
this.instances = reactElementInstance;
this.setStateValue('instances', reactElementInstance);
this._eventPubSubService.publish('onReactGridCreated', reactElementInstance);
// subscribe to column definitions assignment changes
this.observeColumns();
}
componentWillUnmount(shouldEmptyDomElementContainer = false) {
this._eventPubSubService.publish('onBeforeGridDestroy', this.grid);
this._eventHandler?.unsubscribeAll();
if (typeof this._i18next?.off === 'function') {
this._i18next.off('languageChanged');
}
// we could optionally also empty the content of the grid container DOM element
if (shouldEmptyDomElementContainer) {
this.emptyGridContainerElm();
}
this._collectionObservers.forEach((obs) => obs?.disconnect());
this._eventPubSubService.publish('onAfterGridDestroyed', true);
// dispose of all Services
this.serviceList.forEach((service) => {
if (service?.dispose) {
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.dataView) {
if (this.dataView.setItems) {
this.dataView.setItems([]);
}
if (this.dataView.destroy) {
this.dataView.destroy();
}
}
if (this.grid?.destroy) {
this.grid.destroy(shouldEmptyDomElementContainer);
}
// also dispose of all Subscriptions
this.subscriptions = disposeAllSubscriptions(this.subscriptions);
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.props.columns)) {
this.props.columns[prop] = null;
}
for (const prop of Object.keys(this.sharedService)) {
this.sharedService[prop] = null;
}
this._dataset = null;
this._columns = [];
}
emptyGridContainerElm() {
const gridContainerId = this._options?.gridContainerId || 'grid1';
const gridContainerElm = document.querySelector(`#${gridContainerId}`);
emptyElement(gridContainerElm);
}
dispose(shouldEmptyDomElementContainer = false) {
this.componentWillUnmount(shouldEmptyDomElementContainer);
}
disposeExternalResources() {
if (Array.isArray(this._registeredResources)) {
while (this._registeredResources.length > 0) {
const res = this._registeredResources.pop();
if (res?.dispose) {
res.dispose();
}
}
}
this._registeredResources = [];
}
componentDidUpdate(prevProps) {
// get the grid options (order of precedence is Global Options first, then user option which could overwrite the Global options)
if (this.props.options !== prevProps.options) {
this._options = { ...GlobalGridOptions, ...this._options };
}
if (this.props.columns !== prevProps.columns) {
this._columns = this.props.columns;
this.columnsChanged(this.props.columns);
}
if (this.props.dataset !== prevProps.dataset) {
this.dataset = this.props.dataset || prevProps.dataset;
}
if (this.props.datasetHierarchical && this.props.datasetHierarchical !== prevProps.datasetHierarchical) {
this.datasetHierarchical = this.props.datasetHierarchical;
}
this.suggestDateParsingWhenHelpful();
}
columnsChanged(columns) {
if (columns) {
this._columns = columns;
}
if (this._isGridInitialized) {
this.updateColumnsList(this._columns);
}
if (this._columns.length > 0) {
this.copyColumnWidthsReference(this._columns);
}
}
/**
* 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) => {
const datasetName = backendApi && backendApiService && typeof backendApiService.getDatasetName === 'function'
? backendApiService.getDatasetName()
: '';
if (processResult?.data[datasetName]) {
const data = processResult.data[datasetName].hasOwnProperty('nodes')
? processResult.data[datasetName].nodes
: processResult.data[datasetName];
const totalCount = processResult.data[datasetName].hasOwnProperty('totalCount')
? processResult.data[datasetName].totalCount
: processResult.data[datasetName].length;
this.refreshGridData(data, totalCount || 0);
}
};
}
}
}
bindDifferentHooks(grid, gridOptions, dataView) {
// translate some of them on first load, then on each language change
if (gridOptions.enableTranslate) {
this.extensionService.translateAllExtensions();
}
// on locale change, we have to manually translate the Headers, GridMenu
if (typeof this._i18next?.on === 'function') {
this._i18next.on('languageChanged', (lang) => {
// publish event of the same name that Slickgrid-Universal uses on a language change event
this._eventPubSubService.publish('onLanguageChange', lang);
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.grid, this.sharedService);
}
}
if (dataView && grid) {
// on cell click, mainly used with the columnDef.action callback
this.gridEventService.bindOnBeforeEditCell(grid);
this.gridEventService.bindOnCellChange(grid);
this.gridEventService.bindOnClick(grid);
if (dataView && 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.sharedService.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.options.autosizeColumnsByCellContentOnFirstLoad || this.options.enableAutoResizeColumnsByCellContent)) {
this.resizerService.resizeColumnsByCellContent(!this._options?.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();
}
});
}
}
}
}
bindBackendCallbackFunctions(gridOptions) {
const backendApi = gridOptions.backendServiceApi;
const backendApiService = backendApi?.service;
const serviceOptions = backendApiService?.options || {};
const isExecuteCommandOnInit = !serviceOptions
? false
: serviceOptions && serviceOptions.hasOwnProperty('executeProcessCommandOnInit')
? 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._options?.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() : '';
const process = isExecuteCommandOnInit ? (backendApi.process?.(query) ?? null) : (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 it after process execution
const startTime = new Date();
// run any pre-process, if defined, for example a spinner
if (backendApi.preProcess) {
backendApi.preProcess();
}
// the processes can be a Promise (like Http)
const totalItems = this._options?.pagination?.totalItems ?? 0;
if (process instanceof Promise) {
process
.then((processResult) => backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems))
.catch((error) => backendUtilityService.onBackendError(error, backendApi));
}
else if (process && this.rxjs?.isObservable(process)) {
this.subscriptions.push(process.subscribe((processResult) => backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems), (error) => backendUtilityService.onBackendError(error, backendApi)));
}
});
}
// when user enables Infinite Scroll
if (backendApi.service.options?.infiniteScroll) {
this.addBackendInfiniteScrollCallback();
}
}
}
addBackendInfiniteScrollCallback() {
if (this.grid && this.options.backendServiceApi && this.hasBackendInfiniteScroll() && !this.options.backendServiceApi?.onScrollEnd) {
const onScrollEnd = () => {
this.backendUtilityService.setInfiniteScrollBottomHit(true);
// even if we're not showing pagination, we still use pagination service behind the scene
// to keep track of the scroll position and fetch next set of data (aka next page)
// we also need a flag to know if we reached the of the dataset or not (no more pages)
this.paginationService.goToNextPage().then((hasNext) => {
if (!hasNext) {
this.backendUtilityService.setInfiniteScrollBottomHit(false);
}
});
};
this.options.backendServiceApi.onScrollEnd = onScrollEnd;
// subscribe to SlickGrid onScroll to determine when reaching the end of the scroll bottom position
// run onScrollEnd() method when that happens
this._eventHandler.subscribe(this.grid.onScroll, (_e, args) => {
const viewportElm = args.grid.getViewportNode();
if (['mousewheel', 'scroll'].includes(args.triggeredBy || '') &&
this.paginationService?.totalItems &&
args.scrollTop > 0 &&
Math.ceil(viewportElm.offsetHeight + args.scrollTop) >= args.scrollHeight) {
if (!this._scrollEndCalled) {
onScrollEnd();
this._scrollEndCalled = true;
}
}
});
// use postProcess to identify when scrollEnd process is finished to avoid calling the scrollEnd multiple times
// we also need to keep a ref of the user's postProcess and call it after our own postProcess
const orgPostProcess = this.options.backendServiceApi.postProcess;
this.options.backendServiceApi.postProcess = (processResult) => {
this._scrollEndCalled = false;
if (orgPostProcess) {
orgPostProcess(processResult);
}
};
}
}
bindResizeHook(grid, options) {
if ((options.autoFitColumnsOnFirstLoad && options.autosizeColumnsByCellContentOnFirstLoad) ||
(options.enableAutoSizeColumns && options.enableAutoResizeColumnsByCellContent)) {
throw new Error(`[Slickgrid-React] You cannot enable both autosize/fit viewport & resize by content, you must choose which resize technique to use. You can enable these 2 options ("autoFitColumnsOnFirstLoad" and "enableAutoSizeColumns") OR these other 2 options ("autosizeColumnsByCellContentOnFirstLoad" and "enableAutoResizeColumnsByCellContent").`);
}
// auto-resize grid on browser resize
if (options.gridHeight || options.gridWidth) {
this.resizerService.resizeGrid(0, { height: options.gridHeight, width: options.gridWidth });
}
else {
this.resizerService.resizeGrid();
}
// expand/autofit columns on first page load
if (grid &&
options?.enableAutoResize &&
options.autoFitColumnsOnFirstLoad &&
options.enableAutoSizeColumns &&
!this._isAutosizeColsCalled) {
grid.autosizeColumns();
this._isAutosizeColsCalled = true;
}
}
executeAfterDataviewCreated(_grid, gridOptions) {
// if user entered some Sort "presets", we need to reflect them all in the DOM
if (gridOptions.enableSorting) {
if (gridOptions.presets && Array.isArray(gridOptions.presets.sorters)) {
// when using multi-column sort, we can have multiple but on single sort then only grab the first sort provided
const sortColumns = this._options?.multiColumnSort ? gridOptions.presets.sorters : gridOptions.presets.sorters.slice(0, 1);
this.sortService.loadGridSorters(sortColumns);
}
}
}
/**
* 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.grid &&
!isSyncGridSelectionEnabled &&
this.options?.backendServiceApi &&
(this.options.enableRowSelection || this.options.enableHybridSelection || this.options.ena