angular-slickgrid
Version:
Slickgrid components made available in Angular
979 lines (970 loc) • 110 kB
JavaScript
import { GlobalGridOptions as GlobalGridOptions$1, SlickEventHandler, SlickgridConfig as SlickgridConfig$1, BackendUtilityService, GridEventService, SharedService, CollectionService, ExtensionUtility, FilterFactory, FilterService, ResizerService, SortService, TreeDataService, PaginationService, ExtensionService, GridStateService, GridService, HeaderGroupingService, unsubscribeAll, emptyElement, SlickGroupItemMetadataProvider, SlickDataView, autoAddEditorFormatterToColumnsWithEditor, SlickGrid, PluginFlagMappings, isColumnDateType } from '@slickgrid-universal/common';
export * from '@slickgrid-universal/common';
import * as i0 from '@angular/core';
import { ViewContainerRef, Injectable, Inject, Optional, EventEmitter, output, Component, ContentChild, Input, Output } from '@angular/core';
import * as i1 from '@ngx-translate/core';
import { NgTemplateOutlet } from '@angular/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 { RxJsResource } from '@slickgrid-universal/rxjs-observable';
import { extend } from '@slickgrid-universal/utils';
import { dequal } from 'dequal/lite';
class AngularUtilService {
vcr;
constructor(vcr) {
this.vcr = vcr;
}
createInteractiveAngularComponent(component, targetElement, data, createCompOptions) {
// Create a component reference from the component
const componentRef = this.vcr.createComponent(component, createCompOptions);
// user could provide data to assign to the component instance
if (componentRef?.instance && data) {
Object.assign(componentRef.instance, data);
}
// Get DOM element from component
let domElem = null;
const viewRef = componentRef.hostView;
if (viewRef && Array.isArray(viewRef.rootNodes) && viewRef.rootNodes[0]) {
domElem = viewRef.rootNodes[0];
// when user provides the DOM element target, we will move the dynamic component into that target (aka portal-ing it)
if (targetElement && domElem) {
targetElement.replaceChildren(componentRef.location.nativeElement);
}
}
return { componentRef, domElement: domElem };
}
/**
* Dynamically create an Angular component, user could also provide optional arguments for target, data & createComponent options
* @param {Component} component
* @param {HTMLElement} [targetElement]
* @param {*} [data]
* @param {CreateComponentOption} [createCompOptions]
* @returns
*/
createAngularComponent(component, targetElement, data, createCompOptions) {
// Create a component reference from the component
const componentRef = this.vcr.createComponent(component, createCompOptions);
// user could provide data to assign to the component instance
if (componentRef?.instance && data) {
Object.assign(componentRef.instance, data);
}
// Get DOM element from component
let domElem = null;
const viewRef = componentRef.hostView;
// get DOM element from the new dynamic Component, make sure this is read after any data
if (viewRef && Array.isArray(viewRef.rootNodes) && viewRef.rootNodes[0]) {
domElem = viewRef.rootNodes[0];
// when user provides the DOM element target, we will read the new Component html and use it to replace the target html
if (targetElement && domElem) {
targetElement.innerHTML =
typeof createCompOptions?.sanitizer === 'function' ? createCompOptions.sanitizer(domElem.innerHTML || '') : domElem.innerHTML;
}
}
return { componentRef, domElement: domElem };
}
/**
* Dynamically create an Angular component and append it to the DOM unless a target element is provided,
* user could also provide other optional arguments for data & createComponent options.
* @param {Component} component
* @param {HTMLElement} [targetElement]
* @param {*} [data]
* @param {CreateComponentOption} [createCompOptions]
* @returns
*/
createAngularComponentAppendToDom(component, targetElement, data, createCompOptions) {
const componentOutput = this.createAngularComponent(component, targetElement, data, createCompOptions);
// Append DOM element to the HTML element specified
if (targetElement?.replaceChildren) {
targetElement.replaceChildren(componentOutput.domElement);
}
else {
document.body.appendChild(componentOutput.domElement); // when no target provided, we'll simply add it to the HTML Body
}
return componentOutput;
}
static ɵfac = function AngularUtilService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || AngularUtilService)(i0.ɵɵinject(ViewContainerRef)); };
static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: AngularUtilService, factory: AngularUtilService.ɵfac });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AngularUtilService, [{
type: Injectable
}], () => [{ type: i0.ViewContainerRef, decorators: [{
type: Inject,
args: [ViewContainerRef]
}] }], null); })();
class ContainerService {
dependencies = [];
get(key) {
const dependency = this.dependencies.find((dep) => dep.key === key);
if (dependency?.instance) {
return dependency.instance;
}
return null;
}
dispose() {
this.dependencies = [];
}
registerInstance(key, instance) {
const dependency = this.dependencies.some((dep) => dep.key === key);
if (!dependency) {
this.dependencies.push({ key, instance });
}
}
static ɵfac = function ContainerService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ContainerService)(); };
static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: ContainerService, factory: ContainerService.ɵfac, providedIn: 'root' });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ContainerService, [{
type: Injectable,
args: [{
providedIn: 'root', // This ensures it can be injected anywhere
}]
}], null, null); })();
/**
* This is a Translate Service Wrapper for Slickgrid-Universal monorepo lib to work properly,
* it must implement Slickgrid-Universal TranslaterService interface to work properly
*/
class TranslaterService {
translateService;
constructor(translateService) {
this.translateService = translateService;
}
/**
* Method to return the current language used by the App
* @return {string} current language
*/
getCurrentLanguage() {
return this.translateService?.getCurrentLang?.() ?? '';
}
/**
* Method to set the language to use in the App and Translate Service
* @param {string} language
* @return {Promise} output
*/
async use(newLang) {
return this.translateService?.use?.(newLang);
}
/**
* Method which receives a translation key and returns the translated value assigned to that key
* @param {string} translation key
* @return {string} translated value
*/
translate(translationKey) {
return this.translateService?.instant?.(translationKey || ' ');
}
static ɵfac = function TranslaterService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || TranslaterService)(i0.ɵɵinject(i1.TranslateService, 8)); };
static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: TranslaterService, factory: TranslaterService.ɵfac });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TranslaterService, [{
type: Injectable
}], () => [{ type: i1.TranslateService, decorators: [{
type: Optional
}] }], null); })();
/**
* Unsubscribe all Observables Subscriptions
* It will return an empty array if it all went well
* @param subscriptions
*/
function unsubscribeAllObservables(subscriptions) {
if (Array.isArray(subscriptions)) {
let subscription = subscriptions.pop();
while (subscription) {
if (typeof subscription.unsubscribe === 'function') {
subscription.unsubscribe();
}
subscription = subscriptions.pop();
}
}
}
/** Global Grid Options Defaults */
const GlobalGridOptions = {
...GlobalGridOptions$1,
eventNamingStyle: 'camelCase',
// technically speaking the Row Detail requires the process & viewComponent but we'll ignore it just to set certain options
rowDetailView: {
collapseAllOnSort: true,
cssClass: 'detail-view-toggle',
panelRows: 1,
keyPrefix: '__',
useRowClick: false,
saveDetailViewOnScroll: false,
},
};
class SlickgridConfig {
options;
constructor() {
this.options = GlobalGridOptions;
}
}
class Constants {
// English Locale texts when using only 1 Locale instead of I18N
static locales = {
TEXT_ALL_SELECTED: 'All Selected',
TEXT_ALL_X_RECORDS_SELECTED: 'All {{x}} records selected',
TEXT_APPLY_MASS_UPDATE: 'Apply Mass Update',
TEXT_APPLY_TO_SELECTION: 'Update Selection',
TEXT_CANCEL: 'Cancel',
TEXT_CLEAR_ALL_FILTERS: 'Clear all Filters',
TEXT_CLEAR_ALL_GROUPING: 'Clear all Grouping',
TEXT_CLEAR_ALL_SORTING: 'Clear all Sorting',
TEXT_CLEAR_PINNING: 'Unfreeze Columns/Rows',
TEXT_CLONE: 'Clone',
TEXT_COLLAPSE_ALL_GROUPS: 'Collapse all Groups',
TEXT_CONTAINS: 'Contains',
TEXT_COLUMNS: 'Columns',
TEXT_COLUMN_RESIZE_BY_CONTENT: 'Resize by Content',
TEXT_COMMANDS: 'Commands',
TEXT_COPY: 'Copy',
TEXT_EQUALS: 'Equals',
TEXT_EQUAL_TO: 'Equal to',
TEXT_ENDS_WITH: 'Ends With',
TEXT_ERROR_EDITABLE_GRID_REQUIRED: 'Your grid must be editable in order to use the Composite Editor Modal.',
TEXT_ERROR_ENABLE_CELL_NAVIGATION_REQUIRED: 'Composite Editor requires the flag "enableCellNavigation" to be set to True in your Grid Options.',
TEXT_ERROR_NO_CHANGES_DETECTED: 'Sorry we could not detect any changes.',
TEXT_ERROR_NO_EDITOR_FOUND: 'We could not find any Editor in your Column Definition.',
TEXT_ERROR_NO_RECORD_FOUND: 'No records selected for edit or clone operation.',
TEXT_ERROR_ROW_NOT_EDITABLE: 'Current row is not editable.',
TEXT_ERROR_ROW_SELECTION_REQUIRED: 'You must select some rows before trying to apply new value(s).',
TEXT_EXPAND_ALL_GROUPS: 'Expand all Groups',
TEXT_EXPORT_TO_CSV: 'Export in CSV format',
TEXT_EXPORT_TO_TEXT_FORMAT: 'Export in Text format (Tab delimited)',
TEXT_EXPORT_TO_EXCEL: 'Export to Excel',
TEXT_EXPORT_TO_PDF: 'Export to PDF',
TEXT_EXPORT_TO_TAB_DELIMITED: 'Export in Text format (Tab delimited)',
TEXT_FORCE_FIT_COLUMNS: 'Force fit columns',
TEXT_FREEZE_COLUMNS: 'Freeze Columns',
TEXT_GREATER_THAN: 'Greater than',
TEXT_GREATER_THAN_OR_EQUAL_TO: 'Greater than or equal to',
TEXT_GROUP_BY: 'Group By',
TEXT_HIDE_COLUMN: 'Hide Column',
TEXT_ITEMS: 'items',
TEXT_ITEMS_PER_PAGE: 'items per page',
TEXT_ITEMS_SELECTED: 'items selected',
TEXT_OF: 'of',
TEXT_OK: 'OK',
TEXT_OPTIONS: 'Options',
TEXT_LAST_UPDATE: 'Last Update',
TEXT_LESS_THAN: 'Less than',
TEXT_LESS_THAN_OR_EQUAL_TO: 'Less than or equal to',
TEXT_LOADING: 'Loading...',
TEXT_NO_ELEMENTS_FOUND: 'Aucun élément trouvé',
TEXT_NOT_CONTAINS: 'Not contains',
TEXT_NOT_EQUAL_TO: 'Not equal to',
TEXT_PAGE: 'Page',
TEXT_REFRESH_DATASET: 'Refresh Dataset',
TEXT_REMOVE_FILTER: 'Remove Filter',
TEXT_REMOVE_SORT: 'Remove Sort',
TEXT_SAVE: 'Save',
TEXT_SELECT_ALL: 'Select All',
TEXT_SYNCHRONOUS_RESIZE: 'Synchronous resize',
TEXT_SORT_ASCENDING: 'Sort Ascending',
TEXT_SORT_DESCENDING: 'Sort Descending',
TEXT_STARTS_WITH: 'Starts With',
TEXT_TOGGLE_DARK_MODE: 'Toggle Dark Mode',
TEXT_TOGGLE_FILTER_ROW: 'Toggle Filter Row',
TEXT_TOGGLE_PRE_HEADER_ROW: 'Toggle Pre-Header Row',
TEXT_UNFREEZE_COLUMNS: 'Unfreeze Columns',
TEXT_X_OF_Y_SELECTED: '# of % selected',
TEXT_X_OF_Y_MASS_SELECTED: '{{x}} of {{y}} selected',
};
static treeDataProperties = {
CHILDREN_PROP: 'children',
COLLAPSED_PROP: '__collapsed',
HAS_CHILDREN_PROP: '__hasChildren',
LAZY_LOADING_PROP: '__lazyLoading',
TREE_LEVEL_PROP: '__treeLevel',
PARENT_PROP: '__parentId',
};
// some Validation default texts
static VALIDATION_REQUIRED_FIELD = 'Field is required';
static VALIDATION_EDITOR_VALID_NUMBER = 'Please enter a valid number';
static VALIDATION_EDITOR_VALID_INTEGER = 'Please enter a valid integer number';
static VALIDATION_EDITOR_INTEGER_BETWEEN = 'Please enter a valid integer number between {{minValue}} and {{maxValue}}';
static VALIDATION_EDITOR_INTEGER_MAX = 'Please enter a valid integer number that is lower than {{maxValue}}';
static VALIDATION_EDITOR_INTEGER_MAX_INCLUSIVE = 'Please enter a valid integer number that is lower than or equal to {{maxValue}}';
static VALIDATION_EDITOR_INTEGER_MIN = 'Please enter a valid integer number that is greater than {{minValue}}';
static VALIDATION_EDITOR_INTEGER_MIN_INCLUSIVE = 'Please enter a valid integer number that is greater than or equal to {{minValue}}';
static VALIDATION_EDITOR_NUMBER_BETWEEN = 'Please enter a valid number between {{minValue}} and {{maxValue}}';
static VALIDATION_EDITOR_NUMBER_MAX = 'Please enter a valid number that is lower than {{maxValue}}';
static VALIDATION_EDITOR_NUMBER_MAX_INCLUSIVE = 'Please enter a valid number that is lower than or equal to {{maxValue}}';
static VALIDATION_EDITOR_NUMBER_MIN = 'Please enter a valid number that is greater than {{minValue}}';
static VALIDATION_EDITOR_NUMBER_MIN_INCLUSIVE = 'Please enter a valid number that is greater than or equal to {{minValue}}';
static VALIDATION_EDITOR_DECIMAL_BETWEEN = 'Please enter a valid number with a maximum of {{maxDecimal}} decimals';
static VALIDATION_EDITOR_TEXT_LENGTH_BETWEEN = 'Please make sure your text length is between {{minLength}} and {{maxLength}} characters';
static VALIDATION_EDITOR_TEXT_MAX_LENGTH = 'Please make sure your text is less than {{maxLength}} characters';
static VALIDATION_EDITOR_TEXT_MAX_LENGTH_INCLUSIVE = 'Please make sure your text is less than or equal to {{maxLength}} characters';
static VALIDATION_EDITOR_TEXT_MIN_LENGTH = 'Please make sure your text is more than {{minLength}} character(s)';
static VALIDATION_EDITOR_TEXT_MIN_LENGTH_INCLUSIVE = 'Please make sure your text is at least {{minLength}} character(s)';
}
const _c0 = ["slickgridHeader"];
const _c1 = ["slickgridFooter"];
function AngularSlickgridComponent_ng_container_3_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementContainer(0);
} }
function AngularSlickgridComponent_ng_container_7_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementContainer(0);
} }
const WARN_NO_PREPARSE_DATE_SIZE = 10000; // data size to warn user when pre-parse isn't enabled
class AngularSlickgridComponent {
angularUtilService;
appRef;
containerService;
elm;
translate;
translaterService;
forRootConfig;
_dataset;
_columns;
_currentDatasetLength = 0;
_darkMode = false;
_eventHandler = new SlickEventHandler();
_eventPubSubService;
_angularGridInstances;
_hideHeaderRowAfterPageLoad = false;
_isAutosizeColsCalled = false;
_isGridInitialized = false;
_isDatasetInitialized = false;
_isDatasetHierarchicalInitialized = false;
_isPaginationInitialized = false;
_isLocalGrid = true;
_paginationOptions;
_registeredResources = [];
_scrollEndCalled = false;
dataView;
slickGrid;
groupingDefinition = {};
groupItemMetadataProvider;
backendServiceApi;
locales;
metrics;
showPagination = false;
serviceList = [];
totalItems = 0;
paginationData;
subscriptions = [];
// components / plugins
slickEmptyWarning;
slickFooter;
slickPagination;
paginationComponent;
slickRowDetailView;
// services
backendUtilityService;
collectionService;
extensionService;
extensionUtility;
filterFactory;
filterService;
gridEventService;
gridService;
gridStateService;
headerGroupingService;
paginationService;
resizerService;
rxjs;
sharedService;
sortService;
treeDataService;
customDataView;
gridId = '';
options = {};
containerClasses = undefined;
get paginationOptions() {
return this._paginationOptions;
}
set paginationOptions(newPaginationOptions) {
if (newPaginationOptions && this._paginationOptions) {
this._paginationOptions = { ...this.options.pagination, ...this._paginationOptions, ...newPaginationOptions };
}
else {
this._paginationOptions = newPaginationOptions;
}
this.options.pagination = this._paginationOptions ?? this.options.pagination;
this.paginationService.updateTotalItems(this.options.pagination?.totalItems ?? 0, true);
}
get columns() {
return this._columns;
}
set columns(columns) {
this._columns = columns;
if (this._isGridInitialized) {
this.updateColumnDefinitionsList(columns);
}
if (columns.length > 0) {
this.copyColumnWidthsReference(columns);
}
}
// make the columnDefinitions a 2-way binding so that plugin adding cols
// are synched on user's side as well (RowMove, RowDetail, RowSelections)
columnsChange = new EventEmitter(true);
// SlickGrid events
onActiveCellChanged = output();
onActiveCellPositionChanged = output();
onAddNewRow = output();
onAutosizeColumns = output();
onBeforeAppendCell = output();
onBeforeCellEditorDestroy = output();
onBeforeColumnsResize = output();
onBeforeDestroy = output();
onBeforeEditCell = output();
onBeforeHeaderCellDestroy = output();
onBeforeHeaderRowCellDestroy = output();
onBeforeFooterRowCellDestroy = output();
onBeforeSetColumns = output();
onBeforeSort = output();
onCellChange = output();
onCellCssStylesChanged = output();
onClick = output();
onColumnsDrag = output();
onColumnsReordered = output();
onColumnsResized = output();
onColumnsResizeDblClick = output();
onCompositeEditorChange = output();
onContextMenu = output();
onDrag = output();
onDragEnd = output();
onDragInit = output();
onDragStart = output();
onDragReplaceCells = output();
onDblClick = output();
onFooterContextMenu = output();
onFooterRowCellRendered = output();
onHeaderCellRendered = output();
onFooterClick = output();
onHeaderClick = output();
onHeaderContextMenu = output();
onHeaderMouseEnter = output();
onHeaderMouseLeave = output();
onHeaderRowCellRendered = output();
onHeaderRowMouseEnter = output();
onHeaderRowMouseLeave = output();
onKeyDown = output();
onMouseEnter = output();
onMouseLeave = output();
onValidationError = output();
onViewportChanged = output();
onRendered = output();
onSelectedRowsChanged = output();
onSetOptions = output();
onScroll = output();
onSort = output();
// DataView events
onBeforePagingInfoChanged = output();
onGroupExpanded = output();
onGroupCollapsed = output();
onPagingInfoChanged = output();
onRowCountChanged = output();
onRowsChanged = output();
onRowsOrCountChanged = output();
onSelectedRowIdsChanged = output();
onSetItemsCalled = output();
// other Slick Events
onAfterMenuShow = output();
onBeforeMenuClose = output();
onBeforeMenuShow = output();
onColumnsChanged = output();
onCommand = output();
onGridMenuColumnsChanged = output();
onMenuClose = output();
onCopyCells = output();
onCopyCancelled = output();
onPasteCells = output();
onBeforePasteCell = output();
// Slickgrid-Universal events
onAfterExportToExcel = output();
onBeforeExportToExcel = output();
onBeforeFilterChange = output();
onBeforeFilterClear = output();
onBeforeSearchChange = output();
onBeforeSortChange = output();
onContextMenuClearGrouping = output();
onContextMenuCollapseAllGroups = output();
onContextMenuExpandAllGroups = output();
onOptionSelected = output();
onColumnPickerColumnsChanged = output();
onGridMenuMenuClose = output();
onGridMenuBeforeMenuShow = output();
onGridMenuAfterMenuShow = output();
onGridMenuClearAllPinning = output();
onGridMenuClearAllFilters = output();
onGridMenuClearAllSorting = output();
onGridMenuCommand = output();
onHeaderButtonCommand = output();
onHeaderMenuCommand = output();
onHeaderMenuColumnResizeByContent = output();
onHeaderMenuBeforeMenuShow = output();
onHeaderMenuAfterMenuShow = output();
onHideColumns = output();
onItemsAdded = output();
onItemsDeleted = output();
onItemsUpdated = output();
onItemsUpserted = output();
onFullResizeByContentRequested = output();
onGridStateChanged = output();
onBeforePaginationChange = output();
onPaginationChanged = output();
onPaginationRefreshed = output();
onPaginationVisibilityChanged = output();
onPaginationSetCursorBased = output();
onGridBeforeResize = output();
onGridAfterResize = output();
onBeforeResizeByContent = output();
onAfterResizeByContent = output();
onSortCleared = output();
onFilterChanged = output();
onFilterCleared = output();
onSortChanged = output();
onTreeItemToggled = output();
onTreeFullToggleEnd = output();
onTreeFullToggleStart = output();
// Angular-Slickgrid specific events
onBeforeGridCreate = output();
onGridCreated = output();
onDataviewCreated = output();
onAngularGridCreated = output();
onBeforeGridDestroy = output();
onLanguageChange = output();
get dataset() {
return (this.customDataView ? this.slickGrid?.getData?.() : this.dataView?.getItems()) || [];
}
set dataset(newDataset) {
const prevDatasetLn = this._currentDatasetLength;
const isDatasetEqual = dequal(newDataset, this._dataset || []);
let data = newDataset;
// when Tree Data is enabled and we don't yet have the hierarchical dataset filled, we can force a convert+sort of the array
if (this.slickGrid &&
this.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 prevDataset was empty then we are on first load
if (this.slickGrid && this.options?.autoFitColumnsOnFirstLoad && prevDatasetLn === 0 && !this._isAutosizeColsCalled) {
this.slickGrid.autosizeColumns();
this._isAutosizeColsCalled = true;
}
this.suggestDateParsingWhenHelpful();
}
get datasetHierarchical() {
return this.sharedService.hierarchicalDataset;
}
set datasetHierarchical(newHierarchicalDataset) {
const isDatasetEqual = dequal(newHierarchicalDataset, this.sharedService?.hierarchicalDataset ?? []);
const prevFlatDatasetLn = this._currentDatasetLength;
this.sharedService.hierarchicalDataset = newHierarchicalDataset;
if (newHierarchicalDataset && this.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.slickGrid && 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;
}
}
get elementRef() {
return this.elm;
}
get backendService() {
return this.options?.backendServiceApi?.service;
}
get eventHandler() {
return this._eventHandler;
}
get gridContainerElement() {
return document.querySelector(`#${this.options.gridContainerId || ''}`);
}
/** GETTER to know if dataset was initialized or not */
get isDatasetInitialized() {
return this._isDatasetInitialized;
}
/** SETTER to change if dataset was initialized or not (stringly used for unit testing purposes) */
set isDatasetInitialized(isInitialized) {
this._isDatasetInitialized = isInitialized;
}
set isDatasetHierarchicalInitialized(isInitialized) {
this._isDatasetHierarchicalInitialized = isInitialized;
}
get registeredResources() {
return this._registeredResources;
}
slickgridHeader = null;
slickgridFooter = null;
constructor(angularUtilService, appRef, containerService, elm, translate, translaterService, forRootConfig, externalServices) {
this.angularUtilService = angularUtilService;
this.appRef = appRef;
this.containerService = containerService;
this.elm = elm;
this.translate = translate;
this.translaterService = translaterService;
this.forRootConfig = forRootConfig;
const slickgridConfig = new SlickgridConfig$1();
// initialize and assign all Service Dependencies
this._eventPubSubService = externalServices?.eventPubSubService ?? new EventPubSubService(this.elm.nativeElement);
this._eventPubSubService.eventNamingStyle = 'camelCase';
this.backendUtilityService = externalServices?.backendUtilityService ?? new BackendUtilityService();
this.gridEventService = externalServices?.gridEventService ?? new GridEventService();
this.sharedService = externalServices?.sharedService ?? new SharedService();
this.collectionService = externalServices?.collectionService ?? new CollectionService(this.translaterService);
// prettier-ignore
this.extensionUtility = externalServices?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.backendUtilityService, this.translaterService);
this.filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService);
// prettier-ignore
this.filterService = externalServices?.filterService ?? new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService);
this.resizerService = externalServices?.resizerService ?? new ResizerService(this._eventPubSubService);
// prettier-ignore
this.sortService = externalServices?.sortService ?? new SortService(this.collectionService, this.sharedService, this._eventPubSubService, this.backendUtilityService);
// prettier-ignore
this.treeDataService = externalServices?.treeDataService ?? new TreeDataService(this._eventPubSubService, this.filterService, this.sharedService, this.sortService);
// prettier-ignore
this.paginationService = externalServices?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService);
this.extensionService =
externalServices?.extensionService ??
new ExtensionService(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService, this.translaterService, () => this.gridService);
// prettier-ignore
/* v8 ignore next 8 */
this.gridStateService = externalServices?.gridStateService ?? new GridStateService(this.extensionService, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService);
// prettier-ignore
/* v8 ignore next 9 */
this.gridService = externalServices?.gridService ?? new GridService(this.gridStateService, this.filterService, this._eventPubSubService, this.paginationService, this.sharedService, this.sortService, this.treeDataService);
this.headerGroupingService = externalServices?.headerGroupingService ?? new HeaderGroupingService(this.extensionUtility);
this.serviceList = [
this.containerService,
this.extensionService,
this.filterService,
this.gridEventService,
this.gridService,
this.gridStateService,
this.headerGroupingService,
this.paginationService,
this.resizerService,
this.sortService,
this.treeDataService,
];
// register all Service instances in the container
this.containerService.registerInstance('ExtensionUtility', this.extensionUtility);
this.containerService.registerInstance('FilterService', this.filterService);
this.containerService.registerInstance('CollectionService', this.collectionService);
this.containerService.registerInstance('ExtensionService', this.extensionService);
this.containerService.registerInstance('GridEventService', this.gridEventService);
this.containerService.registerInstance('GridService', this.gridService);
this.containerService.registerInstance('GridStateService', this.gridStateService);
this.containerService.registerInstance('HeaderGroupingService', this.headerGroupingService);
this.containerService.registerInstance('PaginationService', this.paginationService);
this.containerService.registerInstance('ResizerService', this.resizerService);
this.containerService.registerInstance('SharedService', this.sharedService);
this.containerService.registerInstance('SortService', this.sortService);
this.containerService.registerInstance('EventPubSubService', this._eventPubSubService);
this.containerService.registerInstance('PubSubService', this._eventPubSubService);
this.containerService.registerInstance('TranslaterService', this.translaterService);
this.containerService.registerInstance('TreeDataService', this.treeDataService);
}
ngAfterViewInit() {
if (!this.columns) {
throw new Error('Using `<angular-slickgrid>` requires [columns], it seems that you might have forgot to provide the missing bindable input.');
}
this.initialization(this._eventHandler);
this._isGridInitialized = true;
// recheck the empty warning message after grid is shown so that it works in every use case
if (this.options?.enableEmptyDataWarningMessage && Array.isArray(this.dataset)) {
const finalTotalCount = this.dataset.length;
this.displayEmptyDataWarning(finalTotalCount < 1);
}
// add dark mode CSS class when enabled
if (this.options.darkMode) {
this.setDarkMode(true);
}
this.suggestDateParsingWhenHelpful();
}
ngOnDestroy() {
this._eventPubSubService.publish('onBeforeGridDestroy', this.slickGrid);
this.destroy();
}
destroy(shouldEmptyDomElementContainer = false) {
// dispose of all Services
this.serviceList.forEach((service) => {
if (typeof service?.dispose === 'function') {
service.dispose();
}
});
this.serviceList.length = 0;
this._eventPubSubService?.unsubscribeAll();
// dispose backend service when defined and a dispose method exists
this.backendService?.dispose?.();
// dispose all registered external resources
this.disposeExternalResources();
// dispose the Components
this.slickEmptyWarning?.dispose();
this.slickFooter?.dispose();
this.slickPagination?.dispose();
if (this._eventHandler?.unsubscribeAll) {
this._eventHandler.unsubscribeAll();
}
if (this.dataView) {
this.dataView.setItems([]);
this.dataView.destroy();
}
if (this.slickGrid?.destroy) {
this.slickGrid.destroy(shouldEmptyDomElementContainer);
}
if (this.backendServiceApi) {
for (const prop of Object.keys(this.backendServiceApi)) {
delete this.backendServiceApi[prop];
}
this.backendServiceApi = undefined;
}
if (this.columns) {
for (const prop of Object.keys(this.columns)) {
this.columns[prop] = null;
}
}
for (const prop of Object.keys(this.sharedService)) {
this.sharedService[prop] = null;
}
// also unsubscribe all RxJS subscriptions
this.subscriptions = unsubscribeAll(this.subscriptions);
this._dataset = null;
this.datasetHierarchical = undefined;
this._columns = [];
this._angularGridInstances = undefined;
this.slickGrid = undefined;
// we could optionally also empty the content of the grid container DOM element
if (shouldEmptyDomElementContainer) {
this.emptyGridContainerElm();
}
}
disposeExternalResources() {
if (Array.isArray(this._registeredResources)) {
while (this._registeredResources.length > 0) {
const res = this._registeredResources.pop();
if (typeof res?.dispose === 'function') {
res.dispose();
}
}
}
this._registeredResources = [];
}
emptyGridContainerElm() {
const gridContainerId = this.options?.gridContainerId || 'grid1';
const gridContainerElm = document.querySelector(`#${gridContainerId}`);
emptyElement(gridContainerElm);
}
/**
* Define our internal Post Process callback, it will execute internally after we get back result from the Process backend call
* Currently ONLY available with the GraphQL Backend Service.
* The behavior is to refresh the Dataset & Pagination without requiring the user to create his own PostProcess every time
*/
createBackendApiInternalPostProcessCallback(gridOptions) {
const backendApi = gridOptions?.backendServiceApi;
if (backendApi?.service) {
const backendApiService = backendApi.service;
// internalPostProcess only works (for now) with a GraphQL Service, so make sure it is of that type
if (typeof backendApiService.getDatasetName === 'function') {
backendApi.internalPostProcess = (processResult) => {
// prettier-ignore
const datasetName = backendApi && backendApiService && typeof backendApiService.getDatasetName === 'function' ? backendApiService.getDatasetName() : '';
if (!Array.isArray(processResult) && processResult?.data[datasetName]) {
const data = 'nodes' in processResult.data[datasetName]
? processResult.data[datasetName].nodes
: processResult.data[datasetName];
const totalCount = 'totalCount' in processResult.data[datasetName]
? processResult.data[datasetName].totalCount
: processResult.data[datasetName].length;
this.refreshGridData(data, totalCount || 0);
}
};
}
}
}
initialization(eventHandler) {
this.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.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?.inlineFilters ?? false;
let dataViewOptions = { ...this.options.dataView, inlineFilters: dataviewInlineFilters };
if (this.options.draggableGrouping || this.options.enableGrouping) {
this.groupItemMetadataProvider = new SlickGroupItemMetadataProvider(this.options.groupItemMetadataOption);
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.gridStateService.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;
// before certain extentions/plugins potentially adds extra columns not created by the user itself (RowMove, RowDetail, RowSelections)
// we'll subscribe to the event and push back the change to the user so they always use full column defs array including extra cols
this.subscriptions.push(this._eventPubSubService.subscribe('onPluginColumnsChanged', (data) => {
this._columns = data.columns;
this.columnsChange.emit(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.slickGrid = new SlickGrid(`#${this.gridId}`, this.customDataView || this.dataView, this._columns, this.options, this._eventPubSubService);
if (typeof this.dataView.setGrid === 'function') {
this.dataView.setGrid(this.slickGrid);
}
this.sharedService.dataView = this.dataView;
this.sharedService.slickGrid = this.slickGrid;
this.sharedService.gridContainerElement = this.elm.nativeElement;
if (this.groupItemMetadataProvider) {
this.slickGrid.registerPlugin(this.groupItemMetadataProvider); // register GroupItemMetadataProvider when Grouping is enabled
}
// get any possible Services that user want to register
this.registerResources();
this.extensionService.bindDifferentExtensions();
this.bindDifferentHooks(this.slickGrid, 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.slickGrid.getFrozenColumnId();
// initialize the SlickGrid grid
this.slickGrid.init();
// initialized the resizer service only after SlickGrid is initialized
// if we don't we end up binding our resize to a grid element that doesn't yet exist in the DOM and the resizer service will fail silently (because it has a try/catch that unbinds the resize without throwing back)
if (this.gridContainerElement) {
this.resizerService.init(this.slickGrid, this.gridContainerElement);
}
// user could show a custom footer with the data metrics (dataset length and last updated timestamp)
if (!this.options.enablePagination && this.options.showCustomFooter && this.options.customFooterOptions && this.gridContainerElement) {
this.slickFooter = new SlickFooterComponent(this.slickGrid, this.options.customFooterOptions, this._eventPubSubService, this.translaterService);
this.slickFooter.renderFooter(this.gridContainerElement);
}
if (!this.customDataView && this.dataView) {
// load the data in the DataView (unless it's a hierarchical dataset, if so it will be loaded after the initial tree sort)
const initialDataset = this.options?.enableTreeData ? this.sortTreeDataset(this._dataset) : this._dataset;
this.dataView.beginUpdate();
this.dataView.setItems(initialDataset || [], this.options.datasetIdPropertyName ?? 'id');
this.dataView.endUpdate();
// if you don't want the items that are not visible (due to being filtered out or being on a different page)
// to stay selected, pass 'false' to the second arg
if (this.slickGrid?.getSelectionModel() && this.options?.dataView && 'syncGridSelection' in this.options.dataView) {
// if we are using a Backend Service, we will do an extra flag check, the reason is because it might have some unintended behaviors
// with the BackendServiceApi because technically the data in the page changes the DataView on every page change.
let preservedRowSelectionWithBackend = false;
if (this.options.backendServiceApi && 'syncGridSelectionWithBackendService' in this.options.dataView) {
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.slickGrid, preservedRowSelection);
}
else if (typeof syncGridSelection === 'object') {
this.dataView.syncGridSelection(this.slickGrid, syncGridSelection.preserveHidden, syncGridSelection.preserveHiddenOnSelectionChange);
}
}
const datasetLn = this.dataView.getLength() || this._dataset?.length || 0;
if (datasetLn > 0) {
if (!this._isDatasetInitialized && (this.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.slickGrid);
// after the DataView is created & updated execute some processes
if (!this.customDataView) {
this.executeAfterDataviewCreated(this.slickGrid, this.options);
}
// bind resize ONLY after the dataView is ready
this.bindResizeHook(this.slickGrid, 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);
}
// 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(this.dataset);
}
this._angularGridInstances = {
// Slick Grid & DataView objects
dataView: this.dataView,
slickGrid: this.slickGrid,
extensions: this.extensionService?.extensionList,
// public methods
destroy: this.destroy.bind(this),
// return all available Services (non-singleton)
backendService: this.backendService,
eventPubSubService: this._eventPubSubService,
filterService: this.filterService,
gridEventService: this.gridEventService,
gridStateService: this.gridStateService,
gridService: this.gridService,
headerGroupingService: this.headerGroupingService,
extensionService: this.extensionService,
paginationComponent: this.slickPagination,
paginationService: this.paginationService,
resizerService: this.resizerService,
sortService: this.sortService,
treeDataService: this.treeDataService,
};
// all instances (SlickGrid, DataView & all Services)
this._eventPubSubService.publish('onAngularGridCreated', this._angularGridInstances);
}
/**
* On a Pagination changed, we will trigger a Grid State changed with the new pagination info
* Also if we use Row Selection or the Checkbox Selector with a Backend Service (Odata, GraphQL), we need to reset any selection