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 guide › Editing › 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.
| Event | Fired on | Carries |
|---|---|---|
| beforeRowReceive | the target, before the insert | { data, at, overKey, source }, cancellable |
| rowReceive:cancelled | the target, on a veto | { data, at, overKey, source, reason } |
| row:received | the target | { data, at, overKey, rejected } |
| row:sent | the source, on a move | { key, data, mode } |
| row:copied | the 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.
| Event | Fires when | Carries |
|---|---|---|
| rowDrag:started | the press passed the drag threshold | { key, data, over, at, overKey } |
| rowDrag:moved | the pointer is over a candidate position; at most once per animation frame | { key, data, over, at, overKey } |
| rowDrag:left | the pointer left a grid; over is the grid it left | { key, data, over, at: null, overKey: null } |
| rowDrag:ended | the 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.