Extensions
Every built-in feature in PGrid — selection, editing, copy/paste, formatting, resize — is itself an extension. Extensions hook into rendering, data updates, and key events to add behavior without forking the core.
The extension model
An extension is a plain object with an optional init(grid, config) method and any number of named hook methods. PGrid calls each hook at the appropriate point in its lifecycle.
const myExt = {
init(grid, config) {
this._grid = grid;
},
cellAfterRender(e) {
// Called after every cell renders
}
};
new PGrid({ /* … */ extensions: [myExt] });
Multiple extensions can register the same hook — they all run, in load order. Built-in extensions load first (driven by the feature toggles in your config), then your custom extensions.
Hook reference
| Hook | When it fires |
|---|---|
cellRender(e) | Before a cell's text is written. Set e.handled = true to suppress default rendering. |
cellAfterRender(e) | After a cell's content is in place. Add classes, attach listeners. |
cellUpdate(e) | Before a cell is updated in place (without full re-render). |
cellAfterUpdate(e) | After an in-place update. |
cellEditableCheck(e) | Decide whether a cell is editable. Set e.canEdit. |
cellAfterRecycled(e) | When a cell DOM node is reassigned to a different (row, col). Reset state here. |
keyDown(e) | Native KeyboardEvent on the grid, before default handling. |
gridAfterRender(e) | After the initial grid mount. |
dataBeforeRender(e) | Before the grid renders rows from data. |
dataBeforeUpdate(e) | Before a single value writes. Set e.cancel = true to veto, or modify e.data to transform. |
dataAfterUpdate(e) | After a write has been applied. |
dataFinishUpdate(e) | After a batch of writes settles. Receives e.updates. |
Most cell hooks pass an event object with these fields:
e.cell | The .pgrid-cell DOM node. |
e.cellContent | The inner .pgrid-cell-content wrapper where you should write content. |
e.rowIndex / e.colIndex | Visible coordinates. Header rows are 0..headerRowCount-1. |
e.rowId / e.field | Stable id and field name (undefined for header cells). |
e.data | The value being rendered. |
e.handled | Set to true in cellRender to skip default text rendering. |
Writing an extension
Three quick examples — each is just an object literal.
Zebra striping
const zebra = {
cellAfterRender(e) {
if (e.rowIndex % 2 === 1) e.cell.classList.add('row-zebra');
},
cellAfterRecycled(e) {
e.cell.classList.remove('row-zebra');
}
};
Conditional editability
const lockClosedRows = {
cellEditableCheck(e) {
if (e.dataRow && e.dataRow.status === 'Closed') e.canEdit = false;
}
};
Live demo: Conditional editable.
Validating writes
const positiveNumbersOnly = {
dataBeforeUpdate(e) {
if (e.field === 'salary') {
const n = parseFloat(e.data);
if (isNaN(n) || n < 0) e.cancel = true;
else e.data = n;
}
}
};
For a fully worked example with hover highlighting and indicator stripes, see the Custom extensions demo.
Custom editors
Set column.editor to a plain object with two methods:
const dropdownEditor = {
clear(e) { e.done(null); },
attach(e) {
// e.cell — floating editor container positioned over the cell
// e.data — current value
// e.dataRow — full row object
// e.done(v) — call with new value to commit (or no value to cancel)
e.cell.innerHTML = '';
const select = document.createElement('select');
['Active', 'Remote', 'On Leave'].forEach(opt => {
const o = document.createElement('option');
o.value = opt; o.textContent = opt;
if (opt === e.data) o.selected = true;
select.appendChild(o);
});
e.cell.appendChild(select);
select.focus();
select.showPicker?.();
select.addEventListener('change', () => e.done(select.value));
select.addEventListener('blur', () => e.done(select.value));
}
};
columns: [
{ id: 0, field: 'status', title: 'Status', editable: true, editor: dropdownEditor }
]
Live demo: Custom editors (dropdown + date picker + star rating).
Cell formatters
Formatters render the cell's display HTML. They run before the default text renderer and mark the cell as handled.
const moneyFormatter = {
render(e) {
if (e.rowIndex === 0) { // skip header row
e.cellContent.textContent = e.data || '';
return;
}
e.cellContent.textContent = '$' + (e.data || 0).toLocaleString();
}
};
columns: [
{ id: 0, field: 'salary', title: 'Salary', formatter: moneyFormatter }
]
Enable formatters with columnFormatter: true in the grid config. Live demo: Cell formatters.
Built-in extensions
Toggle these via top-level config keys (see Configuration):
SelectionExtension | Click + arrow keys + Tab. Loaded by selection. |
EditorExtension | Built-in text editor. Loaded by editing. |
CopyPasteExtension | Range copy/paste. Loaded by copypaste. |
ViewUpdaterExtension | Re-render on data changes. Loaded by autoUpdate. |
FormatterExtension | Honors column.formatter. Loaded by columnFormatter. |
ColumnResizeExtension | Drag handles on header. Loaded by columnResize. |
TextOverflowExtension | Cascaded overflow modes. Loaded by textOverflow. |
Two extensions are passed in extensions: [] rather than via toggles:
CheckboxColumnExtension | Renders boolean fields as checkboxes. Demo |
Cell recycling
PGrid virtualizes by recycling cell DOM nodes — the same <div> may be reused for cell (5, 2) and then later cell (47, 8). If your extension mutates a cell's classes, listeners, or inline styles, reset that state in cellAfterRecycled.
const myExt = {
cellAfterRender(e) {
e.cell.classList.add('my-marker');
},
cellAfterRecycled(e) {
e.cell.classList.remove('my-marker'); // important!
}
};
cellAfterRecycled causes stale styling to "leak" onto wrong cells as the user scrolls.