Skip to content

Commit

Permalink
feat(core): add "Empty Data" warning message when grid is empty (#155)
Browse files Browse the repository at this point in the history
* feat(core): add "Empty Data" warning message when grid is empty
- this is in fact a div element that is created and reposition inside the grid, the reason we do that is because we cannot put that text inside a cell because it has its cell boundary, so creating a div and position it over the grid is the best approach

* fix(core): mem leaks w/orphan DOM elements when disposing

* feat(formatters): add FakeHyperlink Formatter
  • Loading branch information
ghiscoding authored Nov 9, 2020
1 parent 9633d4a commit 13875b4
Show file tree
Hide file tree
Showing 38 changed files with 403 additions and 81 deletions.
1 change: 1 addition & 0 deletions examples/webpack-demo-vanilla-bundle/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"COMMANDS": "Commands",
"CONTAINS": "Contains",
"COPY": "Copy",
"EMPTY_DATA_WARNING_MESSAGE": "No data to display.",
"ENDS_WITH": "Ends With",
"EQUALS": "Equals",
"EQUAL_TO": "Equal to",
Expand Down
1 change: 1 addition & 0 deletions examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"COMMANDS": "Commandes",
"CONTAINS": "Contient",
"COPY": "Copier",
"EMPTY_DATA_WARNING_MESSAGE": "Aucune donnée à afficher.",
"ENDS_WITH": "Se termine par",
"EQUALS": "Égale",
"EQUAL_TO": "Égal à",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class Example10 {
* @param query
* @return Promise<GraphqlPaginatedResult>
*/
getCustomerApiCall(query: string): Promise<GraphqlPaginatedResult> {
getCustomerApiCall(_query: string): Promise<GraphqlPaginatedResult> {
// in your case, you will call your WebAPI function (wich needs to return a Promise)
// for the demo purpose, we will call a mock WebAPI function
const mockedResult = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ export class Example11 {

pushNewFilterToSelectPreFilter(predefinedFilters: FilterPreset | FilterPreset[], isOptionSelected = false) {
if (isOptionSelected) {
this.resetPredefinedFilterSelection(this.predefinedPresets);
this.predefinedPresets.forEach(preFilter => preFilter.isSelected = false); // reset selection
}
const presetFilters: FilterPreset[] = Array.isArray(predefinedFilters) ? predefinedFilters : [predefinedFilters];
const filterSelect = document.querySelector('.selected-filter');
Expand Down Expand Up @@ -611,6 +611,7 @@ export class Example11 {
event.stopPropagation();
return;
}
this.predefinedPresets.forEach(preFilter => preFilter.isSelected = false); // reset selection
const currentFilters = this.sgb.filterService.getCurrentLocalFilters();

const filterName = await prompt('Please provide a name for the new Filter.');
Expand Down Expand Up @@ -665,7 +666,7 @@ export class Example11 {
}

usePredefinedFilter(filterValue: string) {
this.resetPredefinedFilterSelection(this.predefinedPresets);
this.predefinedPresets.forEach(preFilter => preFilter.isSelected = false); // reset selection
const selectedFilter = this.predefinedPresets.find(preset => preset.value === filterValue);
if (selectedFilter) {
selectedFilter.isSelected = true;
Expand All @@ -681,10 +682,6 @@ export class Example11 {
this.currentSelectedFilterPreset = selectedFilter;
}

resetPredefinedFilterSelection(predefinedFilters: FilterPreset[]) {
predefinedFilters.forEach(preFilter => preFilter.isSelected = false);
}

mockProducts() {
return [
{
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/editors/dateEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ export class DateEditor implements Editor {
if (this.flatInstance.element) {
setTimeout(() => destroyObjectDomElementProps(this.flatInstance));
}
this.flatInstance = null;
}
if (this._$editorInputElm?.remove) {
this._$editorInputElm.remove();
Expand All @@ -178,6 +177,7 @@ export class DateEditor implements Editor {
this._$inputWithData.remove();
this._$inputWithData = null;
}
this._$input.remove();
}

disable(isDisabled = true) {
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/editors/sliderEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class SliderEditor implements Editor {
// if user chose to display the slider number on the right side, then update it every time it changes
// we need to use both "input" and "change" event to be all cross-browser
if (!this.editorParams.hideSliderNumber) {
this._$editorElm.on('input change', (event: JQueryEventObject & { target: HTMLInputElement }) => {
this._$editorElm.on('input change', (event: JQuery.Event & { target: HTMLInputElement }) => {
const value = event && event.target && event.target.value || '';
if (value && document) {
const elements = document.getElementsByClassName(this._elementRangeOutputId || '');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,14 @@ describe('checkboxSelectorExtension', () => {

it('should dispose of the addon', () => {
const instance = extension.create(columnsMock, gridOptionsMock);
const destroySpy = jest.spyOn(instance, 'destroy');
const selectionModel = extension.register();
const addonDestroySpy = jest.spyOn(instance, 'destroy');
const smDestroySpy = jest.spyOn(selectionModel, 'destroy');

extension.dispose();

expect(destroySpy).toHaveBeenCalled();
expect(addonDestroySpy).toHaveBeenCalled();
expect(smDestroySpy).toHaveBeenCalled();
});

it('should provide addon options and expect them to be called in the addon constructor', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ declare const Slick: SlickNamespace;
export class CellExternalCopyManagerExtension implements Extension {
private _addon: SlickCellExternalCopyManager | null;
private _addonOptions: ExcelCopyBufferOption;
private _cellSelectionModel: SlickCellSelectionModel;
private _eventHandler: SlickEventHandler;
private _commandQueue: EditCommand[];
private _undoRedoBuffer: EditUndoRedoBuffer;
Expand Down Expand Up @@ -55,6 +56,10 @@ export class CellExternalCopyManagerExtension implements Extension {
if (this._addon && this._addon.destroy) {
this._addon.destroy();
}
if (this._cellSelectionModel?.destroy) {
this._cellSelectionModel.destroy();
}
document.removeEventListener('keydown', this.hookUndoShortcutKey.bind(this));
}

/** Get the instance of the SlickGrid addon (control or plugin). */
Expand All @@ -72,7 +77,8 @@ export class CellExternalCopyManagerExtension implements Extension {
this.hookUndoShortcutKey();

this._addonOptions = { ...this.getDefaultOptions(), ...this.sharedService.gridOptions.excelCopyBufferOptions } as ExcelCopyBufferOption;
this.sharedService.slickGrid.setSelectionModel(new Slick.CellSelectionModel() as SlickCellSelectionModel);
this._cellSelectionModel = new Slick.CellSelectionModel() as SlickCellSelectionModel;
this.sharedService.slickGrid.setSelectionModel(this._cellSelectionModel);
this._addon = new Slick.CellExternalCopyManager(this._addonOptions);
if (this._addon) {
this.sharedService.slickGrid.registerPlugin<SlickCellExternalCopyManager>(this._addon);
Expand Down
6 changes: 5 additions & 1 deletion packages/common/src/extensions/checkboxSelectorExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ declare const Slick: SlickNamespace;

export class CheckboxSelectorExtension implements Extension {
private _addon: SlickCheckboxSelectColumn | null;
private _rowSelectionPlugin: SlickRowSelectionModel;

constructor(private extensionUtility: ExtensionUtility, private sharedService: SharedService) { }

Expand All @@ -16,6 +17,9 @@ export class CheckboxSelectorExtension implements Extension {
this._addon.destroy();
this._addon = null;
}
if (this._rowSelectionPlugin?.destroy) {
this._rowSelectionPlugin.destroy();
}
}

/**
Expand Down Expand Up @@ -73,7 +77,7 @@ export class CheckboxSelectorExtension implements Extension {
if (this.sharedService.gridOptions.preselectedRows && rowSelectionPlugin && this.sharedService.slickGrid.getSelectionModel()) {
setTimeout(() => this._addon?.selectRows(this.sharedService.gridOptions.preselectedRows || []));
}

this._rowSelectionPlugin = rowSelectionPlugin;
return rowSelectionPlugin;
}
return null;
Expand Down
6 changes: 5 additions & 1 deletion packages/common/src/extensions/rowMoveManagerExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ declare const Slick: SlickNamespace;
export class RowMoveManagerExtension implements Extension {
private _addon: SlickRowMoveManager | null;
private _eventHandler: SlickEventHandler;
private _rowSelectionPlugin: SlickRowSelectionModel;

constructor(private extensionUtility: ExtensionUtility, private sharedService: SharedService) {
this._eventHandler = new Slick.EventHandler();
Expand All @@ -35,6 +36,9 @@ export class RowMoveManagerExtension implements Extension {
this._addon.destroy();
this._addon = null;
}
if (this._rowSelectionPlugin?.destroy) {
this._rowSelectionPlugin.destroy();
}
}

/**
Expand Down Expand Up @@ -101,7 +105,7 @@ export class RowMoveManagerExtension implements Extension {
rowSelectionPlugin = new Slick.RowSelectionModel(this.sharedService.gridOptions.rowSelectionOptions);
this.sharedService.slickGrid.setSelectionModel(rowSelectionPlugin);
}

this._rowSelectionPlugin = rowSelectionPlugin;
this.sharedService.slickGrid.registerPlugin<SlickRowMoveManager>(this._addon);

// hook all events
Expand Down
3 changes: 1 addition & 2 deletions packages/common/src/filters/compoundDateFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,13 @@ export class CompoundDateFilter implements Filter {
if (this.flatInstance.element) {
destroyObjectDomElementProps(this.flatInstance);
}
this.flatInstance = null;
}
if (this.$filterElm) {
this.$filterElm.off('keyup').remove();
this.$filterElm = null;
}
if (this.$selectOperatorElm) {
this.$selectOperatorElm.off('change').remove()
this.$selectOperatorElm.off('change').remove();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { fakeHyperlinkFormatter } from '../fakeHyperlinkFormatter';

describe('the Edit Icon Formatter', () => {
it('should return a span with the "fake-hyperlink" class when a value is provided', () => {
const value = 'Custom Value';
const result = fakeHyperlinkFormatter(0, 0, value);
expect(result).toBe('<span class="fake-hyperlink">Custom Value</span>');
});

it('should return an empty string formatter when no value is provided', () => {
const value = null;
const result = fakeHyperlinkFormatter(0, 0, value);
expect(result).toBe('');
});
});
5 changes: 5 additions & 0 deletions packages/common/src/formatters/fakeHyperlinkFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Formatter } from './../interfaces/index';

export const fakeHyperlinkFormatter: Formatter = (_row: number, _cell: number, value: string) => {
return value ? `<span class="fake-hyperlink">${value}</span>` : '';
};
8 changes: 8 additions & 0 deletions packages/common/src/global-grid-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ export const GlobalGridOptions: GridOption = {
defaultSlickgridEventPrefix: '',
editable: false,
editorTypingDebounce: 450,
enableEmptyDataWarningMessage: true,
emptyDataWarning: {
class: 'slick-empty-data-warning',
message: 'No data to display.',
messageKey: 'EMPTY_DATA_WARNING_MESSAGE',
marginTop: 100,
marginLeft: 10
},
enableAutoResize: true,
enableAutoSizeColumns: true,
enableCellNavigation: false,
Expand Down
16 changes: 16 additions & 0 deletions packages/common/src/interfaces/emptyWarning.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface EmptyWarning {
/** Empty data warning message, defaults to "No data to display." */
message: string;

/** Empty data warning message translation key, defaults to "EMPTY_DATA_WARNING_MESSAGE" */
messageKey?: string;

/** DOM Element class name, defaults to "empty-data-warning" */
class?: string;

/** Top margin position, number in pixel, of where the warning message will be displayed, default calculation is (header title row + filter row + 5px) */
marginTop?: number;

/** Left margin position, number in pixel, of where the warning message will be displayed, defaults to 10px */
marginLeft?: number;
}
10 changes: 10 additions & 0 deletions packages/common/src/interfaces/gridOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DataViewOption,
DraggableGrouping,
EditCommand,
EmptyWarning,
ExcelCopyBufferOption,
ExcelExportOption,
ExportOption,
Expand Down Expand Up @@ -170,6 +171,15 @@ export interface GridOption {
/** Default to 450ms and only applies to Composite Editor, how long to wait until we start validating the editor changes on Editor that support it (integer, float, text, longText). */
editorTypingDebounce?: number;

emptyDataWarning?: EmptyWarning;

/**
* Defaults to true, will display a warning message positioned inside the grid when there's no data returned.
* When using local (in-memory) dataset, it will show the message when there's no filtered data returned.
* When using backend Pagination it will display the message as soon as the total row count is 0.
*/
enableEmptyDataWarningMessage?: boolean;

/** Do we want to emulate paging when we are scrolling? */
emulatePagingWhenScrolling?: boolean;

Expand Down
1 change: 1 addition & 0 deletions packages/common/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export * from './editorValidationResult.interface';
export * from './editorValidator.interface';
export * from './editUndoRedoBuffer.interface';
export * from './elementPosition.interface';
export * from './emptyWarning.interface';
export * from './excelCellFormat.interface';
export * from './excelCopyBufferOption.interface';
export * from './excelExportOption.interface';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SlickCellRangeDecorator, SlickGrid } from './index';
import { SlickCellRangeDecorator, SlickGrid, SlickRange } from './index';
import { CellRange } from './cellRange.interface';
import { SlickEvent } from './slickEvent.interface';
import { SlickEventData } from './slickEventData.interface';

export interface SlickCellRangeSelector {
pluginName: 'CellRangeSelector'
Expand All @@ -28,5 +27,5 @@ export interface SlickCellRangeSelector {
onBeforeCellRangeSelected: SlickEvent<{ cell: { row: number; cell: number; } }>;

/** Triggered after a cell range selection happened */
onCellRangeSelected: SlickEventData;
onCellRangeSelected: SlickEvent<{ range: SlickRange }>;
}
20 changes: 15 additions & 5 deletions packages/common/src/services/__tests__/grid.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ jest.useFakeTimers();

declare const Slick: SlickNamespace;

const mockSelectionModel = jest.fn().mockImplementation(() => ({
const mockSelectionModel = {
init: jest.fn(),
destroy: jest.fn()
}));
};
const mockSelectionModelImplementation = jest.fn().mockImplementation(() => mockSelectionModel);

jest.mock('flatpickr', () => { });
jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModel);
Slick.RowSelectionModel = mockSelectionModel;
jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModelImplementation);
Slick.RowSelectionModel = mockSelectionModelImplementation;

const extensionServiceStub = {
getAllColumns: jest.fn(),
Expand Down Expand Up @@ -99,6 +100,15 @@ describe('Grid Service', () => {
expect(service).toBeTruthy();
});

it('should dispose of the service', () => {
const destroySpy = jest.spyOn(mockSelectionModel, 'destroy');

service.highlightRow(0, 10, 15);
service.dispose();

expect(destroySpy).toHaveBeenCalled();
});

describe('getAllColumnDefinitions method', () => {
it('should call "allColumns" GETTER ', () => {
const mockColumns = [{ id: 'field1', field: 'field1', width: 100 }, { id: 'field2', field: 'field2', width: 100 }];
Expand Down Expand Up @@ -1358,7 +1368,7 @@ describe('Grid Service', () => {
const extensionSpy = jest.spyOn(extensionServiceStub, 'getAllColumns').mockReturnValue(mockColumns);
// const gridStateSpy = jest.spyOn(gridStateServiceStub, 'resetColumns');

service.resetGrid(mockColumns);
service.resetGrid();

expect(extensionSpy).toHaveBeenCalled();
// expect(gridStateSpy).toHaveBeenCalledWith(mockColumns);
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/services/extension.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class ExtensionService {
for (const extensionName of Object.keys(this._extensionList)) {
if (this._extensionList.hasOwnProperty(extensionName)) {
const extension = this._extensionList[extensionName] as ExtensionModel<any, any>;
if (extension && extension.class && extension.class.dispose) {
if (extension?.class?.dispose) {
extension.class.dispose();
}
}
Expand Down
12 changes: 10 additions & 2 deletions packages/common/src/services/grid.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
OnEventArgs,
SlickGrid,
SlickNamespace,
SlickRowSelectionModel,
} from '../interfaces/index';
import { ExtensionService } from './extension.service';
import { FilterService } from './filter.service';
Expand All @@ -27,6 +28,7 @@ const GridServiceUpdateOptionDefaults: GridServiceUpdateOption = { highlightRow:

export class GridService {
private _grid: SlickGrid;
private _rowSelectionPlugin: SlickRowSelectionModel;

constructor(
private extensionService: ExtensionService,
Expand All @@ -47,6 +49,12 @@ export class GridService {
return (this._grid?.getOptions) ? this._grid.getOptions() : {};
}

dispose() {
if (this._rowSelectionPlugin?.destroy) {
this._rowSelectionPlugin.destroy();
}
}

init(grid: SlickGrid): void {
this._grid = grid;
}
Expand Down Expand Up @@ -215,8 +223,8 @@ export class GridService {
highlightRow(rowNumber: number | number[], fadeDelay = 1500, fadeOutDelay = 300) {
// create a SelectionModel if there's not one yet
if (!this._grid.getSelectionModel() && Slick && Slick.RowSelectionModel) {
const rowSelectionPlugin = new Slick.RowSelectionModel(this._gridOptions.rowSelectionOptions);
this._grid.setSelectionModel(rowSelectionPlugin);
this._rowSelectionPlugin = new Slick.RowSelectionModel(this._gridOptions.rowSelectionOptions);
this._grid.setSelectionModel(this._rowSelectionPlugin);
}

if (Array.isArray(rowNumber)) {
Expand Down
Loading

0 comments on commit 13875b4

Please sign in to comment.