Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions cypress/e2e/quirk-runtime-footer-enable.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Regression test for enabling createFooterRow at runtime.
*
* Footer-row DOM was built only in the init path, and internal_setOptions never
* created it — so `setOptions({ createFooterRow: true })` on a live grid flowed
* into setColumns → createColumnFooter, which dereferenced the undefined
* `_footerRowL` and threw, leaving the grid with a half-mutated options state.
*
* The grid now materializes the footer DOM lazily when the flag flips true
* (mirroring the init construction and binding footer events on the live grid);
* runtime disable hides the footer rather than destroying it, symmetric with
* showFooterRow.
*
* The spec is SELF-HOSTING (harness served via cy.intercept; no example page).
* The harness wraps the enabling setOptions in try/catch so the pre-fix
* TypeError reports as a graceful check failure. Verified to FAIL pre-fix
* (TypeError) and PASS with the fix.
*/

const harnessHtml = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Harness: runtime footer enable</title>
<link rel="stylesheet" href="/dist/styles/css/slick-alpine-theme.css"/>
<style> #myGrid { width: 700px; height: 300px; } </style>
</head>
<body>
<div id="myGrid"></div>
<div id="checkResults" style="white-space:pre; font-family:monospace;"></div>
<script src="/dist/browser/slick.core.js"></script>
<script src="/dist/browser/slick.interactions.js"></script>
<script src="/dist/browser/slick.grid.js"></script>
<script>
var columns = [
{ id: 'id', name: '#', field: 'id', width: 80 },
{ id: 'a', name: 'A', field: 'a', width: 200 },
{ id: 'b', name: 'B', field: 'b', width: 200 }
];
var COLCOUNT = columns.length;
var data = [];
for (var i = 0; i < 20; i++) { data.push({ id: i, a: 'a' + i, b: 'b' + i }); }

var grid = new Slick.Grid('#myGrid', data, columns, {
enableCellNavigation: true,
enableColumnReorder: false,
rowHeight: 25
});
var footerRenderCount = 0;
grid.onFooterRowCellRendered.subscribe(function () { footerRenderCount++; });
window.grid = grid;

