diff --git a/assets/i18n/en/aurelia-slickgrid.json b/assets/i18n/en/aurelia-slickgrid.json
index e46ec3e35..9d281256e 100644
--- a/assets/i18n/en/aurelia-slickgrid.json
+++ b/assets/i18n/en/aurelia-slickgrid.json
@@ -24,6 +24,7 @@
"IN_COLLECTION_SEPERATED_BY_COMMA": "Search items in a collection, must be separated by a comma (a,b)",
"ITEMS": "items",
"ITEMS_PER_PAGE": "items per page",
+ "LAST_UPDATE": "Last Update",
"NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Search items not in a collection, must be separated by a comma (a,b)",
"OF": "of",
"OK": "OK",
diff --git a/assets/i18n/fr/aurelia-slickgrid.json b/assets/i18n/fr/aurelia-slickgrid.json
index f5ac336e0..e2379555c 100644
--- a/assets/i18n/fr/aurelia-slickgrid.json
+++ b/assets/i18n/fr/aurelia-slickgrid.json
@@ -24,6 +24,7 @@
"INVALID_FLOAT": "Le nombre doit être valide et avoir un maximum de {{maxDecimal}} décimales.",
"ITEMS": "éléments",
"ITEMS_PER_PAGE": "éléments par page",
+ "LAST_UPDATE": "Dernière mise à jour",
"NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche excluant certain éléments d'une collection, doit être séparé par une virgule (a,b)",
"OF": "de",
"OK": "Terminé",
diff --git a/src/assets/i18n/en/aurelia-slickgrid.json b/src/assets/i18n/en/aurelia-slickgrid.json
index e46ec3e35..9d281256e 100644
--- a/src/assets/i18n/en/aurelia-slickgrid.json
+++ b/src/assets/i18n/en/aurelia-slickgrid.json
@@ -24,6 +24,7 @@
"IN_COLLECTION_SEPERATED_BY_COMMA": "Search items in a collection, must be separated by a comma (a,b)",
"ITEMS": "items",
"ITEMS_PER_PAGE": "items per page",
+ "LAST_UPDATE": "Last Update",
"NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Search items not in a collection, must be separated by a comma (a,b)",
"OF": "of",
"OK": "OK",
diff --git a/src/assets/i18n/fr/aurelia-slickgrid.json b/src/assets/i18n/fr/aurelia-slickgrid.json
index f5ac336e0..e2379555c 100644
--- a/src/assets/i18n/fr/aurelia-slickgrid.json
+++ b/src/assets/i18n/fr/aurelia-slickgrid.json
@@ -24,6 +24,7 @@
"INVALID_FLOAT": "Le nombre doit être valide et avoir un maximum de {{maxDecimal}} décimales.",
"ITEMS": "éléments",
"ITEMS_PER_PAGE": "éléments par page",
+ "LAST_UPDATE": "Dernière mise à jour",
"NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche excluant certain éléments d'une collection, doit être séparé par une virgule (a,b)",
"OF": "de",
"OK": "Terminé",
diff --git a/src/aurelia-slickgrid/constants.ts b/src/aurelia-slickgrid/constants.ts
index f81e30e0a..131b3d884 100644
--- a/src/aurelia-slickgrid/constants.ts
+++ b/src/aurelia-slickgrid/constants.ts
@@ -25,6 +25,7 @@ export class Constants {
TEXT_ITEMS_PER_PAGE: 'items per page',
TEXT_OF: 'of',
TEXT_OK: 'OK',
+ TEXT_LAST_UPDATE: 'Last Update',
TEXT_PAGE: 'Page',
TEXT_REFRESH_DATASET: 'Refresh Dataset',
TEXT_REMOVE_FILTER: 'Remove Filter',
diff --git a/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts b/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts
index 931f4b40a..0acf0b1c5 100644
--- a/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts
+++ b/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid-constructor.spec.ts
@@ -1,8 +1,10 @@
import 'jest-extended';
+import { BindingEngine, Container } from 'aurelia-framework';
+import { BindingSignaler } from 'aurelia-templating-resources';
import { EventAggregator } from 'aurelia-event-aggregator';
import { HttpClient } from 'aurelia-fetch-client';
-import { BindingEngine, Container } from 'aurelia-framework';
import { DOM } from 'aurelia-pal';
+import { I18N } from 'aurelia-i18n';
import { AureliaSlickgridCustomElement } from '../aurelia-slickgrid';
import { ExtensionUtility } from '../../extensions';
@@ -21,7 +23,18 @@ import {
SharedService,
SortService,
} from '../../services';
-import { Column, CurrentFilter, CurrentSorter, GraphqlServiceApi, GraphqlServiceOption, GridOption, GridState, GridStateType, Pagination } from '../../models';
+import {
+ Column,
+ CurrentFilter,
+ CurrentSorter,
+ GraphqlPaginatedResult,
+ GraphqlServiceApi,
+ GraphqlServiceOption,
+ GridOption,
+ GridState,
+ GridStateType,
+ Pagination
+} from '../../models';
import { Filters } from '../../filters';
import { Editors } from '../../editors';
import * as utilities from '../../services/backend-utilities';
@@ -168,7 +181,7 @@ const mockDraggableGrouping = {
constructor: jest.fn(),
init: jest.fn(),
destroy: jest.fn(),
-}
+};
const mockSlickCore = {
handlers: [],
@@ -273,6 +286,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
let divContainer: HTMLDivElement;
let cellDiv: HTMLDivElement;
let ea: EventAggregator;
+ let i18n: I18N;
const http = new HttpStub();
const template = `
@@ -295,6 +309,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
document.body.appendChild(divContainer);
ea = new EventAggregator();
+ i18n = new I18N(ea, new BindingSignaler());
container = new Container();
customElement = new AureliaSlickgridCustomElement(
bindingEngineStub,
@@ -310,12 +325,23 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
gridServiceStub,
gridStateServiceStub,
groupingAndColspanServiceStub,
+ i18n,
paginationServiceStub,
resizerServiceStub,
sharedService,
sortServiceStub
);
+ i18n.setup({
+ resources: {
+ en: { translation: { ITEMS: 'items', OF: 'of', } },
+ fr: { translation: { ITEMS: 'éléments', OF: 'de', } }
+ },
+ lng: 'fr',
+ fallbackLng: 'en',
+ debug: false
+ });
+
customElement.gridId = 'grid1';
customElement.columnDefinitions = [{ id: 'name', field: 'name' }];
customElement.dataset = [];
@@ -731,7 +757,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
customElement.bind();
customElement.attached();
- customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { users: { nodes: [{ firstName: 'John' }], pageInfo: { hasNextPage: false }, totalCount: 2 } } });
+ customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { users: { nodes: [{ firstName: 'John' }], totalCount: 2 } } } as GraphqlPaginatedResult);
expect(spy).toHaveBeenCalled();
expect(customElement.gridOptions.backendServiceApi.internalPostProcess).toEqual(expect.any(Function));
@@ -757,7 +783,7 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
customElement.bind();
customElement.attached();
- customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { notUsers: { nodes: [{ firstName: 'John' }], pageInfo: { hasNextPage: false }, totalCount: 2 } } });
+ customElement.gridOptions.backendServiceApi.internalPostProcess({ data: { notUsers: { nodes: [{ firstName: 'John' }], totalCount: 2 } } } as GraphqlPaginatedResult);
expect(spy).not.toHaveBeenCalled();
expect(customElement.dataset).toEqual([]);
@@ -1219,5 +1245,54 @@ describe('Aurelia-Slickgrid Custom Component instantiated via Constructor', () =
});
});
});
+
+ describe('Custom Footer', () => {
+ it('should have a Custom Footer when "showCustomFooter" is enabled and there are no Pagination used', (done) => {
+ const mockColDefs = [{ id: 'name', field: 'name', editor: undefined, internalColumnEditor: {} }];
+
+ customElement.gridOptions.enableTranslate = true;
+ customElement.gridOptions.showCustomFooter = true;
+ customElement.bind();
+ customElement.attached();
+ customElement.columnDefinitions = mockColDefs;
+
+ setTimeout(() => {
+ expect(customElement.columnDefinitions).toEqual(mockColDefs);
+ expect(customElement.showCustomFooter).toBeTrue();
+ expect(customElement.customFooterOptions).toEqual({
+ dateFormat: 'YYYY-DD-MM h:mm:ss a',
+ hideLastUpdateTimestamp: true,
+ hideTotalItemCount: false,
+ footerHeight: 20,
+ leftContainerClass: 'col-xs-12 col-sm-5',
+ metricSeparator: '|',
+ metricTexts: {
+ items: 'éléments',
+ itemsKey: 'ITEMS',
+ of: 'de',
+ ofKey: 'OF',
+ },
+ rightContainerClass: 'col-xs-6 col-sm-7',
+ });
+ done();
+ }, 1);
+ });
+
+ it('should NOT have a Custom Footer when "showCustomFooter" is enabled WITH Pagination in use', (done) => {
+ const mockColDefs = [{ id: 'name', field: 'name', editor: undefined, internalColumnEditor: {} }];
+
+ customElement.gridOptions.enablePagination = true;
+ customElement.gridOptions.showCustomFooter = true;
+ customElement.bind();
+ customElement.attached();
+ customElement.columnDefinitions = mockColDefs;
+
+ setTimeout(() => {
+ expect(customElement.columnDefinitions).toEqual(mockColDefs);
+ expect(customElement.showCustomFooter).toBeFalse();
+ done();
+ }, 1);
+ });
+ });
});
});
diff --git a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html
index 253673524..29b3c46a5 100644
--- a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html
+++ b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.html
@@ -1,13 +1,40 @@
+ focusout.delegate="commitEdit($event.target)">
+
+ asg-on-pagination-changed.delegate="paginationChanged($event.detail)" dataview.bind="dataview"
+ grid.bind="grid"
+ enable-translate.bind="gridOptions.enableTranslate" options.bind="paginationOptions"
+ locales.bind="locales"
+ total-items.bind="totalItems" backend-service-api.bind="backendServiceApi">
+
+
+
diff --git a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts
index 63f75a416..cd4dd52a7 100644
--- a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts
+++ b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts
@@ -8,8 +8,9 @@ import 'slickgrid/slick.dataview';
import 'slickgrid/slick.grid';
import { bindable, BindingEngine, bindingMode, Container, Factory, inject } from 'aurelia-framework';
-import { DOM } from 'aurelia-pal';
import { EventAggregator, Subscription } from 'aurelia-event-aggregator';
+import { DOM } from 'aurelia-pal';
+import { I18N } from 'aurelia-i18n';
import { Constants } from '../constants';
import { GlobalGridOptions } from '../global-grid-options';
@@ -18,6 +19,7 @@ import {
BackendServiceApi,
BackendServiceOption,
Column,
+ CustomFooterOption,
ExtensionName,
GraphqlResult,
GraphqlPaginatedResult,
@@ -25,6 +27,7 @@ import {
GridStateChange,
GridStateType,
Locale,
+ Metrics,
Pagination,
SlickEventHandler,
} from '../models/index';
@@ -69,6 +72,7 @@ const DEFAULT_SLICKGRID_EVENT_PREFIX = 'sg';
GridService,
GridStateService,
GroupingAndColspanService,
+ I18N,
PaginationService,
ResizerService,
SharedService,
@@ -83,8 +87,11 @@ export class AureliaSlickgridCustomElement {
private _hideHeaderRowAfterPageLoad = false;
groupItemMetadataProvider: any;
backendServiceApi: BackendServiceApi | undefined;
+ customFooterOptions: CustomFooterOption;
locales: Locale;
+ metrics: Metrics;
isGridInitialized = false;
+ showCustomFooter = false;
showPagination = false;
serviceList: any[] = [];
subscriptions: Subscription[] = [];
@@ -117,6 +124,7 @@ export class AureliaSlickgridCustomElement {
private gridService: GridService,
private gridStateService: GridStateService,
private groupingAndColspanService: GroupingAndColspanService,
+ private i18n: I18N,
private paginationService: PaginationService,
private resizerService: ResizerService,
private sharedService: SharedService,
@@ -290,6 +298,10 @@ export class AureliaSlickgridCustomElement {
resizerService: this.resizerService,
sortService: this.sortService,
};
+
+ // user could show a custom footer with the data metrics (dataset length and last updated timestamp)
+ this.optionallyShowCustomFooterWithMetrics();
+
this.dispatchCustomEvent(`${DEFAULT_AURELIA_EVENT_PREFIX}-on-aurelia-grid-created`, aureliaElementInstance);
}
@@ -430,6 +442,7 @@ export class AureliaSlickgridCustomElement {
this.extensionService.translateContextMenu();
this.extensionService.translateGridMenu();
this.extensionService.translateHeaderMenu();
+ this.translateCustomFooterTexts();
}
})
);
@@ -519,7 +532,16 @@ export class AureliaSlickgridCustomElement {
this.gridEventService.bindOnClick(grid, dataView);
if (dataView && grid) {
- this._eventHandler.subscribe(dataView.onRowCountChanged, () => grid.invalidate());
+ this._eventHandler.subscribe(dataView.onRowCountChanged, (e: Event, args: any) => {
+ grid.invalidate();
+
+ this.metrics = {
+ startTime: new Date(),
+ endTime: new Date(),
+ itemCount: args && args.current || 0,
+ totalItemCount: this.dataset.length || 0
+ };
+ });
// without this, filtering data with local dataset will not always show correctly
// also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row
@@ -790,6 +812,32 @@ export class AureliaSlickgridCustomElement {
}
}
+ /**
+ * We could optionally display a custom footer below the grid to show some metrics (last update, item count with/without filters)
+ * It's an opt-in, user has to enable "showCustomFooter" and it cannot be used when there's already a Pagination since they display the same kind of info
+ */
+ private optionallyShowCustomFooterWithMetrics() {
+ if (this.gridOptions) {
+ setTimeout(() => {
+ // we will display the custom footer only when there's no Pagination
+ if (!(this.gridOptions.backendServiceApi || this.gridOptions.enablePagination)) {
+ this.showCustomFooter = this.gridOptions.hasOwnProperty('showCustomFooter') ? this.gridOptions.showCustomFooter : false;
+ this.customFooterOptions = this.gridOptions.customFooterOptions || {};
+ }
+ });
+
+ if ((this.gridOptions.enableTranslate || this.gridOptions.i18n)) {
+ this.translateCustomFooterTexts();
+ } else if (this.gridOptions.customFooterOptions) {
+ const customFooterOptions = this.gridOptions.customFooterOptions;
+ customFooterOptions.metricTexts = customFooterOptions.metricTexts || {};
+ customFooterOptions.metricTexts.lastUpdate = this.locales && this.locales.TEXT_LAST_UPDATE || 'TEXT_LAST_UPDATE';
+ customFooterOptions.metricTexts.items = this.locales && this.locales.TEXT_ITEMS || 'TEXT_ITEMS';
+ customFooterOptions.metricTexts.of = this.locales && this.locales.TEXT_OF || 'TEXT_OF';
+ }
+ }
+ }
+
/**
* For convenience to the user, we provide the property "editor" as an Aurelia-Slickgrid editor complex object
* however "editor" is used internally by SlickGrid for it's own Editor Factory
@@ -811,6 +859,20 @@ export class AureliaSlickgridCustomElement {
});
}
+ /** Translate all Custom Footer Texts (footer with metrics) */
+ private translateCustomFooterTexts() {
+ if (this.i18n && this.i18n.tr) {
+ const customFooterOptions = this.gridOptions && this.gridOptions.customFooterOptions || {};
+ customFooterOptions.metricTexts = customFooterOptions.metricTexts || {};
+ for (const propName of Object.keys(customFooterOptions.metricTexts)) {
+ if (propName.lastIndexOf('Key') > 0) {
+ const propNameWithoutKey = propName.substring(0, propName.lastIndexOf('Key'));
+ customFooterOptions.metricTexts[propNameWithoutKey] = this.i18n.tr(customFooterOptions.metricTexts[propName] || ' ');
+ }
+ }
+ }
+ }
+
/**
* Update the "internalColumnEditor.collection" property.
* Since this is called after the async call resolves, the pointer will not be the same as the "column" argument passed.
diff --git a/src/aurelia-slickgrid/global-grid-options.ts b/src/aurelia-slickgrid/global-grid-options.ts
index 433f53cfc..5a1dfb8b2 100644
--- a/src/aurelia-slickgrid/global-grid-options.ts
+++ b/src/aurelia-slickgrid/global-grid-options.ts
@@ -52,6 +52,21 @@ export const GlobalGridOptions: GridOption = {
iconExportTextDelimitedCommand: 'fa fa-download',
width: 200,
},
+ customFooterOptions: {
+ dateFormat: 'YYYY-DD-MM h:mm:ss a',
+ hideTotalItemCount: false,
+ hideLastUpdateTimestamp: true,
+ footerHeight: 20,
+ leftContainerClass: 'col-xs-12 col-sm-5',
+ rightContainerClass: 'col-xs-6 col-sm-7',
+ metricSeparator: '|',
+ metricTexts: {
+ items: 'items',
+ of: 'of',
+ itemsKey: 'ITEMS',
+ ofKey: 'OF',
+ }
+ },
datasetIdPropertyName: 'id',
defaultAureliaEventPrefix: 'asg',
defaultSlickgridEventPrefix: 'sg',
diff --git a/src/aurelia-slickgrid/index.ts b/src/aurelia-slickgrid/index.ts
index b6b2688a9..72b3d572e 100644
--- a/src/aurelia-slickgrid/index.ts
+++ b/src/aurelia-slickgrid/index.ts
@@ -20,6 +20,7 @@ export * from './sorters/index';
export function configure(aurelia: FrameworkConfiguration, callback: (instance: SlickgridConfig) => void) {
aurelia.globalResources(PLATFORM.moduleName('./custom-elements/aurelia-slickgrid'));
aurelia.globalResources(PLATFORM.moduleName('./custom-elements/slick-pagination'));
+ aurelia.globalResources(PLATFORM.moduleName('./value-converters/asgDateFormat'));
aurelia.globalResources(PLATFORM.moduleName('./value-converters/asgNumber'));
// must register a transient so the container will get a new instance everytime
diff --git a/src/aurelia-slickgrid/models/customFooterOption.interface.ts b/src/aurelia-slickgrid/models/customFooterOption.interface.ts
new file mode 100644
index 000000000..94f77af6a
--- /dev/null
+++ b/src/aurelia-slickgrid/models/customFooterOption.interface.ts
@@ -0,0 +1,51 @@
+export interface CustomFooterOption {
+ /** Optionally pass some text to be displayed on the left side (in the "left-footer" css class) */
+ leftFooterText?: string;
+
+ /** CSS class used for the left container */
+ leftContainerClass?: string;
+
+ /** Date format used when showing the "Last Update" timestamp in the metrics section. */
+ dateFormat?: string;
+
+ /** Defaults to 20, height of the Custom Footer in pixels (this is required and is used by the auto-resizer) */
+ footerHeight?: number;
+
+ /** Defaults to false, do we want to hide the last update timestamp (endTime)? */
+ hideLastUpdateTimestamp?: boolean;
+
+ /** Defaults to false, do we want to hide the metrics when the footer is displayed? */
+ hideMetrics?: boolean;
+
+ /** Defaults to false, do we want to hide the total item count of the entire dataset (the count exclude any filtered data) */
+ hideTotalItemCount?: boolean;
+
+ /** Defaults to "|", separator between the timestamp and the total count */
+ metricSeparator?: string;
+
+ /** Text shown in the custom footer on the far right for the metrics */
+ metricTexts?: {
+ /** Defaults to empty string, optionally pass a text (Last Update) to display before the metrics endTime timestamp. */
+ lastUpdate?: string;
+
+ /** Defaults to "items", word to display at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */
+ items?: string;
+
+ /** Defaults to "of", text word separator to display between the filtered items count and the total unfiltered items count (e.g.: "10 of 100 items"). */
+ of?: string;
+
+ // -- Translation Keys --//
+
+ /** Defaults to "ITEMS", translation key used for the word displayed at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */
+ itemsKey?: string;
+
+ /** Defaults to empty string, optionally pass a translation key (internally we use "LAST_UPDATE") to display before the metrics endTime timestamp. */
+ lastUpdateKey?: string;
+
+ /** Defaults to "OF", translation key used for the to display between the filtered items count and the total unfiltered items count. */
+ ofKey?: string;
+ };
+
+ /** CSS class used for the right container */
+ rightContainerClass?: string;
+}
diff --git a/src/aurelia-slickgrid/models/gridOption.interface.ts b/src/aurelia-slickgrid/models/gridOption.interface.ts
index 1dccfc667..cd0cfd4ba 100644
--- a/src/aurelia-slickgrid/models/gridOption.interface.ts
+++ b/src/aurelia-slickgrid/models/gridOption.interface.ts
@@ -6,6 +6,7 @@ import {
Column,
ColumnPicker,
CheckboxSelector,
+ CustomFooterOption,
DraggableGrouping,
EditCommand,
ExcelCopyBufferOption,
@@ -101,6 +102,9 @@ export interface GridOption {
/** Default to false, which leads to create an extra pre-header panel (on top of column header) for column grouping purposes */
createPreHeaderPanel?: boolean;
+ /** Custom Footer Options */
+ customFooterOptions?: CustomFooterOption;
+
/** Data item column value extractor (getter) that can be used by the Excel like copy buffer plugin */
dataItemColumnValueExtractor?: (item: any, columnDef: Column) => any;
@@ -378,12 +382,21 @@ export interface GridOption {
/** Do we want to show cell selection? */
showCellSelection?: boolean;
+ /**
+ * Do we want to show a custom footer with some metrics?
+ * By default it will show how many items are in the dataset and when was last update done (timestamp)
+ */
+ showCustomFooter?: boolean;
+
/** Do we want to show the footer row? */
showFooterRow?: boolean;
/** Do we want to show header row? */
showHeaderRow?: boolean;
+ /** Do we want to show metrics in custom footer? (dataset length, data filtered, last update timestamp) */
+ showFooterMetrics?: boolean;
+
/** Do we want to show the extra pre-header panel (on top of column header) for column grouping purposes */
showPreHeaderPanel?: boolean;
diff --git a/src/aurelia-slickgrid/models/index.ts b/src/aurelia-slickgrid/models/index.ts
index 3c1c99c86..f1c384021 100644
--- a/src/aurelia-slickgrid/models/index.ts
+++ b/src/aurelia-slickgrid/models/index.ts
@@ -26,6 +26,7 @@ export * from './currentColumn.interface';
export * from './currentFilter.interface';
export * from './currentPagination.interface';
export * from './currentSorter.interface';
+export * from './customFooterOption.interface';
export * from './delimiterType.enum';
export * from './draggableGrouping.interface';
export * from './editCommand.interface';
diff --git a/src/aurelia-slickgrid/models/locale.interface.ts b/src/aurelia-slickgrid/models/locale.interface.ts
index 47c7124d3..211ad85a5 100644
--- a/src/aurelia-slickgrid/models/locale.interface.ts
+++ b/src/aurelia-slickgrid/models/locale.interface.ts
@@ -65,6 +65,9 @@ export interface Locale {
/** Text "items per page" displayed in the Pagination (when enabled) */
TEXT_ITEMS_PER_PAGE?: string;
+ /** Text "Last Update" displayed in the Footer (when enabled) */
+ TEXT_LAST_UPDATE?: string;
+
/** Text "of" displayed in the Pagination (when enabled) */
TEXT_OF?: string;
diff --git a/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts b/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts
index d6d418ccb..d6f9cb7b8 100644
--- a/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts
+++ b/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts
@@ -6,6 +6,7 @@ import { ResizerService } from '../resizer.service';
const DATAGRID_MIN_HEIGHT = 180;
const DATAGRID_MIN_WIDTH = 300;
const DATAGRID_BOTTOM_PADDING = 20;
+const DATAGRID_FOOTER_HEIGHT = 20;
const DATAGRID_PAGINATION_HEIGHT = 35;
const aureliaEventPrefix = 'asg';
const gridId = 'grid1';
@@ -87,6 +88,10 @@ describe('Resizer Service', () => {
gridOptionMock.gridId = 'grid1';
});
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
it('should return null when calling "bindAutoResizeDataGrid" method with a gridId that is not found in the DOM', () => {
gridOptionMock.gridId = 'unknown';
const output = service.bindAutoResizeDataGrid();
@@ -161,6 +166,39 @@ describe('Resizer Service', () => {
expect(serviceCalculateSpy).toReturnWith({ height: (newHeight - DATAGRID_BOTTOM_PADDING), width: fixedWidth });
});
+ it('should calculate new dimensions, minus the custom footer height, when calculateGridNewDimensions is called', () => {
+ const newHeight = 440;
+ const fixedWidth = 800;
+ const newOptions = { ...gridOptionMock, enablePagination: false, showCustomFooter: true } as GridOption;
+ const newGridStub = { ...gridStub, getOptions: () => newOptions };
+ service.init(newGridStub, { width: fixedWidth });
+ const serviceCalculateSpy = jest.spyOn(service, 'calculateGridNewDimensions');
+
+ Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: newHeight });
+ window.dispatchEvent(DOM.createCustomEvent('resize', { bubbles: true }));
+ service.calculateGridNewDimensions(newOptions);
+
+ // same comment as previous test, the height dimension will work because calculateGridNewDimensions() uses "window.innerHeight"
+ expect(serviceCalculateSpy).toReturnWith({ height: (newHeight - DATAGRID_BOTTOM_PADDING - DATAGRID_FOOTER_HEIGHT), width: fixedWidth });
+ });
+
+ it('should calculate new dimensions, minus the custom footer height passed in grid options, when calculateGridNewDimensions is called', () => {
+ const newHeight = 440;
+ const fixedWidth = 800;
+ const footerHeight = 25;
+ const newOptions = { ...gridOptionMock, enablePagination: false, showCustomFooter: true, customFooterOptions: { footerHeight } } as GridOption;
+ const newGridStub = { ...gridStub, getOptions: () => newOptions };
+ service.init(newGridStub, { width: fixedWidth });
+ const serviceCalculateSpy = jest.spyOn(service, 'calculateGridNewDimensions');
+
+ Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: newHeight });
+ window.dispatchEvent(DOM.createCustomEvent('resize', { bubbles: true }));
+ service.calculateGridNewDimensions(newOptions);
+
+ // same comment as previous test, the height dimension will work because calculateGridNewDimensions() uses "window.innerHeight"
+ expect(serviceCalculateSpy).toReturnWith({ height: (newHeight - DATAGRID_BOTTOM_PADDING - footerHeight), width: fixedWidth });
+ });
+
it('should use maxHeight when new dimensions are higher than maximum defined', () => {
const newHeight = 1000;
const fixedWidth = 800;
diff --git a/src/aurelia-slickgrid/services/resizer.service.ts b/src/aurelia-slickgrid/services/resizer.service.ts
index b03710199..dc45dbab4 100644
--- a/src/aurelia-slickgrid/services/resizer.service.ts
+++ b/src/aurelia-slickgrid/services/resizer.service.ts
@@ -8,6 +8,7 @@ import * as $ from 'jquery';
const DATAGRID_MIN_HEIGHT = 180;
const DATAGRID_MIN_WIDTH = 300;
const DATAGRID_BOTTOM_PADDING = 20;
+const DATAGRID_FOOTER_HEIGHT = 20;
const DATAGRID_PAGINATION_HEIGHT = 35;
export interface GridDimension {
@@ -92,6 +93,11 @@ export class ResizerService {
bottomPadding += DATAGRID_PAGINATION_HEIGHT;
}
+ // optionally show a custom footer with the data metrics (dataset length and last updated timestamp)
+ if (bottomPadding && gridOptions.showCustomFooter) {
+ bottomPadding += gridOptions.customFooterOptions && gridOptions.customFooterOptions.footerHeight || DATAGRID_FOOTER_HEIGHT;
+ }
+
let gridHeight = 0;
let gridOffsetTop = 0;
diff --git a/src/aurelia-slickgrid/styles/_variables.scss b/src/aurelia-slickgrid/styles/_variables.scss
index 805796d9c..42e3c223c 100644
--- a/src/aurelia-slickgrid/styles/_variables.scss
+++ b/src/aurelia-slickgrid/styles/_variables.scss
@@ -498,3 +498,22 @@ $viewport-border-top: 0 none !default;
$viewport-border-right: 0 none !default;
$viewport-border-bottom: 0 none !default;
$viewport-border-left: 0 none !default;
+
+/* Custom Footer */
+$footer-bg-color: transparent !default;
+$footer-font-style: italic !default;
+$footer-font-weight: normal !default;
+$footer-height: 30px !default; // if you modify this height, you also have to modify the footerHeight in the customFooterOptions
+$footer-padding: 5px !default;
+$footer-text-color: #808080 !default;
+$footer-left-font-style: italic !default;
+$footer-left-font-weight: normal !default;
+$footer-left-padding: 0px !default;
+$footer-left-text-align: left !default;
+$footer-left-text-color: $footer-text-color !default;
+$footer-right-font-style: italic !default;
+$footer-right-font-weight: normal !default;
+$footer-right-padding: 0px !default;
+$footer-right-separator-margin: 2px !default;
+$footer-right-text-align: right !default;
+$footer-right-text-color: $footer-text-color !default;
diff --git a/src/aurelia-slickgrid/styles/slick-footer.scss b/src/aurelia-slickgrid/styles/slick-footer.scss
new file mode 100644
index 000000000..6f174063a
--- /dev/null
+++ b/src/aurelia-slickgrid/styles/slick-footer.scss
@@ -0,0 +1,30 @@
+@import './variables';
+
+.slick-custom-footer {
+ color: $footer-text-color;
+ padding: $footer-padding;
+ background-color: $footer-bg-color;
+ font-style: $footer-font-style;
+ font-weight: $footer-font-weight;
+ height: $footer-height;
+
+ .left-footer {
+ color: $footer-left-text-color;
+ font-style: $footer-left-font-style;
+ font-weight: $footer-left-font-weight;
+ text-align: $footer-left-text-align;
+ padding: $footer-left-padding;
+ }
+
+ .right-footer.metrics {
+ color: $footer-right-text-color;
+ text-align: $footer-right-text-align;
+ font-style: $footer-right-font-style;
+ font-weight: $footer-right-font-weight;
+ text-align: $footer-right-text-align;
+ padding: $footer-right-padding;
+ .separator {
+ margin: $footer-right-separator-margin;
+ }
+ }
+}
diff --git a/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss b/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss
index d8e05108a..645ed1df8 100644
--- a/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss
+++ b/src/aurelia-slickgrid/styles/slickgrid-theme-bootstrap.scss
@@ -2,6 +2,7 @@
@import './slick-controls';
@import './slick-editors';
@import './slick-pagination';
+@import './slick-footer';
@import './slick-plugins';
@import './slick-default-theme';
@import './slickgrid-examples';
diff --git a/src/aurelia-slickgrid/value-converters/asgDateFormat.spec.ts b/src/aurelia-slickgrid/value-converters/asgDateFormat.spec.ts
new file mode 100644
index 000000000..add3557ca
--- /dev/null
+++ b/src/aurelia-slickgrid/value-converters/asgDateFormat.spec.ts
@@ -0,0 +1,9 @@
+import { AsgDateFormatValueConverter } from './asgDateFormat';
+
+describe('AsgDateFormatConverter method', () => {
+ it('should return a formatted date', () => {
+ const converter = new AsgDateFormatValueConverter();
+ const output = converter.toView(new Date('2019-05-01T02:36:07'), 'YYYY-MM-DD');
+ expect(output).toBe('2019-05-01');
+ });
+});
diff --git a/src/aurelia-slickgrid/value-converters/asgDateFormat.ts b/src/aurelia-slickgrid/value-converters/asgDateFormat.ts
new file mode 100644
index 000000000..16d0b8820
--- /dev/null
+++ b/src/aurelia-slickgrid/value-converters/asgDateFormat.ts
@@ -0,0 +1,7 @@
+import * as moment from 'moment-mini';
+
+export class AsgDateFormatValueConverter {
+ toView(value: any, format: string): string {
+ return moment(value).format(format);
+ }
+}
diff --git a/src/examples/slickgrid/example12.ts b/src/examples/slickgrid/example12.ts
index e2d72fe9e..e8d7ae700 100644
--- a/src/examples/slickgrid/example12.ts
+++ b/src/examples/slickgrid/example12.ts
@@ -142,6 +142,21 @@ export class Example12 {
enableFiltering: true,
enableTranslate: true,
i18n: this.i18n,
+ showCustomFooter: true, // display some metrics in the bottom custom footer
+ customFooterOptions: {
+ metricTexts: {
+ // default text displayed in the metrics section on the right
+ // all texts optionally support translation keys,
+ // if you wish to use that feature then use the text properties with the 'Key' suffix (e.g: itemsKey, ofKey, lastUpdateKey)
+ // example "items" for a plain string OR "itemsKey" to use a translation key
+ itemsKey: 'ITEMS',
+ ofKey: 'OF',
+ lastUpdateKey: 'LAST_UPDATE',
+ },
+ dateFormat: 'YYYY-MM-DD hh:mm a',
+ hideTotalItemCount: false,
+ hideLastUpdateTimestamp: false,
+ },
exportOptions: {
// set at the grid option level, meaning all column will evaluate the Formatter (when it has a Formatter defined)
exportWithFormatter: true,
diff --git a/src/examples/slickgrid/example2.html b/src/examples/slickgrid/example2.html
index 6164d6180..ddd798760 100644
--- a/src/examples/slickgrid/example2.html
+++ b/src/examples/slickgrid/example2.html
@@ -8,7 +8,7 @@
${title}
Pause auto-resize: ${resizerPaused}
- Wiki docs).
@@ -83,6 +83,13 @@ export class Example2 {
sidePadding: 15
},
enableCellNavigation: true,
+ showCustomFooter: true, // display some metrics in the bottom custom footer
+ customFooterOptions: {
+ // optionally display some text on the left footer container
+ leftFooterText: 'custom footer text',
+ hideTotalItemCount: true,
+ hideLastUpdateTimestamp: true
+ },
// you customize all formatter at once certain options through "formatterOptions" in the Grid Options
// or independently through the column definition "params", the option names are the same
@@ -110,7 +117,7 @@ export class Example2 {
getData() {
// mock a dataset
this.dataset = [];
- for (let i = 0; i < 1000; i++) {
+ for (let i = 0; i < 500; i++) {
const randomYear = 2000 + Math.floor(Math.random() * 10);
const randomMonth = Math.floor(Math.random() * 11);
const randomDay = Math.floor((Math.random() * 29));
diff --git a/src/examples/slickgrid/example23.html b/src/examples/slickgrid/example23.html
index e055fc0a1..a08a0747a 100644
--- a/src/examples/slickgrid/example23.html
+++ b/src/examples/slickgrid/example23.html
@@ -11,11 +11,11 @@ ${title}
@@ -41,8 +41,8 @@ ${title}
+ dataset.bind="dataset" asg-on-aurelia-grid-created.delegate="aureliaGridReady($event.detail)"
+ asg-on-grid-state-changed.delegate="gridStateChanged($event.detail)"
+ sg-on-row-count-changed.delegate="refreshMetrics($event.detail.eventData, $event.detail.args)">
diff --git a/src/examples/slickgrid/example23.ts b/src/examples/slickgrid/example23.ts
index c1040f52c..f1ecef95b 100644
--- a/src/examples/slickgrid/example23.ts
+++ b/src/examples/slickgrid/example23.ts
@@ -10,9 +10,9 @@ import {
Formatters,
GridOption,
JQueryUiSliderOption,
+ Metrics,
MultipleSelectOption,
OperatorType,
- Metrics,
} from '../../aurelia-slickgrid';
import * as moment from 'moment-mini';
@@ -267,7 +267,7 @@ export class Example23 {
this.i18n.setLocale(nextLocale).then(() => this.selectedLanguage = nextLocale);
}
- predefinedFilterChangedChanged(newPredefinedFilter) {
+ predefinedFilterChanged(newPredefinedFilter) {
let filters = [];
const currentYear = moment().year();
diff --git a/src/examples/slickgrid/example4.ts b/src/examples/slickgrid/example4.ts
index 8f92347c0..ecbc1c8a3 100644
--- a/src/examples/slickgrid/example4.ts
+++ b/src/examples/slickgrid/example4.ts
@@ -206,6 +206,7 @@ export class Example4 {
enableExcelCopyBuffer: true,
enableFiltering: true,
// enableFilterTrimWhiteSpace: true,
+ showCustomFooter: true, // display some metrics in the bottom custom footer
// use columnDef searchTerms OR use presets as shown below
presets: {
@@ -292,6 +293,7 @@ export class Example4 {
setTimeout(() => {
this.metrics = {
startTime: new Date(),
+ endTime: new Date(),
itemCount: args && args.current || 0,
totalItemCount: this.dataset.length || 0
};
diff --git a/test/cypress/integration/example1.spec.js b/test/cypress/integration/example01.spec.js
similarity index 100%
rename from test/cypress/integration/example1.spec.js
rename to test/cypress/integration/example01.spec.js
diff --git a/test/cypress/integration/example02.spec.js b/test/cypress/integration/example02.spec.js
new file mode 100644
index 000000000..298f90121
--- /dev/null
+++ b/test/cypress/integration/example02.spec.js
@@ -0,0 +1,29 @@
+///
+
+function removeExtraSpaces(textS) {
+ return `${textS}`.replace(/\s+/g, ' ').trim();
+}
+
+describe('Example 2 - Grid with Formatters', () => {
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseExampleUrl')}/example2`);
+ cy.get('h2').should('contain', 'Example 2: Grid with Formatters');
+ });
+
+ it('should show a custom text in the grid footer left portion', () => {
+ cy.get('#slickGridContainer-grid2')
+ .find('.slick-custom-footer')
+ .find('.left-footer')
+ .contains('custom footer text');
+ });
+
+ it('should have some metrics shown in the grid footer', () => {
+ cy.get('#slickGridContainer-grid2')
+ .find('.slick-custom-footer')
+ .find('.right-footer')
+ .should($span => {
+ const text = removeExtraSpaces($span.text()); // remove all white spaces
+ expect(text).to.eq('500 items');
+ });
+ });
+});
diff --git a/test/cypress/integration/example4.spec.js b/test/cypress/integration/example04.spec.js
similarity index 92%
rename from test/cypress/integration/example4.spec.js
rename to test/cypress/integration/example04.spec.js
index ed54b65e1..b159cefd3 100644
--- a/test/cypress/integration/example4.spec.js
+++ b/test/cypress/integration/example04.spec.js
@@ -1,6 +1,10 @@
///
import moment from 'moment-mini';
+function removeExtraSpaces(textS) {
+ return `${textS}`.replace(/\s+/g, ' ').trim();
+}
+
describe('Example 4 - Client Side Sort/Filter Grid', () => {
it('should display Example title', () => {
cy.visit(`${Cypress.config('baseExampleUrl')}/example4`);
@@ -145,6 +149,16 @@ describe('Example 4 - Client Side Sort/Filter Grid', () => {
.click();
});
+ it('should have some metrics shown in the grid footer', () => {
+ cy.get('#slickGridContainer-grid4')
+ .find('.slick-custom-footer')
+ .find('.right-footer')
+ .should($span => {
+ const text = removeExtraSpaces($span.text()); // remove all white spaces
+ expect(text).to.eq('1500 of 1500 items');
+ });
+ });
+
it('should expect the grid to be sorted by "Duration" ascending and "Start" descending', () => {
cy.get('#grid4')
.get('.slick-header-column:nth(2)')
diff --git a/test/cypress/integration/example5.spec.js b/test/cypress/integration/example05.spec.js
similarity index 100%
rename from test/cypress/integration/example5.spec.js
rename to test/cypress/integration/example05.spec.js
diff --git a/test/cypress/integration/example6.spec.js b/test/cypress/integration/example06.spec.js
similarity index 100%
rename from test/cypress/integration/example6.spec.js
rename to test/cypress/integration/example06.spec.js
diff --git a/test/cypress/integration/example7.spec.js b/test/cypress/integration/example07.spec.js
similarity index 100%
rename from test/cypress/integration/example7.spec.js
rename to test/cypress/integration/example07.spec.js
diff --git a/test/cypress/integration/example8.spec.js b/test/cypress/integration/example08.spec.js
similarity index 100%
rename from test/cypress/integration/example8.spec.js
rename to test/cypress/integration/example08.spec.js
diff --git a/test/cypress/integration/example9.spec.js b/test/cypress/integration/example09.spec.js
similarity index 100%
rename from test/cypress/integration/example9.spec.js
rename to test/cypress/integration/example09.spec.js
diff --git a/test/cypress/integration/example12.spec.js b/test/cypress/integration/example12.spec.js
index 8b658691f..50800e80c 100644
--- a/test/cypress/integration/example12.spec.js
+++ b/test/cypress/integration/example12.spec.js
@@ -1,8 +1,14 @@
///
+import moment from 'moment-mini';
+
+function removeExtraSpaces(textS) {
+ return `${textS}`.replace(/\s+/g, ' ').trim();
+}
describe('Example 12: Localization (i18n)', () => {
const fullEnglishTitles = ['Title', 'Description', 'Duration', 'Start', 'Finish', 'Completed', 'Completed'];
const fullFrenchTitles = ['Titre', 'Description', 'Durée', 'Début', 'Fin', 'Terminé', 'Terminé'];
+ let now = new Date();
beforeEach(() => {
cy.restoreLocalStorage();
@@ -14,7 +20,9 @@ describe('Example 12: Localization (i18n)', () => {
it('should display Example title', () => {
cy.visit(`${Cypress.config('baseExampleUrl')}/example12`);
- cy.get('h2').should('contain', 'Example 12: Localization (i18n)');
+ cy.get('h2')
+ .should('contain', 'Example 12: Localization (i18n)')
+ .then(() => now = new Date());
});
describe('English Locale', () => {
@@ -25,6 +33,17 @@ describe('Example 12: Localization (i18n)', () => {
.each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index]));
});
+ it('should have some metrics shown in the grid footer', () => {
+ cy.get('#slickGridContainer-grid12')
+ .find('.slick-custom-footer')
+ .find('.right-footer')
+ .should($span => {
+ const text = removeExtraSpaces($span.text()); // remove all white spaces
+ const dateFormatted = moment(now).format('YYYY-MM-DD hh:mm a');
+ expect(text).to.eq(`Last Update ${dateFormatted} | 1500 of 1500 items`);
+ });
+ });
+
it('should filter certain tasks with the word "ask 1" and expect filter to use contain/include text', () => {
const tasks = ['Task 1', 'Task 10', 'Task 11', 'Task 12'];
@@ -78,6 +97,17 @@ describe('Example 12: Localization (i18n)', () => {
.each(($child, index) => expect($child.text()).to.eq(fullFrenchTitles[index]));
});
+ it('should have some metrics shown in the grid footer', () => {
+ cy.get('#slickGridContainer-grid12')
+ .find('.slick-custom-footer')
+ .find('.right-footer')
+ .should($span => {
+ const text = removeExtraSpaces($span.text()); // remove all white spaces
+ const dateFormatted = moment(now).format('YYYY-MM-DD hh:mm a');
+ expect(text).to.eq(`Dernière mise à jour ${dateFormatted} | 1500 de 1500 éléments`);
+ });
+ });
+
it('should filter certain tasks', () => {
const tasks = ['Tâche 1', 'Tâche 10', 'Tâche 11', 'Tâche 12'];