Column

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-fit

enableResizeUI 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) };
}

On this page