Skip to content

Commit

Permalink
feat(Resizer): add useResizeObserver option
Browse files Browse the repository at this point in the history
  • Loading branch information
jr01 committed Sep 22, 2021
1 parent 09d5d96 commit bb33cdd
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ <h4 class="title is-4">Container Width (1000px)</h4>
</div>
</div>

<div style="width: 1000px" class="grid-container">
<!-- https://www.w3docs.com/snippets/html/how-to-make-a-div-fill-the-height-of-the-remaining-space.html -->
<div style="width: 1000px; height: calc(100vh - 320px)" class="grid-container">
<div class="grid14">
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ export class Example14 {
autoCommitEdit: true,
autoResize: {
container: '.grid-container',
useResizeObserver: true
},
enableAutoResize: true,

Expand Down
26 changes: 23 additions & 3 deletions packages/common/src/interfaces/resizerOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ export interface ResizerOption {
/** Defaults to false, do we want to apply the resized dimentions to the grid container as well? */
applyResizeToContainer?: boolean;

/** Defaults to 'window', which DOM element are we using to calculate the available size for the grid? */
/**
* Defaults to 'window', which DOM element are we using to calculate the available size for the grid?
* When {@link useResizeObserver}=true the {@link container} is used and this option is ignored.
*/
calculateAvailableSizeBy?: 'container' | 'window';

/** bottom padding of the grid in pixels */
bottomPadding?: number;

/** Page Container selector, for example '.page-container' or '#page-container', basically what element in the page will be used to calculate the available space */
container?: string;
/**
* Page Container. Either selector (for example '.page-container' or '#page-container'), or an HTMLElement.
* Basically what element in the page will be used to calculate the available space.
*/
container?: string | HTMLElement;

/**
* Grid Container selector, for example '.myGrid' or '#myGrid', this is provided by the lib internally.
Expand All @@ -33,4 +39,18 @@ export interface ResizerOption {

/** padding on the right side of the grid (pixels) */
rightPadding?: number;

/**
* Use a [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) to detect resizes on the {@link container} element instead of window resize events.
*
* Remarks:
* * Requires {@link container} to be set.
*
* * ResizeObserver is not supported on older browsers like Internet Explorer. If you need the support you need to install a
* polyfill like [resize-observer-polyfill](https://www.npmjs.com/package/resize-observer-polyfill) yourself.
*
* * If you get 'ResizeObserver loop limit exceeded' errors in automated tests take a look
* [here](https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded).
*/
useResizeObserver?: boolean;
}
81 changes: 81 additions & 0 deletions packages/common/src/services/__tests__/resizer.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { FieldType, } from '../../enums/index';
import { Column, GridOption, SlickGrid, SlickNamespace, } from '../../interfaces/index';
import { ResizerService } from '../resizer.service';

import 'jest-extended';

declare const Slick: SlickNamespace;
const DATAGRID_MIN_HEIGHT = 180;
const DATAGRID_MIN_WIDTH = 300;
Expand Down Expand Up @@ -63,12 +65,24 @@ describe('Resizer Service', () => {
let service: ResizerService;
let divContainer: HTMLDivElement;
let mockGridOptions: GridOption;
let resizeObserverMock: jest.Mock<ResizeObserver, [callback: ResizeObserverCallback]>;

beforeEach(() => {
divContainer = document.createElement('div');
divContainer.innerHTML = template;
document.body.appendChild(divContainer);

resizeObserverMock = jest.fn(function (callback: ResizeObserverCallback): ResizeObserver {
this.observe = jest.fn().mockImplementation(() => {
callback([], this); // Execute the callback on observe, similar to the window.ResizeObserver.
});
this.unobserve = jest.fn();
this.disconnect = jest.fn();
return this;
});

global.ResizeObserver = resizeObserverMock;

eventPubSubService = new EventPubSubService();
service = new ResizerService(eventPubSubService);
service.intervalRetryDelay = 1;
Expand Down Expand Up @@ -125,6 +139,57 @@ describe('Resizer Service', () => {

expect(bindAutoResizeDataGridSpy).not.toHaveBeenCalled();
});

it('should observe resize events on the container element when "useResizeObserver" is true', () => {
mockGridOptions.enableAutoResize = true;
mockGridOptions.autoResize.useResizeObserver = true;
const resizeContainer = document.createElement('div');
mockGridOptions.autoResize.container = resizeContainer;

service.init(gridStub, divContainer);

expect(resizeObserverMock.mock.instances.length).toBe(1);
const observerInstance = resizeObserverMock.mock.instances[0];

expect(observerInstance.observe).toHaveBeenCalledTimes(1);
expect(observerInstance.observe).toHaveBeenCalledWith(resizeContainer);
});

it('should throw an error when container element is not valid and "useResizeObserver" is true', () => {
mockGridOptions.enableAutoResize = true;
mockGridOptions.autoResize.useResizeObserver = true;
mockGridOptions.autoResize.container = '#doesnotexist';

expect(() => service.init(gridStub, divContainer)).toThrowError('[Slickgrid-Universal] Resizer Service requires a container when gridOption.autoResize.useResizeObserver=true');
});

it('should execute "resizeGrid" when "useResizeObserver" is true', () => {
mockGridOptions.enableAutoResize = true;
mockGridOptions.autoResize.useResizeObserver = true;
const resizeContainer = document.createElement('div');
mockGridOptions.autoResize.container = resizeContainer;

const resizeGridSpy = jest.spyOn(service, 'resizeGrid');

service.init(gridStub, divContainer);

expect(resizeGridSpy).toHaveBeenCalledWith();
});

it('should not execute "resizeGrid" when "useResizeObserver" is true and the resizer is paused', () => {
mockGridOptions.enableAutoResize = true;
mockGridOptions.autoResize.useResizeObserver = true;
const resizeContainer = document.createElement('div');
mockGridOptions.autoResize.container = resizeContainer;

const resizeGridSpy = jest.spyOn(service, 'resizeGrid');

service.pauseResizer(true);

service.init(gridStub, divContainer);

expect(resizeGridSpy).not.toHaveBeenCalled();
});
});

describe('dispose method', () => {
Expand All @@ -140,6 +205,22 @@ describe('Resizer Service', () => {
done();
}, 2);
});

it('should disconnect from resize events on the container element when "useResizeObserver" is true', () => {
mockGridOptions.enableAutoResize = true;
mockGridOptions.autoResize.useResizeObserver = true;
const resizeContainer = document.createElement('div');
mockGridOptions.autoResize.container = resizeContainer;

service.init(gridStub, divContainer);

service.dispose();

expect(resizeObserverMock.mock.instances.length).toBe(1);
const observerInstance = resizeObserverMock.mock.instances[0];

expect(observerInstance.disconnect).toHaveBeenCalledTimes(1);
});
});

describe('resizeGrid method', () => {
Expand Down
77 changes: 53 additions & 24 deletions packages/common/src/services/resizer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ResizerService {
protected _fixedWidth?: number | string;
protected _gridDomElm!: any;
protected _gridContainerElm!: any;
protected _pageContainerElm!: any;
protected _pageContainerElm!: HTMLElement | null;
protected _gridParentContainerElm!: HTMLElement;
protected _intervalId!: NodeJS.Timeout;
protected _intervalRetryDelay = DEFAULT_INTERVAL_RETRY_DELAY;
Expand All @@ -42,6 +42,7 @@ export class ResizerService {
protected _totalColumnsWidthByContent = 0;
protected _timer!: NodeJS.Timeout;
protected _resizePaused = false;
protected readonly _resizeObserver: ResizeObserver = new ResizeObserver(() => this.resizeObserverCallback());

get eventHandler(): SlickEventHandler {
return this._eventHandler;
Expand Down Expand Up @@ -89,7 +90,11 @@ export class ResizerService {
}
clearTimeout(this._timer);

$(window).off(`resize.grid${this.gridUidSelector}`);
if (this.gridOptions.autoResize?.useResizeObserver) {
this._resizeObserver.disconnect();
} else {
$(window).off(`resize.grid${this.gridUidSelector}`);
}
}

init(grid: SlickGrid, gridParentContainerElm: HTMLElement) {
Expand All @@ -110,7 +115,13 @@ export class ResizerService {

const containerNode = grid?.getContainerNode?.() as HTMLDivElement;
this._gridDomElm = $(containerNode);
this._pageContainerElm = $(this._autoResizeOptions.container!);

if (typeof this._autoResizeOptions.container === 'string') {
this._pageContainerElm = document.querySelector<HTMLElement>(this._autoResizeOptions.container);
} else if (this._autoResizeOptions.container) {
this._pageContainerElm = this._autoResizeOptions.container;
}

this._gridContainerElm = $(gridParentContainerElm);

if (fixedGridSizes) {
Expand Down Expand Up @@ -146,22 +157,31 @@ export class ResizerService {
* Options: we could also provide a % factor to resize on each height/width independently
*/
bindAutoResizeDataGrid(newSizes?: GridSize): null | void {
// if we can't find the grid to resize, return without binding anything
if (this._gridDomElm === undefined || this._gridDomElm.offset() === undefined) {
return null;
}
if (this.gridOptions.autoResize?.useResizeObserver) {
if (!this._pageContainerElm) {
throw new Error(`
[Slickgrid-Universal] Resizer Service requires a container when gridOption.autoResize.useResizeObserver=true
You can fix this by setting your gridOption.autoResize.container`);
}
this._resizeObserver.observe(this._pageContainerElm);
} else {
// if we can't find the grid to resize, return without binding anything
if (this._gridDomElm === undefined || this._gridDomElm.offset() === undefined) {
return null;
}

// -- 1st resize the datagrid size at first load (we need this because the .on event is not triggered on first load)
this.resizeGrid()
.then(() => this.resizeGridWhenStylingIsBrokenUntilCorrected())
.catch((rejection: any) => console.log('Error:', rejection));
// -- 1st resize the datagrid size at first load (we need this because the .on event is not triggered on first load)
this.resizeGrid()
.then(() => this.resizeGridWhenStylingIsBrokenUntilCorrected())
.catch((rejection: any) => console.log('Error:', rejection));

// -- do a 2nd resize with a slight delay (in ms) so that we resize after the grid render is done
this.resizeGrid(10, newSizes);
// -- do a 2nd resize with a slight delay (in ms) so that we resize after the grid render is done
this.resizeGrid(10, newSizes);

// -- 2nd bind a trigger on the Window DOM element, so that it happens also when resizing after first load
// -- bind auto-resize to Window object only if it exist
$(window).on(`resize.grid${this.gridUidSelector}`, this.handleResizeGrid.bind(this, newSizes));
// -- 2nd bind a trigger on the Window DOM element, so that it happens also when resizing after first load
// -- bind auto-resize to Window object only if it exist
$(window).on(`resize.grid${this.gridUidSelector}`, this.handleResizeGrid.bind(this, newSizes));
}
}

handleResizeGrid(newSizes?: GridSize) {
Expand All @@ -174,13 +194,19 @@ export class ResizerService {
}
}

resizeObserverCallback(): void {
if (!this._resizePaused) {
this.resizeGrid();
}
}

/**
* Calculate the datagrid new height/width from the available space, also consider that a % factor might be applied to calculation
* object gridOptions
*/
calculateGridNewDimensions(gridOptions: GridOption): GridSize | null {
const autoResizeOptions = gridOptions?.autoResize ?? {};
if (!window || this._pageContainerElm === undefined || this._gridDomElm.offset() === undefined) {
if (!window || this._gridDomElm.offset() === undefined) {
return null;
}

Expand All @@ -197,22 +223,25 @@ export class ResizerService {
bottomPadding += parseInt(`${footerHeight}`, 10);
}

let gridHeight = 0;
let gridOffsetTop = 0;
let gridHeight;
let gridWidth;

// which DOM element are we using to calculate the available size for the grid?
if (autoResizeOptions.calculateAvailableSizeBy === 'container') {
if (autoResizeOptions.calculateAvailableSizeBy === 'container' || autoResizeOptions.useResizeObserver) {
// uses the container's height to calculate grid height without any top offset
gridHeight = this._pageContainerElm.height() || 0;
gridHeight = this._pageContainerElm?.offsetHeight ?? 0;
gridWidth = this._pageContainerElm?.offsetWidth ?? 0;
} else {
// uses the browser's window height with its top offset to calculate grid height
gridHeight = window.innerHeight || 0;
gridWidth = window.innerWidth || 0;
const coordOffsetTop = this._gridDomElm.offset();
gridOffsetTop = (coordOffsetTop !== undefined) ? coordOffsetTop.top : 0;
const gridOffsetTop = (coordOffsetTop !== undefined) ? coordOffsetTop.top : 0;
gridHeight -= gridOffsetTop;
}

const availableHeight = gridHeight - gridOffsetTop - bottomPadding;
const availableWidth = this._pageContainerElm.width() || window.innerWidth || 0;
const availableHeight = gridHeight - bottomPadding;
const availableWidth = gridWidth;
const maxHeight = autoResizeOptions?.maxHeight;
const minHeight = autoResizeOptions?.minHeight ?? DATAGRID_MIN_HEIGHT;
const maxWidth = autoResizeOptions?.maxWidth;
Expand Down

0 comments on commit bb33cdd

Please sign in to comment.