Lattice Grid Buy a licence

api reference

List View API Reference

createList: a standalone, template-driven list over a real grid, with selection, drag to reorder, move or copy between lists, and a data API.

API reference › The list view module

All 29 pages Everything on one page → Developer guide →

The list view module

modules/listview is an opt-in, standalone list: selection, drag-and-drop reorder and transfer between lists, and a data API, over one column of a real Lattice grid. House rule: a module's list or table is a real grid - createList configures the page's own createGrid (injected, never bundled, so the list shares the page's one licensed core) with no header, no grid lines, one row per item, the row's cell drawn by your own template. Virtual scrolling, keyboard navigation, ARIA and row drag all come from the grid underneath for nothing - this module is wiring, not a second implementation of any of them.

import { createList } from '@toclocoinc/lattice-grid/modules/listview';

const list = createList(document.querySelector('#list'), {
  createGrid,                     // the page's own grid factory (injected, never bundled)
  data: [{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }],
  template: (item) => `<div class="row">${item.name}</div>`,
  selection: 'multi',
  dragMode: 'both',
});

Drag and drop is two already-shipped grid primitives, wired, not rebuilt. dragMode maps onto the inner grid's own rowReorder (the handle, and reorder within one list) and rowTransfer (rows crossing between two grid instances): 'source' only sends, 'target' only receives, 'both' does either, and false (the default) does neither. Neither list needs a shared group to interact - any two lists on a page already match - so moving or copying (dragCopy: true) between two listview instances needs no pairing registry of its own. A target's beforeDrop handler may veto a drop by returning false (or a Promise of it): the same veto-before-mutation gate the grid's own rowTransfer uses guarantees a veto leaves both lists exactly as they were, and afterDrop fires only once a move or copy has actually committed. A drag that leaves a list without landing on another fires dragOut.

The Data API never drifts from what is on screen. list.data drives every mutation through the inner grid's own keyed-diff, sort and filter surface, so serialize() - the items in current on-screen order - can never disagree with what is actually painted, and filter(fn) hides items without removing them from the store (call with no arguments to clear it). list.selection is driven through the same inner grid's own selection model, so a click, a keypress and a direct API call all agree; in 'single' mode getId() is the selected id or null, and in 'multi' mode it is always an array. An input or change event fired by a control inside a row's own template bubbles out through list.on carrying that row's item id, so a host never has to walk the DOM to know which item a field belongs to.

Row size, controls in templates and where a drag starts. The single item column fills the list's width and follows host resizes, so a template's block content spans the row. itemHeight is a number of pixels, or 'auto': every row is then sized to its template with the grid's own auto row height (multi-line content is shown in full, it measures at any list length, and a row whose item changes through data.update is measured again). An HTML-string template is sanitised by the grid, which drops form controls; with allowUnsafeTemplates: true it keeps input, select, textarea, button and label elements and their attributes, while script elements, event-handler attributes and javascript: URLs are still removed. Typing or clicking in such a control reaches list.on('input' | 'change') with the item id, and a click on a link, button, input, select, textarea or label does not also select the row unless the control (or an ancestor) carries data-row-select. dragHandle is 'grip' (the default, the drawn handle) or 'row', which also starts a drag from anywhere on the row except on those controls or an element marked data-no-drag; a double-click still selects a word. A row moved to another list leaves the source completely - its rows, paint, order and selection - and a copy leaves the source untouched.

MemberDescription
createList(el, config)Create the list. config.createGrid and config.template are required.
data.parse(items) / data.add(item, index) / data.remove(id) / data.removeAll()Replace the whole dataset, insert at a display index (default the end), remove by identity, or clear everything.
data.update(id, patch) / data.getItem(id)Shallow-merge a patch into one item and re-render only that row; read the item stored under an identity.
data.getIndex(id) / data.getLength() / data.forEach(fn)The item's current display index (-1 when filtered out or unknown); the total stored count; iterate in display order.
data.serialize()The items in current on-screen order - always what is painted.
data.filter(fn) / data.sort(fnOrField, dir)Hide items a predicate refuses without losing them (no arguments clears it); reorder by a comparator or a field name.
selection.add(id) / selection.remove(id) / selection.getId() / selection.clear()Select, deselect, read (bare id/null in 'single' mode, always an array in 'multi'), or clear the selection.
on(name, fn) / off(name, fn)Events: itemClick, itemDoubleClick, the cancellable beforeDrop, afterDrop, dragOut, selectionChange, and template-bubbled input/change.
destroy()Destroy the inner grid, remove the delegated listeners, drop every handler. The host still owns the element.

The data API, selection and config knobs, executed

Three items seeded, an insert, a patch, a field sort, a filter that hides without losing anything, selection add/remove/clear, a removal and a full clear - run headless on every build with the suite's own DOM test double, since this module always needs a real element and an injected grid.

const { createTestDom, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createList } = await import('../packages/modules/listview/index.js');

const seen = [];
const host = createTestDom().root;
const list = createList(host, {
  createGrid,
  data: [{ id: 1, name: 'Ada', done: false }, { id: 2, name: 'Grace', done: true }, { id: 3, name: 'Linus', done: false }],
  key: 'id',
  template: (item) => `<div class="row">${item.name}</div>`,
  itemHeight: 40,
  css: 'demo-row',
  selection: 'multi',
  dragMode: 'both',
  dragCopy: false,
  dragHandle: 'row',
  emptyText: 'No items',
});
list.on('selectionChange', (e) => seen.push(`sel:${e.ids.join(',')}`));
list.on('afterDrop', (e) => seen.push('afterDrop'));

const initial = list.data.serialize().map((i) => i.name).join(',');     // Ada,Grace,Linus
list.data.add({ id: 4, name: 'Margaret', done: false }, 1);
const afterAdd = list.data.serialize().map((i) => i.id).join(',');       // 1,4,2,3
list.data.update(2, { name: 'Grace H.' });
const updated = list.data.getItem(2).name;                               // Grace H.
list.data.sort('name');
const sorted = list.data.serialize().map((i) => i.id).join(',');
list.data.filter((item) => item.done === false);
const filteredCount = list.data.serialize().length;
const totalCount = list.data.getLength();
list.data.filter();
list.data.sort();
const restored = list.data.serialize().map((i) => i.id).join(',');       // 1,4,2,3
const indexOf4 = list.data.getIndex(4);
list.selection.add([1, 4]);
const selected = list.selection.getId().join(',');
list.selection.remove(1);
const afterRemove = list.selection.getId().join(',');
list.selection.clear();
const cleared = list.selection.getId().join(',') || 'none';
list.data.remove(3);
const afterDelete = list.data.serialize().map((i) => i.id).join(',');
list.data.removeAll();
const empty = list.data.getLength();
const events = seen.join('|');
list.destroy();
return [initial, afterAdd, updated, sorted, filteredCount, totalCount, restored, indexOf4, selected, afterRemove, cleared, afterDelete, empty, events].join(' || ');

Type reference

Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.

The list view module

ListViewConfig

The configuration for {@link createList}. `createGrid` is the page's own grid factory, injected so the list shares the page's one licensed core - `createList` wraps it as a one-column, headerless, borderless grid, one row per item, the cell drawn by `template`.

PropertyTypeDescription
dataListViewItem[]The initial items. Default `[]`. (optional)
keystringThe field each item's identity comes from. Default `'id'`. (optional)
itemHeightnumber | 'auto'Row height in pixels, or `'auto'` to size every row to its template: multi-line content is shown in full (the grid's own auto row height, measured at any list length), and a row whose item changes through `list.data.update` is measured again. Default: the grid's row height. (optional)
allowUnsafeTemplatesboolean`true` lets an HTML-string `template` keep form controls: `input`, `select` (with `option`), `textarea`, `button` and `label` elements and their attributes. `script`, `iframe`, `object`, `embed`, `form` and similar elements, event-handler attributes and `javascript:`, `data:` or `vbscript:` URLs are still removed. Default `false`, where the grid's own sanitiser also drops the controls. Typing into or clicking a control reaches `list.on('input' | 'change')` with the item's id; a click on a link, button, input, select, textarea or label does not also select the row, unless the control or an ancestor carries `data-row-select`. (optional)
dragHandle'grip' | 'row'Where a drag starts. `'grip'` (default) is the drawn handle. `'row'` starts it from anywhere on the row except a link, button, input, select, textarea, label or an element marked `data-no-drag`; a double-click still selects a word, and the grip is still drawn. Applies to the lists that can start a drag (`dragMode` `'source'` or `'both'`). (optional)
cssstring | string[]Class name(s) added to every row. (optional)
selectionfalse | 'single' | 'multi'Selection mode. Default `false`. (optional)
dragModefalse | 'source' | 'target' | 'both'This list's role in a cross-list drag. Default `false`. (optional)
dragCopyboolean`true` leaves the source row in place on a drop (copy, not move). Default `false`. (optional)
emptyTextstring | HTMLElementThe empty-state message or element. (optional)
contextMenubooleanThe grid's own cell right-click menu (Paste, Clear, Fill down). Default `false` - a right-click on a row is `itemContextMenu`, not a menu meant for spreadsheet cells. (optional)
MethodSignatureParametersReturnsDescription
createGrid(el: HTMLElement, config: unknown) => unknownel: HTMLElement
config: unknown
=> unknownThe grid factory the list mounts through (required).
template(item: ListViewItem) => string | HTMLElementitem: ListViewItem=> string | HTMLElementCalled per item to draw its row; a returned string goes through the grid's own sanitiser (see `allowUnsafeTemplates`).

ListViewDataApi

`list.data`: the Data API. Every method runs through the inner grid's own keyed-diff/sort/filter surface, so {@link serialize} never drifts from what is on screen.

MethodSignatureParametersReturnsDescription
parse(items: ListViewItem[]): voiditems: ListViewItem[]voidReplace the whole dataset, keyed by `ListViewConfig.key`.
add(item: ListViewItem, index?: number): voiditem: ListViewItem
index?: number
voidInsert an item at a display index (default: end).
remove(id: unknown): voidid: unknownvoidRemove the item with this identity.
removeAll(): void - voidRemove every item.
update(id: unknown, patch: Partial<ListViewItem>): voidid: unknown
patch: Partial<ListViewItem>
voidShallow-merge `patch` into the item identified by `id`.
getItem(id: unknown): ListViewItem | undefinedid: unknownListViewItem | undefinedThe item stored under an identity, or undefined.
getIndex(id: unknown): numberid: unknownnumberThe item's current display index, or -1 when not currently displayed.
getLength(): number - numberThe total stored item count, filtered or not.
forEach(fn: (item: ListViewItem, index: number) => void): voidfn: (item: ListViewItem, index: number) => voidvoidIterate the items, in current display order.
serialize(): ListViewItem[] - ListViewItem[]The items in current on-screen order.
filter(fn?: (item: ListViewItem) => boolean): voidfn?: (item: ListViewItem) => booleanvoidHide items a predicate refuses, without removing them; call with no arguments to clear.
sort(fnOrField?: ((a: ListViewItem, b: ListViewItem) => number) | string, dir?: 'asc' | 'desc'): voidfnOrField?: ((a: ListViewItem, b: ListViewItem) => number) | string, dir?: 'asc' | 'desc'voidChange display order: a comparator, or a field name with `dir` (default `'asc'`).

ListViewSelectionApi

`list.selection`: the selection API, driven through the inner grid's own selection model.

MethodSignatureParametersReturnsDescription
add(id: unknown | unknown[]): voidid: unknown | unknown[]voidSelect one item, or several, alongside whatever is already selected.
remove(id: unknown | unknown[]): voidid: unknown | unknown[]voidDeselect one item, or several.
getId(): unknown | unknown[] | null - unknown | unknown[] | nullIn `'single'` mode: the selected id, or null. In `'multi'` mode: always an array.
clear(): void - voidDeselect everything.

ListViewItemEvent

The payload `itemClick`/`itemDoubleClick` carry.

PropertyTypeDescription
idunknownThe clicked item's identity.
itemListViewItem | nullThe clicked item, or null.
indexnumberThe item's display index.
originalEventunknownThe originating DOM event.

ListViewBeforeDropEvent

The cancellable `beforeDrop` payload: return `false` (or a Promise of it) to veto, leaving both lists unchanged.

PropertyTypeDescription
sourceListView | import('lattice-grid/modules/dataview').DataView | nullThe list or data view the dragged item(s) came from, or null when it did not come from one.
targetListViewThis list, the drop target.
idsunknown[]The dragged item's identity (identities), as a one-element array.
indexnumberThe candidate display index of the drop.

ListViewAfterDropEvent

The `afterDrop` payload: a move or copy between two lists (or a reorder within one) committed.

PropertyTypeDescription
sourceListView | import('lattice-grid/modules/dataview').DataView | nullThe list or data view the item(s) came from.
targetListView | import('lattice-grid/modules/dataview').DataView | nullThe list or data view the item(s) landed in.
idsunknown[]The moved/copied item's identity (identities), as a one-element array.
indexnumberThe index the item(s) landed at in the target.

ListViewDragOutEvent

The `dragOut` payload: a drag left this list without a cross-list drop landing.

PropertyTypeDescription
idunknownThe dragged item's identity.
indexnumberThe item's display index before the drag.

ListViewSelectionChangeEvent

The `selectionChange` payload: always an array, regardless of `ListViewConfig.selection` mode.

PropertyTypeDescription
idsunknown[]The selected ids.

ListViewTemplateEvent

The `input`/`change` payload: an event bubbled from inside a row's template, carrying the item's identity.

PropertyTypeDescription
idunknownThe row's item identity.
itemListViewItem | nullThe row's item, or null.
originalEventunknownThe originating DOM event.

ListViewItemContextMenuEvent

The `itemContextMenu` payload: a right-click on a row (and the context-menu key on a focused one).

PropertyTypeDescription
idunknownThe row's item identity.
itemListViewItem | nullThe row's item, or null.
eventEvent | undefinedThe originating DOM event, when the right-click came from the pointer (absent from the keyboard route). Call `preventDefault()` on it to suppress the browser's own menu; left alone otherwise.

ListViewEventPayloads

The payload each list event name carries.

PropertyTypeDescription
itemClickListViewItemEventA row was clicked.
itemDoubleClickListViewItemEventA row was double-clicked.
beforeDropListViewBeforeDropEventA cross-list drop is about to land; return `false` (or a Promise of it) to veto it.
afterDropListViewAfterDropEventA reorder, move or copy committed.
dragOutListViewDragOutEventA drag left this list without a cross-list drop landing.
selectionChangeListViewSelectionChangeEventThe selection changed.
inputListViewTemplateEventAn `input` event bubbled from inside a row's template.
changeListViewTemplateEventA `change` event bubbled from inside a row's template.
itemContextMenuListViewItemContextMenuEventA row was right-clicked, or its context-menu key pressed while focused.

ListView

The list instance {@link createList} returns.

Properties
PropertyTypeDescription
dataListViewDataApiThe Data API.
selectionListViewSelectionApiThe selection API.
Methods
MethodSignatureParametersReturnsDescription
on<K extends ListViewEventName>(name: K, fn: (payload: ListViewEventPayloads[K]) => void): () => voidname: K
fn: (payload: ListViewEventPayloads[K]) => void
() => voidSubscribe to a list event; returns an unsubscribe function.
off<K extends ListViewEventName>(name: K, fn: (payload: ListViewEventPayloads[K]) => void): voidname: K
fn: (payload: ListViewEventPayloads[K]) => void
voidRemove an event handler registered with `on`.
destroy(): void - voidTear the list down: destroy the inner grid, remove listeners, drop handlers. The host still owns the element.
Events

This surface raises events, but the names are not declared: on() accepts any string. They cannot be listed here until the declaration names them.