Lattice Grid Buy a licence

developer guide

Drag Rows Between Two JavaScript Data Grids

A row dragged from one grid to another can move or copy, and the receiving grid sees it before it lands: it can change the record, decide where it goes, or refuse it outright. That is what makes an assignment screen, a picker and a basket possible with two grids and no glue.

Developer guideEditing › Drag Rows Between Two JavaScript Data Grids

Moving rows between grids

A row can be dragged out of one grid and into another, a picker beside a basket, an inbox beside a queue, an available list beside an assigned one.

A one-way drag, from a catalogue into a basket

// Sends, never receives. rowReorder draws the handle a drag starts from.
createGrid(left, { columns, rows,
  rowReorder: true,
  rowTransfer: { receive: false, group: 'order' },
});

// Receives, never sends. Draws no handles at all.
createGrid(right, { columns, rows,
  rowTransfer: { send: false, group: 'order' },
});

mode: 'copy' on the sending grid leaves the row where it was. group restricts exchange to grids sharing the same name, so two unrelated grids on a page do not accept each other's rows.

EventFired onCarries
beforeRowReceivethe target, before the insert{ data, at, overKey, source }, cancellable
rowReceive:cancelledthe target, on a veto{ data, at, overKey, source, reason }
row:receivedthe target{ data, at, overKey, rejected }
row:sentthe source, on a move{ key, data, mode }
row:copiedthe source, on a copy{ key, data, mode }

Those events all describe a drag that has settled. To follow one while it is happening - to highlight a candidate row, drive your own drop indicator, or update a side panel as the row travels - there are four more, and they all fire on the grid the drag started in, for a same-grid reorder and a cross-grid transfer alike. A drag is one gesture with one owner, and the source is the only grid present for the whole of it, where the pointer may cross several others or none; over names whichever grid the event is about, so one subscription can decorate any of them.

EventFires whenCarries
rowDrag:startedthe press passed the drag threshold{ key, data, over, at, overKey }
rowDrag:movedthe pointer is over a candidate position; at most once per animation frame{ key, data, over, at, overKey }
rowDrag:leftthe pointer left a grid; over is the grid it left{ key, data, over, at: null, overKey: null }
rowDrag:endedthe gesture ended, drop or no drop{ key, data, over, at, overKey, dropped }

Highlight the row a drag is hovering, and clean up however it ends

backlog.on('rowDrag:moved', (e) => {
  // e.over is the grid under the pointer, null over none.
  paintCandidate(e.over, e.overKey);       // overKey is null where there is no row to name
});
backlog.on('rowDrag:left', (e) => clearCandidate(e.over));
backlog.on('rowDrag:ended', (e) => {
  clearCandidate(e.over);                  // always fires, even released off every grid
  if (!e.dropped) toast('Nothing moved');
});

Notifications, not gates. None of the four is cancellable and none carries preventDefault. The drop is already vetoable twice over - beforeRowMove for a reorder, beforeRowReceive for a drop into another grid - and a third veto on the same gesture would be a third place to look when a drop does not happen.

rowDrag:moved is coalesced to one event per animation frame, carrying that frame's latest pointer position. A pointer produces several hundred moves a second and a handler that draws cannot usefully run faster than the display, so the rate is capped at the display's rather than the pointer's. The other three fire on the transition itself, and no rowDrag:moved is ever delivered after rowDrag:ended.

Read, measure and draw in these handlers; do not mutate. The drag resolves where it would land against the display order, so changing rows, sort, filters or grouping mid-gesture moves the ground under the drop - and data is the source row's own object rather than a copy, so writing to it edits a row that is still in the grid without announcing the change. Work that changes the grid belongs in beforeRowReceive, which is asked before the insert, or in the settled events afterwards.

The end is always reported. Exactly one rowDrag:ended follows every rowDrag:started, including a release outside every grid, where over is null. A press that never passes the drag threshold is a click and raises none of them; a grid destroyed mid-drag raises no rowDrag:ended.

A drop can mean something other than a move. Dragging a backlog row onto a row in another grid often means assign this to that: the host wants to know which row it landed on, record the relationship, and keep the row where it was. beforeRowReceive fires on the receiving grid before the insert, naming the row under the pointer as overKey (null past the last row, on empty space, on the header or on a pinned row) and the grid it came from as source. preventDefault(reason) stops the insert, and the source grid is untouched: the row stays, and neither row:sent nor row:copied fires. The handler may be async, as every before-event may; a drop whose row under the pointer, or source row, is gone by the time it settles is cancelled as 'stale'.

Assign on drop, rather than move

assigned.on('beforeRowReceive', (e) => {
  if (e.overKey === null) return;   // dropped on no row: let it move
  e.preventDefault('assigned');           // the backlog row stays in the backlog
  assign(e.data.id, e.overKey);           // the relationship is the change
});
assigned.on('rowReceive:cancelled', (e) => console.log(e.reason));  // 'assigned'

Off by default, and both ends have to agree. Rows leaving a grid is a data change you have to want: a grid that quietly let its rows be dragged away would lose one to a mis-drag, and there is no gesture a user would think to try to get it back. A one-way relationship is a declaration on both grids rather than a convention, the sender refuses to receive, and the receiver never starts a drag.

The target adds before the source removes. If the add is refused: a duplicate key, most likely: nothing is removed, so a rejected transfer loses no data. The other order would delete a row and then discover it had nowhere to go.

The row object is cloned, not shared. Two grids holding the same object would edit each other's rows through it, which is the sort of coupling nobody goes looking for when a cell changes in a grid they were not touching.

A grid is highlighted while a dragged row is over it only when it would actually accept the drop. Marking one that is going to refuse promises a placement that will not happen. A refusal is announced rather than left silent.

Picking up a row shows it, wherever the pointer goes. The row being dragged dims in its own grid, and a small label naming it follows the pointer for as long as the drag is held: over the gap between two grids, over one that is about to refuse the drop, anywhere the row's own dimming cannot reach. Both clear on release, and a handle press never also starts a range selection underneath it.