window.runChecks = function runChecks() {
var out = [], pass = true;
function check(label, ok, detail) {
out.push((ok ? 'PASS ' : 'FAIL ') + label + (detail ? ' [' + detail + ']' : ''));
if (!ok) { pass = false; }
}
function isVisible(el) { return !!el && el.offsetParent !== null && el.offsetHeight > 0; }

var enableError = null;
try {
grid.setOptions({ createFooterRow: true, showFooterRow: true });
} catch (e) { enableError = (e && e.message) || String(e); }
check('setOptions({ createFooterRow: true }) on a live grid does not throw',
enableError === null, enableError ? 'threw: ' + enableError : 'ok');

if (enableError === null) {
var scrollers = document.querySelectorAll('#myGrid .slick-footerrow');
check('footer scrollers exist and are visible',
scrollers.length === 2 && isVisible(scrollers[0]),
'count=' + scrollers.length + ' visible=' + (scrollers.length ? isVisible(scrollers[0]) : '-'));

var cells = document.querySelectorAll('#myGrid .slick-footerrow-column');
check('one footer cell rendered per visible column', cells.length === COLCOUNT,
'cells=' + cells.length + ' expected=' + COLCOUNT);

check('onFooterRowCellRendered fired once per column', footerRenderCount === COLCOUNT,
'fired=' + footerRenderCount + ' expected=' + COLCOUNT);

var fr = grid.getFooterRow();
check('getFooterRow() returns the footer element', !!fr, 'returned ' + fr);

grid.setFooterRowVisibility(false);
var hiddenNow = !isVisible(document.querySelector('#myGrid .slick-footerrow'));
check('setFooterRowVisibility(false) hides the runtime-built footer', hiddenNow, 'hidden=' + hiddenNow);
}

out.push(pass ? '\\nALL CHECKS PASSED' : '\\nCHECKS FAILED');
document.getElementById('checkResults').textContent = out.join('\\n');
return pass;
};
</script>
</body>
</html>`;

describe('Quirk - createFooterRow must be enableable at runtime', { retries: 1 }, () => {
it('should build, populate, wire and toggle the footer when enabled after init', () => {
cy.intercept('GET', '/quirk-runtime-footer-enable-harness.html', {
headers: { 'content-type': 'text/html' },
body: harnessHtml,
});
cy.visit(`${Cypress.config('baseUrl')}/quirk-runtime-footer-enable-harness.html`);
cy.window().its('grid').should('exist');

cy.window().then((win: any) => {
const ok = win.runChecks();
const detail = win.document.getElementById('checkResults').textContent;
expect(ok, `in-page runtime-footer self-checks:\n${detail}`).to.eq(true);
});
cy.get('#checkResults').should('contain', 'ALL CHECKS PASSED');
});
});
71 changes: 71 additions & 0 deletions examples/example-quirk-runtime-footer-enable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!doctype html>
<!--
TEMPORARY repro page for enabling createFooterRow at runtime.
DO NOT MERGE — human review only; deleted before merge. The permanent
regression test is cypress/e2e/quirk-runtime-footer-enable.cy.ts, which is
fully self-hosting and does NOT depend on this page.

Pre-fix: clicking "Enable footer" throws a TypeError (footer DOM was only
built at init) and the readout shows the error. Fixed: the footer appears,
populated per column, and the show/hide button toggles it.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>SlickGrid quirk repro: runtime footer enable (temporary, do not merge)</title>
<link rel="stylesheet" href="../dist/styles/css/example-demo.css" type="text/css"/>
<link rel="stylesheet" href="../dist/styles/css/slick-alpine-theme.css" type="text/css"/>
<style> #myGrid { width: 700px; height: 300px; } </style>
</head>
<body>
<h2>Runtime <code>createFooterRow</code> enable (TEMPORARY repro, do not merge)</h2>
<div style="width:700px;">
<div id="myGrid"></div>
<button onclick="enableFooter()">Enable footer (setOptions createFooterRow: true)</button>
<button onclick="toggleShow()">Toggle footer visibility (setFooterRowVisibility)</button>
<div id="readout" style="white-space:pre; font-family:monospace; font-size:12px; margin-top:8px;"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/sortablejs/Sortable.min.js"></script>
<script src="sortable-cdn-fallback.js"></script>

<script src="../dist/browser/slick.core.js"></script>
<script src="../dist/browser/slick.interactions.js"></script>
<script src="../dist/browser/slick.grid.js"></script>
<script>
var columns = [
{ id: 'id', name: '#', field: 'id', width: 80 },
{ id: 'a', name: 'A', field: 'a', width: 200 },
{ id: 'b', name: 'B', field: 'b', width: 200 }
];
var data = [];
for (var i = 0; i < 20; i++) { data.push({ id: i, a: 'a' + i, b: 'b' + i }); }
var grid = new Slick.Grid('#myGrid', data, columns, {
enableCellNavigation: true,
enableColumnReorder: false,
rowHeight: 25
});
grid.onFooterRowCellRendered.subscribe(function (e, args) {
args.node.textContent = 'Σ ' + args.column.id;
});
var shown = true;

function enableFooter() {
try {
grid.setOptions({ createFooterRow: true, showFooterRow: true });
shown = true;
document.getElementById('readout').textContent =
'Footer enabled. Cells: ' + document.querySelectorAll('#myGrid .slick-footerrow-column').length +
' (fixed build). Pre-fix build: a TypeError is thrown instead.';
} catch (e) {
document.getElementById('readout').textContent = 'THREW (pre-fix bug): ' + e.message;
}
}
function toggleShow() {
shown = !shown;
grid.setFooterRowVisibility(shown);
document.getElementById('readout').textContent = 'showFooterRow -> ' + shown;
}
</script>
</body>
</html>
71 changes: 51 additions & 20 deletions src/slick.grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,26 +853,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

// footer Row
if (this._options.createFooterRow) {
this._footerRowScrollerR = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopR);
this._footerRowScrollerL = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopL);

this._footerRowScroller = [this._footerRowScrollerL, this._footerRowScrollerR];

this._footerRowSpacerL = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerL);
Utils.width(this._footerRowSpacerL, canvasWithScrollbarWidth);
this._footerRowSpacerR = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerR);
Utils.width(this._footerRowSpacerR, canvasWithScrollbarWidth);

this._footerRowL = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-left' }, this._footerRowScrollerL);
this._footerRowR = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-right' }, this._footerRowScrollerR);

this._footerRow = [this._footerRowL, this._footerRowR];

if (!this._options.showFooterRow) {
this._footerRowScroller.forEach((scroller) => {
Utils.hide(scroller);
});
}
this.materializeFooterRow();
}

this._focusSink2 = this._focusSink.cloneNode(true) as HTMLDivElement;
Expand Down Expand Up @@ -1392,6 +1373,12 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
this.validateAndEnforceOptions();
this.setFrozenOptions();

if (this._options.createFooterRow && !this._footerRow) {
this.materializeFooterRow();
} else if (!this._options.createFooterRow && this._footerRow) {
this._footerRowScroller.forEach((scroller) => Utils.hide(scroller));
}

// when user changed frozen row option, we need to force a recalculation of each viewport heights
if (this._options.frozenBottom !== undefined) {
this.enforceFrozenRowHeightRecalc = true;
Expand Down Expand Up @@ -1425,6 +1412,50 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
}
}

/**
* Builds the footer-row DOM (scrollers, spacers and footer-row containers) in both
* panes — the single construction path shared by init and by a runtime
* `setOptions({ createFooterRow: true })` enable. On an already-initialized grid it
* also binds the footer events (during init they are bound in `finishInitialization`).
* Runtime disable hides the footer rather than destroying it (symmetric with
* `showFooterRow`).
*/
protected materializeFooterRow(): void {
const canvasWithScrollbarWidth = this.getCanvasWidth() + (this.scrollbarDimensions?.width ?? 0);

this._footerRowScrollerR = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopR);
this._footerRowScrollerL = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopL);

this._footerRowScroller = [this._footerRowScrollerL, this._footerRowScrollerR];

this._footerRowSpacerL = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerL);
Utils.width(this._footerRowSpacerL, canvasWithScrollbarWidth);
this._footerRowSpacerR = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerR);
Utils.width(this._footerRowSpacerR, canvasWithScrollbarWidth);

this._footerRowL = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-left' }, this._footerRowScrollerL);
this._footerRowR = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-right' }, this._footerRowScrollerR);

this._footerRow = [this._footerRowL, this._footerRowR];

if (!this._options.showFooterRow) {
this._footerRowScroller.forEach((scroller) => {
Utils.hide(scroller);
});
}

if (this.initialized) {
this._footerRow.forEach((footer) => {
this._bindingEventService.bind(footer, 'contextmenu', this.handleFooterContextMenu.bind(this) as EventListener);
this._bindingEventService.bind(footer, 'click', this.handleFooterClick.bind(this) as EventListener);
});

this._footerRowScroller.forEach((scroller) => {
this._bindingEventService.bind(scroller, 'scroll', this.handleFooterRowScroll.bind(this) as EventListener);
});
}
}

/**
*
* Ensures consistency in option setting, by thastIF autoHeight IS enabled, leaveSpaceForNewRows is set to FALSE.
Expand Down
Loading