PGrid docs

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

HookWhen 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.cellThe .pgrid-cell DOM node.
e.cellContentThe inner .pgrid-cell-content wrapper where you should write content.
e.rowIndex / e.colIndexVisible coordinates. Header rows are 0..headerRowCount-1.
e.rowId / e.fieldStable id and field name (undefined for header cells).
e.dataThe value being rendered.
e.handledSet 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):

SelectionExtensionClick + arrow keys + Tab. Loaded by selection.
EditorExtensionBuilt-in text editor. Loaded by editing.
CopyPasteExtensionRange copy/paste. Loaded by copypaste.
ViewUpdaterExtensionRe-render on data changes. Loaded by autoUpdate.
FormatterExtensionHonors column.formatter. Loaded by columnFormatter.
ColumnResizeExtensionDrag handles on header. Loaded by columnResize.
TextOverflowExtensionCascaded overflow modes. Loaded by textOverflow.

Two extensions are passed in extensions: [] rather than via toggles:

CheckboxColumnExtensionRenders 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!
    }
};
Common bug: forgetting to clean up in cellAfterRecycled causes stale styling to "leak" onto wrong cells as the user scrolls.