developer guide
JavaScript Data Grid Row Selection and Ranges
Selection covers a single row, many rows, and a spreadsheet-style block of cells. Add a checkbox column with a select-all in the header, add row numbers down the side, and read or set what is chosen through grid.selection at any time.
Developer guide › Editing › JavaScript Data Grid Row Selection and Ranges
Selection and ranges
Row selection and cell ranges are separate answers to separate questions, which records versus which values. Dragging across cells does not tick row checkboxes, and selecting rows does not build a range.
Cell ranges are on by default; row selection is not. Set
selection: 'single' or 'multiple' to turn rows on: until you do,
selection.set() accepts the call and keys() comes back empty.
Both
selection: 'multiple' // rows are off until you ask
// Rows, which records
grid.selection.set(['r1', 'r2']);
grid.selection.keys(); // the selected row keys
grid.selection.rows(); // the row wrappers
grid.selection.all(); // select everything that passes the filter
grid.selection.clear();
// Cells, which values
grid.selection.setRange({ startRow: 0, endRow: 9, columns: ['cap', 'margin'] });
grid.selection.cells(); // [{ key, colId }, …]
grid.selection.summary(); // count, sum, min, max, avg over the range
grid.selection.clearRange();
// Several blocks at once: ctrl-click, Ctrl+Shift+Arrow, or from the API
grid.selection.addRange({ startRow: 20, endRow: 29, columns: ['cap'] });
grid.selection.extendRange(34, 'cap'); // grows the block just added
The checkbox column
A column of checkboxes, with select-all in the header
selection: { mode: 'multiple', checkbox: true, headerCheckbox: true }
checkbox: true adds a narrow column of checkboxes at the start of every row,
pinned so it does not scroll away. headerCheckbox: true puts a select-all box in
its heading, which shows three states: unchecked when nothing is selected, checked when
everything is, and the native indeterminate mark when some are. Clicking it selects
everything when it is not already full, and clears when it is: including from the
indeterminate state, where the intent is "select the rest".
"Everything" means every row the filter currently shows, not every row loaded. The column is
generated rather than declared: it does not appear in columns.visible(), in a
saved view, in an export or in the tool panel's visibility list, and it disappears when the
option is turned off. grid.selection.headerState() returns the same tri-state the
header shows, for building your own control.
Selected rows carry aria-selected and the class
lat-row--selected, which the theme styles.
Sizing and pinning the checkbox column, and the resize-drag preview
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
// selectionColumnWidth/selectionColumnPin are grid-level configuration keys,
// not part of the `selection` block, because they size and place the
// generated column rather than choosing what it does. resizePreview is a
// renderer concern (what a column-border drag shows while it is in
// progress); the config key itself is core state, readable without a DOM.
const grid = createHeadlessGrid({
columns: [{ field: 'id' }],
rows: [{ id: 1 }],
resizePreview: 'deferred', // default 'live'
selection: { checkbox: true },
selectionColumnWidth: 60, // default 44
selectionColumnPin: 'end', // default 'start'
});
return `${grid.get('resizePreview')} | ${grid.get('selectionColumnWidth')} | ${grid.get('selectionColumnPin')}`;
All three are live: grid.set('resizePreview', …)/grid.set
('selectionColumnWidth', …)/grid.set('selectionColumnPin', …) change the
drag preview, or resize/re-pin the column, immediately.
A row with its own click action
selection: { mode: 'multiple', checkbox: true, checkboxOnly: true }
checkboxOnly: true restricts row selection to the checkbox column:
clicking the checkbox selects or deselects the row, and clicking anywhere else in the
row does neither. This is for a host that binds its own action - typically opening a
record's detail view - to a plain click on the row: without it, that click also
selects the row, and a bulk action run afterwards operates on rows the user never
chose to select. The same restriction applies to the keyboard: Space still toggles
selection while focus is on the checkbox cell, and does nothing elsewhere. Cell ranges
and the fill handle are unaffected either way. Off by default, so a plain click still
selects a row exactly as it always has.
checkboxOnly only narrows which gesture may change selection; it does not
grant selection where mode: 'none' has already refused it, and it composes
normally with mode: 'single' - the checkbox remains the only way to change
which one row is selected.
With mode: 'single', checking a row's box selects that row and replaces
whichever one was selected before; unchecking the selected row clears the selection.
grid.selection.set(keys) itself keeps only the first key of whatever
array it is given when mode is 'single' - that contract is
unchanged - so the checkbox column never builds a two-key array to hand
it; it sets the one key it just toggled.
The row-number column
A running position, pinned ahead of the checkbox
rowNumbers: true
rowNumbers: true adds a column showing each row's 1-based position among the
rows currently visible - after sort, filter and grouping - pinned left ahead of the
selection checkbox column when one is configured. It is generated the same way the
checkbox is: it does not appear in columns.visible(), in columns.state()
or a saved view, and it is never editable, sortable, filterable, groupable or movable.
A group heading, a group footer, a grand total and a master's open detail row are none
of them numbered - each is a summary or an expansion, not a row of its own - and the
leaves around them continue the count with no gap or jump. A config.tree
branch is different and does take a number: it is a real position in the hierarchy you
configured, the same as a leaf, not a summary derived over rows shown elsewhere.
Pinned-top rows are numbered first, ahead of the scrolling body.
A fixed width, a different start, and a header title
rowNumbers: { width: 48, start: 0, title: '#' }
An object fixes the column's width instead of auto-fitting it to the largest number,
moves where the count starts (0 numbers from zero rather than one), and
gives the header a title, which is empty by default. Excluded from CSV, Excel and
clipboard export by default, the same as the checkbox column; ask for it explicitly with
grid.export.csv({ rowNumbers: true }) (and the matching option on
grid.export.excel() and grid.export.clipboard()).
Renumbered after a sort; excluded from export until asked for
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
columns: [{ field: 'region' }],
rows: [
{ id: '1', region: 'East' },
{ id: '2', region: 'North' },
{ id: '3', region: 'South' },
],
rowNumbers: true,
});
// Sorted the numbers still read 1, 2, 3: the position, not the row, is what
// is numbered.
grid.sort.set([{ col: 'region', dir: 'desc' }]);
const numbered = grid.export.csv({ rowNumbers: true, headers: false })
.trim().split('\r\n').map((line) => line.split(',')[0]).join(' ');
// Left out of an ordinary export, the same as the selection checkbox column.
const plain = grid.export.csv({ headers: false }).includes('1,') ? 'has-number' : 'none';
grid.destroy();
return `${numbered} | ${plain}`;
Ctrl+Shift+Arrow is the keyboard form of ctrl-dragging. The first press opens a block at the focused cell without discarding what is already selected; the presses after it extend that block, so holding the chord draws one rectangle rather than a new one per key repeat. A plain Shift+Arrow goes back to extending a single block, and clicking anywhere ends the run.
What multiple ranges do and do not support. Painting, cells()
and the status-bar summary all work over the union of the selected blocks. Copy is narrower,
because tab-separated text is a rectangle: blocks stacked over the same columns, or joined
over the same rows, copy fine, a diagonal pair has no rectangular form, so
rangeText() returns '' and clipboard:copy reports
reason: 'discontiguous' rather than emitting misaligned rows. Filling is
narrower still: with more than one block selected there is no single source to extend, so the
fill handle is hidden and fillTo and fillDown decline.
Turning it off
selection: 'none' // neither
selection: { ranges: false } // rows only, no drag-select