Column Interaction
Resize columns by dragging their edge, and select cells, columns, and ranges with the pointer.
Beyond styling, the grid ships two built-in pointer interactions for columns: drag-to-resize and cell/column/range selection. Both render their own visuals - the resize handle and the blue selection overlay come from the grid's stylesheet - so a sample only sets a flag or forwards a click.
This is a different system from the rule-based selectAll(...) styling used in
Column Selection: that paints cells from facet
predicates, while the API here selects data-index ranges and draws an overlay.
Resize columns
Column resizing is on by default: with enableResizeUI: true, every column facet
cell grows a drag handle on its right edge. Dragging it resizes the column and
double-clicking it auto-fits to content - the grid wires both, so the sample sets
the flag and nothing else.
const grid = new Grid({ enableResizeUI: true }, mount, "flat"); // default is true
// drag a column's right-edge handle to resize; double-click the handle to auto-fitenableResizeUI is read once at construction, and a manual width is stored on the
layout, so toggling the handles or resetting to the configured widths is done by
rebuilding the grid.
Try it below: drag the right edge of any column, or double-click that edge to fit the column to its widest cell. Toggle the handles off, or reset the widths, from the toolbar.
Source
import Grid, { FlattenedDataViewModel } from "grid/dist/renderer";
import { createToolbar, toolbarButton } from "../../runtime/toolbar";
import { syncGridTheme } from "../../runtime/theme";
import type { SampleContext, SampleRow } from "../../types";
const COLUMNS = [
{ field: "agency_name", label: "Agency" },
{ field: "title_description", label: "Title" },
{ field: "base_salary", label: "Base Salary" },
{ field: "regular_gross_paid", label: "Regular Gross Paid" },
{ field: "total_ot_paid", label: "Total OT Paid" },
];
// Static fractional widths: the two text columns take twice the share of the
// three numeric ones, so the table fills the grid before any manual resize.
const COL_FR = [2, 2, 1, 1, 1];
const GRID_HEIGHT = 420;
export function mount(el: HTMLElement, ctx: SampleContext): () => void {
el.style.cssText = "display:flex;flex-direction:column;gap:12px;";
const toolbar = createToolbar();
const hint = document.createElement("span");
hint.style.cssText = "font-size:12px;opacity:0.75;";
hint.textContent = "Drag a column's right edge to resize; double-click the edge to auto-fit.";
const toggle = toolbarButton("Resize handles: on");
const reset = toolbarButton("Reset widths");
toolbar.append(hint, toggle, reset);
const gridSlot = document.createElement("div");
el.append(toolbar, gridSlot);
let rows: SampleRow[] = [];
let disposed = false;
// `enableResizeUI` is a construction-time flag and manual widths live on the
// layout, so both flipping the handles and resetting to the configured widths
// are done by rebuilding the grid onto a fresh mount.
let resizeEnabled = true;
let disposeTheme: () => void = () => {};
const build = (): void => {
disposeTheme();
const mountEl = document.createElement("div");
mountEl.style.cssText =
`position:relative;height:${GRID_HEIGHT}px;overflow:auto;border-radius:8px;` +
"border:1px solid color-mix(in srgb, currentColor 15%, transparent);";
gridSlot.replaceChildren(mountEl);
// With enableResizeUI on, every column facet cell grows a drag handle on its
// right edge; the grid wires the drag and the double-click auto-fit itself.
const grid = new Grid({ enableResizeUI: resizeEnabled }, mountEl, "flat");
disposeTheme = syncGridTheme(grid);
grid.data = new FlattenedDataViewModel({
data: COLUMNS.map((column) => rows.map((row) => row[column.field] ?? null)),
columnFacets: [COLUMNS.map((column) => column.label)],
totalRows: rows.length,
options: {
vTrackDefs: COLUMNS.map((_, i) => ({ colSize: { strategy: "static", width: COL_FR[i], unit: "fr" } })),
facetDefs: { row: [], col: [{ text: "" }], axis: "col" },
},
});
grid.draw();
};
toggle.addEventListener("click", () => {
resizeEnabled = !resizeEnabled;
toggle.textContent = resizeEnabled ? "Resize handles: on" : "Resize handles: off";
build();
});
reset.addEventListener("click", build);
ctx.loadDataset("payroll").then((loaded: SampleRow[]) => {
if (disposed) return;
rows = loaded;
build();
});
return () => {
disposed = true;
disposeTheme();
el.replaceChildren();
};
}Select cells, columns, and ranges
The selection API works in data indices and renders a blue overlay itself, so
after a call there is no draw() to make. It is purely programmatic - no pointer
handling is built in - so the sample forwards clicks and drags to it. Every cell
carries its data index in data-* attributes, which is what maps a DOM target to
a selection call.
// read the index off the clicked cell, then select
grid.selectColumnByDataIndex(colIndex); // whole column - columns accumulate
grid.selectCellByDataIndex(rowIndex, colIndex); // a single cell
grid.selectRangeByDataIndex(r1, c1, r2, c2); // a rectangle - clears other selections
grid.clearAllSelections();The indices come straight off the DOM: a data cell exposes data-croix /
data-cclix (its data row / column), and a column header exposes data-hix (its
leaf column). A mousedown reads them and calls the matching method; a drag
re-issues selectRangeByDataIndex as the pointer moves.
const cell = (event.target as Element).closest('[data-cell-type="value"]');
grid.selectCellByDataIndex(+cell.dataset.croix, +cell.dataset.cclix);Try it below: click a header to select its column (click more headers to add them), click a single cell, or drag across cells to sweep out a range. Clear removes every selection.
Source
import Grid, { FlattenedDataViewModel } from "grid/dist/renderer";
import { createGridMount } from "../../runtime/mount";
import { createToolbar, toolbarButton } from "../../runtime/toolbar";
import { syncGridTheme } from "../../runtime/theme";
import type { SampleContext, SampleRow } from "../../types";
const COLUMNS = [
{ field: "agency_name", label: "Agency" },
{ field: "title_description", label: "Title" },
{ field: "base_salary", label: "Base Salary" },
{ field: "regular_gross_paid", label: "Regular Gross Paid" },
{ field: "total_ot_paid", label: "Total OT Paid" },
];
const COL_FR = [2, 2, 1, 1, 1];
const GRID_HEIGHT = 420;
export function mount(el: HTMLElement, ctx: SampleContext): () => void {
const gridMount = createGridMount(el, GRID_HEIGHT);
// Resize UI off so a header click always selects the column rather than
// catching the resize handle on the cell's edge.
const grid = new Grid({ enableResizeUI: false }, gridMount, "flat");
const disposeTheme = syncGridTheme(grid);
let disposed = false;
ctx.loadDataset("payroll").then((rows: SampleRow[]) => {
if (disposed) return;
grid.data = new FlattenedDataViewModel({
data: COLUMNS.map((column) => rows.map((row) => row[column.field] ?? null)),
columnFacets: [COLUMNS.map((column) => column.label)],
totalRows: rows.length,
options: {
vTrackDefs: COLUMNS.map((_, i) => ({ colSize: { strategy: "static", width: COL_FR[i], unit: "fr" } })),
facetDefs: { row: [], col: [{ text: "" }], axis: "col" },
},
});
grid.draw();
});
// The selection API is programmatic: it maps data indices to a blue overlay and
// redraws on its own. Here pointer events drive it - a header click selects the
// whole column (columns accumulate), a plain cell click selects one cell, and a
// drag selects a range (which clears any previous selection).
let anchor: { row: number; col: number } | null = null;
let dragged = false;
const onMouseDown = (event: MouseEvent): void => {
const header = closestOfType(event.target, "column-facet");
if (header) {
grid.selectColumnByDataIndex(parseInt(header.dataset.hix!, 10));
return;
}
const cell = closestOfType(event.target, "value");
if (!cell) return;
anchor = cellIndex(cell);
dragged = false;
event.preventDefault();
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
const onMouseMove = (event: MouseEvent): void => {
if (!anchor) return;
const cell = closestOfType(document.elementFromPoint(event.clientX, event.clientY), "value");
if (!cell) return;
const here = cellIndex(cell);
if (!dragged && here.row === anchor.row && here.col === anchor.col) return;
dragged = true;
grid.selectRangeByDataIndex(anchor.row, anchor.col, here.row, here.col);
};
const onMouseUp = (): void => {
if (anchor && !dragged) grid.selectCellByDataIndex(anchor.row, anchor.col);
anchor = null;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
gridMount.addEventListener("mousedown", onMouseDown);
const toolbar = createToolbar();
const hint = document.createElement("span");
hint.style.cssText = "font-size:12px;opacity:0.75;";
hint.textContent = "Click a header to select a column, click a cell, or drag across cells for a range.";
const clear = toolbarButton("Clear");
toolbar.append(hint, clear);
el.append(toolbar, gridMount);
clear.addEventListener("click", () => grid.clearAllSelections());
return () => {
disposed = true;
disposeTheme();
onMouseUp();
gridMount.removeEventListener("mousedown", onMouseDown);
el.removeChild(toolbar);
el.removeChild(gridMount);
};
}
/* ===================================== utils ===================================== */
// The layout tags every cell with its type; data cells also carry their data
// row/column index (`croix`/`cclix`) and column facet cells their leaf column
// index (`hix`). Reading those turns a DOM target into a selection call.
function closestOfType(target: EventTarget | null, type: "value" | "column-facet"): HTMLElement | null {
return target instanceof Element ? target.closest<HTMLElement>(`[data-cell-type="${type}"]`) : null;
}
function cellIndex(cell: HTMLElement): { row: number; col: number } {
return { row: parseInt(cell.dataset.croix!, 10), col: parseInt(cell.dataset.cclix!, 10) };
}