import type { ICollectionObserver, ICollectionSubscriber } from '@aurelia/runtime';
import type {
AutocompleterEditor,
BackendService,
BackendServiceApi,
BackendServiceOption,
BasePaginationComponent,
Column,
DataViewOption,
EventSubscription,
ExtensionList,
ExternalResource,
ExternalResourceConstructor,
Locale,
Metrics,
Pagination,
PaginationMetadata,
SelectEditor,
} from '@slickgrid-universal/common';
import {
autoAddEditorFormatterToColumnsWithEditor,
BackendUtilityService,
CollectionService,
emptyElement,
ExtensionService,
ExtensionUtility,
FilterFactory,
FilterService,
GridEventService,
GridService,
GridStateService,
HeaderGroupingService,
isColumnDateType,
PaginationService,
PluginFlagMappings,
ResizerService,
SharedService,
SlickDataView,
SlickEventHandler,
SlickGrid,
SlickgridConfig,
SlickGroupItemMetadataProvider,
SortService,
TreeDataService,
type Observable,
type RxJsFacade,
} 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 { extend } from '@slickgrid-universal/utils';
import { bindable, BindingMode, customElement, IContainer, IEventAggregator, IObserverLocator, resolve, type IDisposable } from 'aurelia';
import { dequal } from 'dequal/lite';
import { Constants } from '../constants.js';
import { GlobalGridOptions } from '../global-grid-options.js';
import type { AureliaGridInstance, GridOption } from '../models/index.js';
import { AureliaUtilService, ContainerService, disposeAllSubscriptions, TranslaterService } from '../services/index.js';
const WARN_NO_PREPARSE_DATE_SIZE = 10000; // data size to warn user when pre-parse isn't enabled
export interface AureliaRowDetailView {
create(columns: Column[], gridOptions: GridOption): any;
init(grid: SlickGrid, containerService?: ContainerService): void;
}
@customElement({
name: 'aurelia-slickgrid',
template: `
`,
})
export class AureliaSlickgridCustomElement {
protected _columns: Column[] = [];
protected _columnsObserver?: ICollectionObserver<'array'>;
protected _columnsSubscriber: ICollectionSubscriber = {
handleCollectionChange: this.columnsHandler.bind(this),
};
protected _currentDatasetLength = 0;
protected _darkMode = false;
protected _dataset: any[] | null = null;
protected _eventHandler!: SlickEventHandler;
protected _eventPubSubService!: EventPubSubService;
protected _hideHeaderRowAfterPageLoad = false;
protected _isAutosizeColsCalled = false;
protected _isGridInitialized = false;
protected _isDatasetInitialized = false;
protected _isDatasetHierarchicalInitialized = false;
protected _isPaginationInitialized = false;
protected _isLocalGrid = true;
protected _paginationOptions: Pagination | undefined;
protected _registeredResources: Array = [];
protected _scrollEndCalled = false;
backendServiceApi: BackendServiceApi | undefined;
locales!: Locale;
groupItemMetadataProvider?: SlickGroupItemMetadataProvider;
metrics?: Metrics;
paginationData?: {
gridOptions: GridOption;
paginationService: PaginationService;
};
serviceList: any[] = [];
showPagination = false;
subscriptions: Array = [];
// components / plugins
slickEmptyWarning: SlickEmptyWarningComponent | undefined;
slickFooter: SlickFooterComponent | undefined;
paginationComponent: BasePaginationComponent | undefined;
slickPagination: BasePaginationComponent | undefined;
slickRowDetailView?: AureliaRowDetailView;
// services
backendUtilityService!: BackendUtilityService;
collectionService: CollectionService;
extensionService: ExtensionService;
extensionUtility: ExtensionUtility;
filterFactory!: FilterFactory;
filterService: FilterService;
gridContainer!: HTMLDivElement;
gridEventService: GridEventService;
gridService: GridService;
gridStateService: GridStateService;
headerGroupingService: HeaderGroupingService;
paginationService: PaginationService;
resizerService!: ResizerService;
rxjs?: RxJsFacade;
sharedService: SharedService;
sortService: SortService;
treeDataService: TreeDataService;
@bindable({ mode: BindingMode.twoWay }) columns: Column[] = [];
@bindable({ mode: BindingMode.twoWay }) element!: Element;
@bindable({ mode: BindingMode.twoWay }) dataview!: SlickDataView;
@bindable({ mode: BindingMode.twoWay }) grid!: SlickGrid;
@bindable({ mode: BindingMode.twoWay }) paginationOptions: Pagination | undefined;
@bindable({ mode: BindingMode.twoWay }) totalItems = 0;
@bindable({ mode: BindingMode.fromView }) extensions!: ExtensionList;
@bindable({ mode: BindingMode.fromView }) instances: AureliaGridInstance | null = null;
@bindable() customDataView?: SlickDataView;
@bindable() dataset: any[] = [];
@bindable() datasetHierarchical?: any[] | null;
@bindable() gridId = '';
@bindable() options: GridOption = {};
constructor(
protected readonly aureliaUtilService: AureliaUtilService = resolve(AureliaUtilService),
protected readonly observerLocator: IObserverLocator = resolve(IObserverLocator),
protected readonly container: IContainer = resolve(IContainer),
protected readonly elm: HTMLElement = resolve(HTMLElement),
protected readonly globalEa: IEventAggregator = resolve(IEventAggregator),
protected readonly containerService: ContainerService = resolve(ContainerService),
protected readonly translaterService: TranslaterService = resolve(TranslaterService)
) {
const slickgridConfig = new SlickgridConfig();
// initialize and assign all Service Dependencies
this._eventPubSubService = new EventPubSubService(this.elm);
this._eventPubSubService.eventNamingStyle = 'camelCase';
this.backendUtilityService = new BackendUtilityService();
this.gridEventService = new GridEventService();
this.sharedService = new SharedService();
this.collectionService = new CollectionService(this.translaterService);
this.extensionUtility = new ExtensionUtility(this.sharedService, this.backendUtilityService, this.translaterService);
this.filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService);
this.filterService = new FilterService(
this.filterFactory as any,
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.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,
];
// 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);
}
get backendService(): BackendService | undefined {
return this.options.backendServiceApi?.service;
}
get eventHandler(): SlickEventHandler {
return this._eventHandler;
}
get isDatasetInitialized(): boolean {
return this._isDatasetInitialized;
}
set isDatasetInitialized(isInitialized: boolean) {
this._isDatasetInitialized = isInitialized;
}
set isDatasetHierarchicalInitialized(isInitialized: boolean) {
this._isDatasetHierarchicalInitialized = isInitialized;
}
get registeredResources(): Array {
return this._registeredResources;
}
attached() {
if (!this.columns) {
throw new Error(
'Using `` requires `columns.bind`, it seems that you might have forgot to provide the missing bindable model.'
);
}
this._eventHandler = new SlickEventHandler();
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.options?.enableEmptyDataWarningMessage) {
const dataset = this.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: SlickEventHandler) {
if (!this.options || !this.columns) {
throw new Error(
'Using `` requires `columns.bind="columns"`, ' +
'it seems that you might have forgot to provide them since at least of them is undefined.'
);
}
this.options.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.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 ?? '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._currentDatasetLength = this._dataset.length;
this.options = this.mergeGridOptions(this.options);
this._paginationOptions = this.options?.pagination;
this.locales = this.options?.locales ?? Constants.locales;
this.backendServiceApi = this.options?.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.options.backendServiceApi && !this.options.backendServiceApi?.disableInternalPostProcess) {
this.createBackendApiInternalPostProcessCallback(this.options);
}
if (!this.customDataView) {
const dataviewInlineFilters = (this.options.dataView && this.options.dataView.inlineFilters) || false;
let dataViewOptions: Partial = { ...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.
// Wrap each editor class in the Factory resolver so consumers of this library can use
// dependency injection. Aurelia will resolve all dependencies when we pass the container
// and allow slickgrid to pass its arguments to the editors constructor last
// when slickgrid creates the editor
// https://github.com/aurelia/dependency-injection/blob/master/src/resolvers.js
this._columns = this.loadSlickGridEditors(this._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;
// TODO: revisit later, this conflicts with Grid State (Example 15)
// 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<{ columns: Column[]; grid: SlickGrid }>('onPluginColumnsChanged', data => {
// this.columns = data.columns;
// this.columnsChanged();
// })
// );
// 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.gridContainer,
this.customDataView || this.dataview,
this._columns,
this.options,
this._eventPubSubService
);
this.sharedService.dataView = this.dataview;
this.sharedService.slickGrid = this.grid;
this.sharedService.gridContainerElement = this.elm as HTMLDivElement;
if (this.groupItemMetadataProvider) {
this.grid.registerPlugin(this.groupItemMetadataProvider); // register GroupItemMetadataProvider when Grouping is enabled
}
// get any possible Services that user want to register
this.registerResources();
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();
// 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.querySelector('div');
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.translaterService
);
this.slickFooter.renderFooter(gridContainerElm as HTMLDivElement);
}
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.options?.enableTreeData ? this.sortTreeDataset(this.dataset) : this.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 as boolean;
}
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.enableSelection)) {
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.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 Aurelia Grid Instance with reference to all Services
const aureliaElementInstance: AureliaGridInstance = {
// Slick Grid & DataView objects
dataView: this.dataview,
slickGrid: this.grid,
extensions: this.extensionService?.extensionList,
// public methods
dispose: this.disposeInstance.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,
headerGroupingService: this.headerGroupingService,
extensionService: this.extensionService,
paginationComponent: this.slickPagination,
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 = aureliaElementInstance;
this._eventPubSubService.publish('onAureliaGridCreated', aureliaElementInstance);
}
/** Do not confuse with the Aurelia hook - it's been renamed */
detaching(shouldEmptyDomElementContainer = false) {
this._eventPubSubService.publish('onBeforeGridDestroy', this.grid);
this._eventHandler?.unsubscribeAll();
// dispose of all Services
this.serviceList.forEach((service: any) => {
if (service?.dispose) {
service.dispose();
}
});
this._eventPubSubService.unsubscribeAll();
this.serviceList.length = 0;
// 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);
this._columnsObserver?.unsubscribe(this._columnsSubscriber);
if (this.backendServiceApi) {
for (const prop of Object.keys(this.backendServiceApi)) {
(this.backendServiceApi as any)[prop] = null;
}
this.backendServiceApi = undefined;
}
for (const prop of Object.keys(this.columns)) {
(this.columns as any)[prop] = null;
}
for (const prop of Object.keys(this.sharedService)) {
(this.sharedService as any)[prop] = null;
}
this._dataset = null;
this.datasetHierarchical = null;
this._columns = [];
// we could optionally also empty the content of the grid container DOM element
if (shouldEmptyDomElementContainer) {
this.emptyGridContainerElm();
}
}
emptyGridContainerElm() {
const gridContainerId = this.options?.gridContainerId || 'grid1';
const gridContainerElm = document.querySelector(`#${gridContainerId}`) as HTMLDivElement;
emptyElement(gridContainerElm);
}
/** Do not rename to `dispose` as it's an Aurelia hook */
disposeInstance(shouldEmptyDomElementContainer = false) {
this.detaching(shouldEmptyDomElementContainer);
}
disposeExternalResources() {
if (Array.isArray(this._registeredResources)) {
while (this._registeredResources.length > 0) {
const res = this._registeredResources.pop();
if (typeof (res as ExternalResource)?.dispose === 'function') {
(res as ExternalResource).dispose!();
}
}
}
this._registeredResources = [];
}
bound() {
// get the grid options (order of precedence is Global Options first, then user option which could overwrite the Global options)
this.options = { ...GlobalGridOptions, ...this.options };
// in Au2, the xChanged is not fired upon initial component initialization which differs from Au1
// we do however need this to happen, so we will call it manually
this.columnsChanged();
// subscribe to column definitions assignment changes
this.observeColumnDefinitions();
}
/** on columnDefinitions assignment and/or .slice() call */
columnsChanged() {
this.columnsHandler();
this.observeColumnDefinitions();
}
datasetChanged(newDataset: any[], oldValue: any[]) {
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.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._dataset = data;
this.refreshGridData(data || []);
this._currentDatasetLength = (newDataset || []).length;
// expand/autofit columns on first page load
// we can assume that if the oldValue was empty then we are on first load
if (this.grid && this.options.autoFitColumnsOnFirstLoad && (!oldValue || oldValue.length < 1) && !this._isAutosizeColsCalled) {
this.grid.autosizeColumns();
this._isAutosizeColsCalled = true;
}
this.suggestDateParsingWhenHelpful();
}
datasetHierarchicalChanged(newHierarchicalDataset: any[] | undefined) {
const isDatasetEqual = dequal(newHierarchicalDataset, this.sharedService?.hierarchicalDataset ?? []);
const prevFlatDatasetLn = this._currentDatasetLength;
this.sharedService.hierarchicalDataset = newHierarchicalDataset;
if (newHierarchicalDataset && this.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 (newHierarchicalDataset && this.grid && this.sortService?.processTreeDataInitialSort) {
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();
if (flatDatasetLn > 0 && (flatDatasetLn !== prevFlatDatasetLn || !isDatasetEqual)) {
this.filterService.refreshTreeDataFilters();
}
});
this._isDatasetHierarchicalInitialized = true;
}
}
/**
* 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: GridOption) {
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: any) => {
const datasetName =
backendApi && backendApiService && typeof backendApiService.getDatasetName === 'function'
? backendApiService.getDatasetName()
: '';
if (!Array.isArray(processResult) && processResult?.data[datasetName]) {
const data = processResult.data[datasetName].hasOwnProperty('nodes')
? (processResult as any).data[datasetName].nodes
: (processResult as any).data[datasetName];
const totalCount = processResult.data[datasetName].hasOwnProperty('totalCount')
? (processResult as any).data[datasetName].totalCount
: (processResult as any).data[datasetName].length;
this.refreshGridData(data, totalCount || 0);
}
};
}
}
}
bindDifferentHooks(grid: SlickGrid, gridOptions: GridOption, dataView: SlickDataView) {
// 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
this.subscriptions.push(
this.globalEa.subscribe('i18n:locale:changed', (args: { oldLocale: string; newLocale: string }) => {
// publish event of the same name that Slickgrid-Universal uses on a language change event
this._eventPubSubService.publish('onLanguageChange', args.newLocale);
if (gridOptions.enableTranslate) {
this.extensionService.translateAllExtensions(args.newLocale);
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 SharedService flag
this._eventHandler.subscribe(grid.onColumnsReordered, () => {
this.sharedService.hasColumnsReordered = true;
});
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: number) => grid.updateRow(row));
grid.render();
}
});
}
}
}
}
bindBackendCallbackFunctions(gridOptions: GridOption) {
const backendApi = gridOptions.backendServiceApi;
const backendApiService = backendApi?.service;
const serviceOptions: BackendServiceOption = 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 as 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: any) =>
backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems)
)
.catch((error) => backendUtilityService.onBackendError(error, backendApi));
} else if (process && this.rxjs?.isObservable(process)) {
this.subscriptions.push(
(process as Observable).subscribe(
(processResult: any) =>
backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems),
(error: any) => backendUtilityService.onBackendError(error, backendApi)
)
);
}
});
}
// when user enables Infinite Scroll
if (backendApi.service.options?.infiniteScroll) {
this.addBackendInfiniteScrollCallback();
}
}
}
protected addBackendInfiniteScrollCallback(): void {
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: any) => {
this._scrollEndCalled = false;
if (orgPostProcess) {
orgPostProcess(processResult);
}
};
}
}
bindResizeHook(grid: SlickGrid, options: GridOption) {
if (
(options.autoFitColumnsOnFirstLoad && options.autosizeColumnsByCellContentOnFirstLoad) ||
(options.enableAutoSizeColumns && options.enableAutoResizeColumnsByCellContent)
) {
throw new Error(
`[Aurelia-Slickgrid] 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: SlickGrid, gridOptions: GridOption) {
// 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: PaginationMetadata) {
const isSyncGridSelectionEnabled = this.gridStateService?.needToPreserveRowSelection() ?? false;
if (
this.grid &&
!isSyncGridSelectionEnabled &&
this.options?.backendServiceApi &&
(this.options.enableSelection || this.options.enableCheckboxSelector)
) {
this.grid.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: 'pagination' },
gridState: this.gridStateService.getCurrentGridState(),
});
}
paginationOptionsChanged(newPaginationOptions: Pagination) {
if (newPaginationOptions && this._paginationOptions) {
this._paginationOptions = { ...this._paginationOptions, ...newPaginationOptions };
} else {
this._paginationOptions = newPaginationOptions;
}
this.options.pagination = this._paginationOptions;
this.paginationService.updateTotalItems(newPaginationOptions?.totalItems ?? 0, true);
}
/**
* When dataset changes, we need to refresh the entire grid UI & possibly resize it as well
* @param dataset
*/
refreshGridData(dataset: any[], totalCount?: number) {
// 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.options?.enablePagination && this._isLocalGrid) {
this.showPagination = true;
this.loadLocalGridPagination(dataset);
}
if (this.options?.enableEmptyDataWarningMessage && Array.isArray(dataset)) {
const finalTotalCount = totalCount || dataset.length;
this.displayEmptyDataWarning(finalTotalCount < 1);
}
if (Array.isArray(dataset) && this.grid && this.dataview?.setItems) {
this.dataview.setItems(dataset, this.options.datasetIdPropertyName ?? 'id');
if (!this.options.backendServiceApi && !this.options.enableTreeData) {
this.dataview.reSort();
}
if (dataset.length > 0) {
if (!this._isDatasetInitialized) {
this.loadFilterPresetsWhenDatasetInitialized();
if (this.options.enableCheckboxSelector) {
this.loadRowSelectionPresetWhenExists();
}
}
this._isDatasetInitialized = true;
}
// 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.options &&
(this.options.enablePagination || (this.options.backendServiceApi && this.options.enablePagination === undefined))
);
if (this._paginationOptions && this.options?.pagination && this.options?.backendServiceApi) {
const paginationOptions = this.setPaginationOptionsWhenPresetDefined(this.options, 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.options?.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.grid && this.options.enableAutoResize) {
const delay = this.options.autoResize && this.options.autoResize.delay;
this.resizerService.resizeGrid(delay || 10);
}
}
}
/**
* Show the filter row displayed on first row, we can optionally pass false to hide it.
* @param showing
*/
showHeaderRow(showing = true) {
this.grid.setHeaderRowVisibility(showing);
if (showing === true && this._isGridInitialized) {
this.grid.setColumns(this.columns);
}
return showing;
}
setData(data: any[], 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
*/
protected setPaginationOptionsWhenPresetDefined(gridOptions: GridOption, paginationOptions: Pagination): Pagination {
if (gridOptions.presets?.pagination && gridOptions.pagination) {
if (this.hasBackendInfiniteScroll()) {
console.warn('[Aurelia-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) {
this.sharedService.gridContainerElement?.classList.toggle('slick-dark-mode', dark);
}
/**
* 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(newColumns: Column[]) {
if (newColumns) {
// map the Editor model to editorClass and load editor collectionAsync
newColumns = this.loadSlickGridEditors(newColumns);
// 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(newColumns, this.options.autoAddCustomEditorFormatter);
}
if (this.options.enableTranslate) {
this.extensionService.translateColumnHeaders(undefined, newColumns);
}
this.extensionService.renderColumnHeaders(newColumns, true);
if (this.options?.enableAutoSizeColumns) {
this.grid.autosizeColumns();
} else if (this.options?.enableAutoResizeColumnsByCellContent && this.resizerService?.resizeColumnsByCellContent) {
this.resizerService.resizeColumnsByCellContent();
}
}
}
//
// protected functions
// ------------------
/** handler for when column definitions changes */
protected columnsHandler() {
this._columns = this.columns;
if (this._isGridInitialized) {
this.updateColumnDefinitionsList(this.columns);
}
if (this._columns.length > 0) {
this.copyColumnWidthsReference(this._columns);
}
}
/**
* assignment changes are not triggering a "columnDefinitionsChanged" event https://stackoverflow.com/a/30286225/1212166
* we can use array observer for these other changes done via (push, pop, ...)
* see docs https://docs.aurelia.io/components/bindable-properties#calling-a-change-function-when-bindable-is-modified
*/
protected observeColumnDefinitions() {
this._columnsObserver?.unsubscribe(this._columnsSubscriber);
this._columnsObserver = this.observerLocator.getArrayObserver(this.columns);
this._columnsObserver.subscribe(this._columnsSubscriber);
}
/**
* 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.
*/
protected copyColumnWidthsReference(columns: Column[]) {
columns.forEach((col) => (col.originalWidth = col.width));
}
protected displayEmptyDataWarning(showWarning = true) {
this.slickEmptyWarning?.showEmptyDataMessage(showWarning);
}
/** When data changes in the DataView, we'll refresh the metrics and/or display a warning if the dataset is empty */
protected handleOnItemCountChanged(currentPageRowItemCount: number, totalItemCount: number) {
this._currentDatasetLength = totalItemCount;
this.metrics = {
startTime: new Date(),
endTime: new Date(),
itemCount: currentPageRowItemCount,
totalItemCount,
};
// if custom footer is enabled, then we'll update its metrics
if (this.slickFooter) {
this.slickFooter.metrics = this.metrics;
}
// when using local (in-memory) dataset, we'll display a warning message when filtered data is empty
if (this._isLocalGrid && this.options?.enableEmptyDataWarningMessage) {
this.displayEmptyDataWarning(currentPageRowItemCount === 0);
}
// when autoResize.autoHeight is enabled, we'll want to call a resize
if (this.options.enableAutoResize && this.resizerService.isAutoHeightEnabled && currentPageRowItemCount > 0) {
this.resizerService.resizeGrid();
}
}
/** Initialize the Pagination Service once */
protected initializePaginationService(paginationOptions: Pagination) {
if (this.options) {
this.paginationData = {
gridOptions: this.options,
paginationService: this.paginationService,
};
this.paginationService.totalItems = this.totalItems;
this.paginationService.init(this.grid, paginationOptions, this.backendServiceApi);
this.subscriptions.push(
this._eventPubSubService.subscribe('onPaginationChanged', (paginationChanges: PaginationMetadata) =>
this.paginationChanged(paginationChanges)
),
this._eventPubSubService.subscribe('onPaginationVisibilityChanged', (visibility: { visible: boolean }) => {
this.showPagination = visibility?.visible ?? false;
if (this.options?.backendServiceApi) {
this.backendUtilityService?.refreshBackendDataset(this.options);
}
this.renderPagination(this.showPagination);
})
);
// also initialize (render) the pagination component
this.renderPagination();
this._isPaginationInitialized = true;
}
}
/** Load the Editor Collection asynchronously and replace the "collection" property when Promise resolves */
protected loadEditorCollectionAsync(column: Column) {
if (column?.editor) {
const collectionAsync = column.editor.collectionAsync;
column.editor.disabled = true; // disable the Editor DOM element, we'll re-enable it after receiving the collection with "updateEditorCollection()"
if (collectionAsync instanceof Promise) {
// wait for the "collectionAsync", once resolved we will save it into the "collection"
// the collectionAsync can be of 3 types HttpClient, HttpFetch or a Promise
collectionAsync.then((response: any | any[]) => {
if (Array.isArray(response)) {
this.updateEditorCollection(column, response); // from Promise
} else if (response instanceof Response && typeof response.json === 'function') {
if (response.bodyUsed) {
console.warn(
`[Aurelia-SlickGrid] The response body passed to collectionAsync was already read. ` +
`Either pass the dataset from the Response or clone the response first using response.clone()`
);
} else {
// from Fetch
(response as Response).json().then((data) => this.updateEditorCollection(column, data));
}
} else if (response?.content) {
this.updateEditorCollection(column, response.content); // from http-client
}
});
} else if (this.rxjs?.isObservable(collectionAsync)) {
// wrap this inside a microtask at the end of the task to avoid timing issue since updateEditorCollection requires to call SlickGrid getColumns() method after columns are available
queueMicrotask(() => {
this.subscriptions.push(
(collectionAsync as Observable).subscribe((resolvedCollection) => this.updateEditorCollection(column, resolvedCollection))
);
});
}
}
}
/** Load any possible Columns Grid Presets */
protected loadColumnPresetsWhenDatasetInitialized() {
// if user entered some Columns "presets", we need to reflect them all in the grid
if (this.options.presets && Array.isArray(this.options.presets.columns) && this.options.presets.columns.length > 0) {
// delegate to GridStateService for centralized column arrangement logic
// we pass `false` for triggerAutoSizeColumns to maintain original behavior on preset load
this.gridStateService.changeColumnsArrangement(this.options.presets.columns, false);
}
}
/** Load any possible Filters Grid Presets */
protected loadFilterPresetsWhenDatasetInitialized() {
if (this.options && !this.customDataView) {
// if user entered some Filter "presets", we need to reflect them all in the DOM
// also note that a presets of Tree Data Toggling will also call this method because Tree Data toggling does work with data filtering
// (collapsing a parent will basically use Filter for hidding (aka collapsing) away the child underneat it)
if (
this.options.presets &&
(Array.isArray(this.options.presets.filters) || Array.isArray(this.options.presets?.treeData?.toggledItems))
) {
this.filterService.populateColumnFilterSearchTermPresets(this.options.presets?.filters || []);
}
}
}
/**
* 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
*/
protected loadLocalGridPagination(dataset?: any[]) {
if (this.options && this._paginationOptions) {
this.totalItems = Array.isArray(dataset) ? dataset.length : 0;
if (this._paginationOptions && this.dataview?.getPagingInfo) {
const slickPagingInfo = this.dataview.getPagingInfo();
if (slickPagingInfo?.hasOwnProperty('totalRows') && this._paginationOptions.totalItems !== slickPagingInfo.totalRows) {
this.totalItems = slickPagingInfo.totalRows || 0;
}
}
this._paginationOptions.totalItems = this.totalItems;
const paginationOptions = this.setPaginationOptionsWhenPresetDefined(this.options, this._paginationOptions);
this.initializePaginationService(paginationOptions);
}
}
/** Load any Row Selections into the DataView that were presets by the user */
protected loadRowSelectionPresetWhenExists() {
// if user entered some Row Selections "presets"
const presets = this.options?.presets;
const enableRowSelection = this.options && (this.options.enableCheckboxSelector || this.options.enableSelection);
if (
enableRowSelection &&
this.grid?.getSelectionModel() &&
presets?.rowSelection &&
(Array.isArray(presets.rowSelection.gridRowIndexes) || Array.isArray(presets.rowSelection.dataContextIds))
) {
let dataContextIds = presets.rowSelection.dataContextIds;
let gridRowIndexes = presets.rowSelection.gridRowIndexes;
// maps the IDs to the Grid Rows and vice versa, the "dataContextIds" has precedence over the other
if (Array.isArray(dataContextIds) && dataContextIds.length > 0) {
gridRowIndexes = this.dataview.mapIdsToRows(dataContextIds) || [];
} else if (Array.isArray(gridRowIndexes) && gridRowIndexes.length > 0) {
dataContextIds = this.dataview.mapRowsToIds(gridRowIndexes) || [];
}
// apply row selection when defined as grid presets
if (this.grid && Array.isArray(gridRowIndexes)) {
this.grid.setSelectedRows(gridRowIndexes);
this.dataview!.setSelectedIds(dataContextIds || [], {
isRowBeingAdded: true,
shouldTriggerEvent: false, // do not trigger when presetting the grid
applyRowSelectionToGrid: true,
});
}
}
}
hasBackendInfiniteScroll(gridOptions?: GridOption): boolean {
return !!(gridOptions || this.options).backendServiceApi?.service.options?.infiniteScroll;
}
protected mergeGridOptions(gridOptions: GridOption): GridOption {
gridOptions.gridId = this.gridId;
gridOptions.gridContainerId = `slickGridContainer-${this.gridId}`;
// use extend to deep merge & copy to avoid immutable properties being changed in GlobalGridOptions after a route change
const options = extend(true, {}, GlobalGridOptions, gridOptions) as GridOption;
// 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(gridOptions)) {
gridOptions.enablePagination = !!(gridOptions.backendServiceApi && gridOptions.enablePagination === undefined
? true
: gridOptions.enablePagination);
}
// using copy extend to do a deep clone has an unwanted side on objects and pageSizes but ES6 spread has other worst side effects
// so we will just overwrite the pageSizes when needed, this is the only one causing issues so far.
// On a deep extend, Object and Array are extended, but object wrappers on primitive types such as String, Boolean, and Number are not.
if (
options?.pagination &&
(gridOptions.enablePagination || gridOptions.backendServiceApi) &&
gridOptions.pagination &&
Array.isArray(gridOptions.pagination.pageSizes)
) {
options.pagination.pageSizes = gridOptions.pagination.pageSizes;
}
// also make sure to show the header row if user have enabled filtering
this._hideHeaderRowAfterPageLoad = options.showHeaderRow === false;
if (options.enableFiltering && !options.showHeaderRow) {
options.showHeaderRow = options.enableFiltering;
}
// when we use Pagination on Local Grid, it doesn't seem to work without enableFiltering
// so we'll enable the filtering but we'll keep the header row hidden
if (options && !options.enableFiltering && options.enablePagination && this._isLocalGrid) {
options.enableFiltering = true;
options.showHeaderRow = false;
this._hideHeaderRowAfterPageLoad = true;
if (this.sharedService) {
this.sharedService.hideHeaderRowAfterPageLoad = true;
}
}
return options;
}
/** Add a register of a new external resource, user could also optional dispose all previous resources before pushing any new resources to the resources array list. */
registerExternalResources(resources: Array, disposePreviousResources = false) {
if (disposePreviousResources) {
this.disposeExternalResources();
}
resources.forEach((res) => this._registeredResources.push(res));
this.initializeExternalResources(resources);
}
resetExternalResources() {
this._registeredResources = [];
}
/** initialized & auto-enable external registered resources, e.g. if user registers `ExcelExportService` then let's auto-enable `enableExcelExport:true` */
protected autoEnableInitializedResources(resource: ExternalResource | ExternalResourceConstructor): void {
if (this.grid && typeof (resource as ExternalResource).init === 'function') {
(resource as ExternalResource).init!(this.grid, this.containerService);
}
// auto-enable unless the flag was specifically disabled by the end user
if ('pluginName' in (resource as ExternalResource)) {
const pluginFlagName = PluginFlagMappings.get((resource as ExternalResource).pluginName!);
if (pluginFlagName && this.options[pluginFlagName] !== false) {
this.options[pluginFlagName] = true;
this.grid?.setOptions({ [pluginFlagName]: true });
}
}
}
protected initializeExternalResources(resources: Array) {
PluginFlagMappings.set('AureliaRowDetailView', 'enableRowDetailView'); // map the external Row Detail View resource to its flag
if (Array.isArray(resources)) {
for (const resource of resources) {
this.autoEnableInitializedResources(resource);
}
}
}
/** Pre-Register any Resource that don't require SlickGrid to be instantiated (for example RxJS Resource & RowDetail) */
protected preRegisterResources() {
this._registeredResources = this.options?.externalResources || [];
// bind & initialize all Components/Services that were tagged as enabled
// register all services by executing their init method and providing them with the Grid object
if (Array.isArray(this._registeredResources)) {
for (const resource of this._registeredResources) {
if ((resource as ExternalResource)?.pluginName === 'RxJsResource') {
this.registerRxJsResource(resource as RxJsFacade);
}
}
}
if (this.options.enableRowDetailView) {
const RowDetailClass = this._registeredResources.find((res: any) => res.pluginName === 'AureliaRowDetailView') as
| ExternalResourceConstructor
| undefined;
if (!RowDetailClass) {
throw new Error(
'[Aurelia-Slickgrid] You enabled the Row Detail View feature but you did not provide the "AureliaRowDetailView" class as an external resource.'
);
}
if (RowDetailClass) {
const rowDetailInstance = new RowDetailClass(
this.aureliaUtilService,
this._eventPubSubService,
this.elm as HTMLElement
) as AureliaRowDetailView;
this.slickRowDetailView = rowDetailInstance;
rowDetailInstance.create(this.columns, this.options);
this.extensionService.addExtensionToList('rowDetailView', {
name: 'rowDetailView',
instance: this.slickRowDetailView,
});
}
}
}
protected registerResources() {
// at this point, we consider all the registered services as external services, anything else registered afterward aren't external
if (Array.isArray(this._registeredResources)) {
this.sharedService.externalRegisteredResources = this._registeredResources;
}
// push all other Services that we want to be registered
if (!this._registeredResources.some((r) => r instanceof GridService)) {
this._registeredResources.push(this.gridService);
}
if (!this._registeredResources.some((r) => r instanceof GridStateService)) {
this._registeredResources.push(this.gridStateService);
}
// when using Grouping/DraggableGrouping/Colspan register its Service
if (
((this.options.createPreHeaderPanel && this.options.createTopHeaderPanel) ||
(this.options.createPreHeaderPanel && !this.options.enableDraggableGrouping)) &&
!this._registeredResources.some((r) => r instanceof HeaderGroupingService)
) {
this._registeredResources.push(this.headerGroupingService);
}
// when using Tree Data View, register its Service
if (this.options.enableTreeData && !this._registeredResources.some((r) => r instanceof TreeDataService)) {
this._registeredResources.push(this.treeDataService);
}
// when user enables translation, we need to translate Headers on first pass & subsequently in the bindDifferentHooks
if (this.options.enableTranslate) {
this.extensionService.translateColumnHeaders();
}
// also initialize (render) the empty warning component
if (!this._registeredResources.some((r) => r instanceof SlickEmptyWarningComponent)) {
this.slickEmptyWarning = new SlickEmptyWarningComponent();
this._registeredResources.push(this.slickEmptyWarning);
}
// bind & initialize all Components/Services that were tagged as enabled
// register all services by executing their init method and providing them with the Grid object
this.initializeExternalResources(this._registeredResources);
// initialize RowDetail separately since we already added it to the ExtensionList via `addExtensionToList()` but not in external resources,
// because we don't want to dispose the extension/resource more than once (because externalResources/extensionList are both looping through their list to dispose of them)
if (this.options.enableRowDetailView && this.slickRowDetailView) {
this.slickRowDetailView.init(this.grid);
}
}
/** Register the RxJS Resource in all necessary services which uses */
protected registerRxJsResource(resource: RxJsFacade) {
this.rxjs = resource;
this.backendUtilityService.addRxJsResource(this.rxjs);
this.filterFactory.addRxJsResource(this.rxjs);
this.filterService.addRxJsResource(this.rxjs);
this.sortService.addRxJsResource(this.rxjs);
this.paginationService.addRxJsResource(this.rxjs);
this.containerService.registerInstance('RxJsResource', this.rxjs);
}
/**
* Render (or dispose) the Pagination Component, user can optionally provide False (to not show it) which will in term dispose of the Pagination,
* also while disposing we can choose to omit the disposable of the Pagination Service (if we are simply toggling the Pagination, we want to keep the Service alive)
* @param {Boolean} showPagination - show (new render) or not (dispose) the Pagination
* @param {Boolean} shouldDisposePaginationService - when disposing the Pagination, do we also want to dispose of the Pagination Service? (defaults to True)
*/
protected async renderPagination(showPagination = true) {
if (this.grid && this.options?.enablePagination && !this._isPaginationInitialized && showPagination) {
if (this.options.customPaginationComponent) {
const paginationContainer = document.createElement('section');
this.elm.appendChild(paginationContainer);
const vm = await this.aureliaUtilService.createAureliaViewModelAddToSlot(
this.options.customPaginationComponent!,
undefined,
paginationContainer
);
const elmBindingContext = vm?.controller?.children?.[0].scope.bindingContext;
if (elmBindingContext) {
this.instances!.paginationComponent = elmBindingContext as BasePaginationComponent;
this.slickPagination = elmBindingContext as BasePaginationComponent;
}
} else {
this.slickPagination = new SlickPaginationComponent();
}
if (this.slickPagination) {
this.slickPagination.init(this.grid, this.paginationService, this._eventPubSubService, this.translaterService);
this.slickPagination.renderPagination(this.elm.querySelector('div') as HTMLDivElement);
this._isPaginationInitialized = true;
}
} else if (!showPagination) {
this.slickPagination?.dispose();
this._isPaginationInitialized = false;
}
}
/**
* Takes a flat dataset with parent/child relationship, sort it (via its tree structure) and return the sorted flat array
* @param {Array