Django host extensions — renderers, actions, Alpine, counters¶
How to wire host-specific JavaScript when using {% render_grid_view_spec %} in Django.
The spec stays JSON-only; custom cells and page interactions live in the host template +
GridView.registerRenderer / GridView.registerAction.
See also: Django integration, Host contract, AG Grid toolbar counter.
Asset and script order¶
In the host base template, keep this fixed order at the end of <body>:
{% block scripts %}{% endblock %}
{% grid_view_spec_assets part='js' force_core=True %}
{% block after_grid_view_js %}{% endblock %}
| Block | When it runs | Put here |
|---|---|---|
{% block scripts %} |
Before GridView bundle |
Alpine page factories (stockPage()), mixins, window.* JSON blobs |
{% grid_view_spec_assets part='js' %} |
Loads gridviewspec.min.js, optional AG Grid plugin, schedules GridView.bootScope on DOMContentLoaded |
Do not skip on grid pages |
{% block after_grid_view_js %} |
Immediately after bundle parse, before DOMContentLoaded grid boot |
registerRenderer, registerAction, grid post-wire (refreshCells) |
Do not call registerRenderer inside {% block scripts %} — GridView does not exist yet.
Do not put {% block nav_center %} / {% block nav_right %} inside an {% include %} —
Django ignores blocks in included templates. Declare those blocks in the extending base layout
(e.g. _base.html nav actions row).
Custom cell renderers¶
Spec side (page_data):
GridViewColumn(
id="quantity",
field="quantity",
renderer="stock_cart", # registry id, not inline JS
extra={"action": "open_stock_cart", "record_key": "id"},
)
Host side (after_grid_view_js):
(function () {
var gv = window.GridView;
if (!gv || gv.__myPageHooks) return;
gv.__myPageHooks = true;
gv.registerRenderer("stock_cart", function (params) {
var data = params && params.data;
if (!data) return "";
return (
'<button type="button" data-cm-cell-action="open_stock_cart"' +
' data-cm-row-id="' + String(data.id) + '">' + data.quantity + "</button>"
);
});
})();
Built-in registry ids: money, link, badge, date, button, image (see MCP
gridview_catalog → registries.renderers). Anything else must be registered by the host.
column.extra maps to AG Grid cellRendererParams (e.g. action, record_key, badges,
ag_filter).
Custom actions (clicks)¶
Delegated clicks on [data-cm-cell-action] are handled by the package. Register handlers:
gv.registerAction("open_stock_cart", function (ctx) {
var ref = window._stockRef; // Alpine instance — must be on window
if (!ref || !ctx.event) return;
var data = resolveRowData(ctx); // getRowNode + fallback — see below
if (data) ref.openCartPopover({ data: data, event: ctx.event });
});
Use window._pageRef (not let _pageRef in a script tag). Each <script> block has its own
lexical scope; actions run in a different closure.
Row lookup (AG Grid infinite model)¶
Infinite datasource requires getRowId (package boot sets it from data.id). In actions, resolve
row data safely:
function resolveRowData(ctx, gridId) {
var host = window.GridView.byId.get(ctx.gridId || gridId || "stock");
if (!host || !host.gridApi) return null;
if (ctx.rowId) {
var node = host.gridApi.getRowNode(String(ctx.rowId));
if (node && node.data) return node.data;
}
if (!ctx.event || !ctx.event.target) return null;
var rowEl = ctx.event.target.closest(".ag-row");
if (!rowEl) return null;
var idx = rowEl.getAttribute("row-index");
if (idx == null) return null;
var displayed = host.gridApi.getDisplayedRowAtIndex(Number(idx));
return displayed && displayed.data ? displayed.data : null;
}
After registering renderers, refresh once the grid exists:
function refreshGrid(gridId) {
var host = window.GridView.byId.get(gridId);
if (host && host.gridApi) host.gridApi.refreshCells({ force: true });
}
document.addEventListener("DOMContentLoaded", function () { refreshGrid("stock"); });
Toolbar row counter¶
Use GridViewCounter on GridViewToolbar.counters, not a hidden #row-count span in the nav.
Toolbar template emits:
Wire the table:
_STOCK_TABLE_ID = "stock"
_ROW_FIELD = "row_total"
toolbar = GridViewToolbar(
id="stock_toolbar",
target=_STOCK_TABLE_ID,
counters=(
GridViewCounter(
id="stock_row_count",
label="products",
value=0,
field=_ROW_FIELD,
),
# …static counters (FX badges) without `field`
),
...
)
table = ag_grid_table(
table_id=_STOCK_TABLE_ID,
endpoint="/stock/api/",
columns=columns,
row_count_selector=(
f'[data-cm-count-for="{_STOCK_TABLE_ID}"][data-cm-count-field="{_ROW_FIELD}"]'
),
)
row_count_selector → table.extra.row_count_selector → AG Grid boot rowCountSelector. On each
infinite fetch onLastRow updates that element’s textContent.
When multiple counters share data-cm-count-for, field disambiguates the row-total counter
from static badges.
Page overlays vs toolbar z-index¶
Package toolbar search uses z-index: 200 ([data-cm-toolbar-search-root]). Host modals that use
Tailwind z-50 (50) will render under the search bar.
In host CSS, raise page overlays above toolbar chrome:
Popovers (not full-screen): e.g. z-index: 250. Package column-settings overlay uses 1300+.
Theming tables (tokens, not internals)¶
Style tables through --cm-* tokens so simple and AG-Grid backends stay in parity.
Do not target package internals in host CSS — they are not a stable contract:
.ag-header*, .ag-icon*, .ag-filter*, .cm-table thead, .cm-sort-arrow,
.cm-col-filter-*, .cm-set-filter-*.
Common host needs and their tokens:
| Need | Token(s) |
|---|---|
| Header background | --cm-table-head-bg (→ --cm-table-header-bg) |
| Section divider under header | --cm-table-header-divider |
| Header font size | --cm-table-header-font-size |
| Sort/filter icon idle / hover opacity | --cm-table-icon-idle-opacity, --cm-table-icon-hover-opacity |
| Filter control always visible (not hover) | --cm-th-filter-slot-flex: 0 0 auto; --cm-th-filter-slot-width: auto; --cm-th-filter-slot-overflow: visible |
| Lock a column's sort+filter controls | --cm-th-controls-visibility / --cm-th-controls-pointer-events set on th[data-cm-col-key="…"] |
See Table backend parity plan for the full token list and behavioral hooks.
Checklist (new grid page)¶
page_data→GridViewSpecwithGridViewTable(backend="ag_grid"), toolbartarget=table_id.{% render_grid_view_spec page.grid %}in template shell.validate_spec/ MCPgridview_validate→ok: true.- Custom
rendererids registered inafter_grid_view_js. - Alpine ref on
window._*Refif actions/modals need component methods. - Row counter:
GridViewCounter+row_count_selector, not hidden nav spans. - Modals: z-index above toolbar (see above).
- Nav slots:
nav_center/nav_rightin base layout, not inside{% include %}.
Host reference layout¶
A host typically wires these pieces together:
host/shared/_base.html— script order, nav blockshost/shared/_grid_page.html—render_grid_view_spec page.gridhost/pages/stock.html— page hooks + Alpine cart/detailhost/views/stock_page_data.py— counters +row_count_selectorhost/static/host/css/cm-host.css— host tokens, modal z-index, filter panel50vh