Documentation
API: primitives (@fragiola/dockable-react)

Dockable.DragGroup

Layouts of different models that exchange tabs by drag and drop, each side through its model's middleware, keeping content mounted.

"Layouts of different models that exchange tabs by drag and drop. Wrap the Dockable.Roots in it: a tab dragged from one drops into another (a tab.add in the target model and a tab.close in the source, each through its model's middleware, which can veto), and its content keeps its state. Renders no element of its own."

<Dockable.DragGroup onTransfer={(transfer) => history.push(transfer)}>
    <Dockable.Root model={workspace}>{/* … */}</Dockable.Root>
    <Dockable.Root model={scratch}>{/* … */}</Dockable.Root>
</Dockable.DragGroup>

See Drag across windows and layouts and the two layouts example.

Props

DragGroupProps:

proptypedefaultdescription
childrenReactNodenonethe layouts (Dockable.Roots) that exchange tabs, anywhere below
onTransferTransferListener: (transfer: Transfer) => voidnonecalled after a tab moved from one layout to another, with where it came from and where it went (what an app's undo needs)

Transfer

Transfer (a core type, re-exported by @fragiola/dockable-react), what onTransfer receives:

fieldtypedescription
tabIdstringthe tab's id in the target model (its old id, unless that was taken there: then a generated one)
previousIdstringits id in the source model
initTabInitits fields, as it left the source model (component, data, flags, sizes)
fromTransferEndwhere it was
toTransferEndwhere it went

TransferEnd, one side of a transfer:

fieldtypedescription
modelModelHandlethe model. Compare it with yours: transfer.from.model === workspace
layoutIdstringthe layout the tab was (or is now) in
tabsetIdstring | undefinedits tabset (or border), when it has one
indexnumberits position among the tabset's tabs

A ModelHandle is a model of any registry, where only its identity matters: the two models of a transfer may declare different registries, so the handle is untyped. Find your own typed model with ===:

const modelOf = (handle: ModelHandle) => [workspace, scratch].find((model) => model === handle);

How it works

  • Each Dockable.Root inside joins the group (the engine's dragGroup option).
  • A drop into another model runs a tab.add in the target model (the dragged tab's fields, at the drop's placement) and a tab.close in the source, each through its own model's middleware. Both are asked first with model.can: a veto on either side refuses the drop and changes nothing.
  • Both commands carry meta.transfer (TransferMeta, below), so a middleware or a listener can tell a transfer from a plain add or close.
  • When the tab's id is taken in the target, the new tab gets a generated id (transfer.tabId). The new tab adopts the old one's moveable element, so its content moves with it.
  • The group renders every tab's content in one place (directly under it), so moving a tab between roots keeps its component state. Without a DragGroup, panels render their content themselves. The content gets the layout's contexts, but not providers or boundaries placed between the group and the panel: put those above the group, or inside the content.
  • Record history from onTransfer (or each model's model.subscribe), not from middleware: the middleware also sees dry runs and commands another middleware may still veto.
  • A root that unmounts (or gets a new model) leaves the group.

TransferMeta

TransferMeta is the meta of a transfer's tab.add and tab.close (ctx.meta in a middleware, event.meta in a listener): { transfer: { tabId, from, to } }, the tab's id in the source and both models' handles.

workspace.use((ctx, next) => {
    const transfer = (ctx.meta as TransferMeta | undefined)?.transfer;
    if (ctx.command === "tab.close" && transfer) {
        // the tab is leaving for another model (transfer.to), not being closed
    }
    return next();
});

Data attributes

attributevalue
data-layout-pathnone: it renders no element

useDragGroup

useDragGroup() returns the core DragGroup of the enclosing Dockable.DragGroup (it throws outside one), for code that moves tabs between its layouts, as an undo would, or listens to transfers:

const group = useDragGroup();
useEffect(() => group.onTransfer((transfer) => record(transfer)), [group]);

// move a tab back, as an undo would
const moved = group.transfer({ tabId: last.tabId, from: scratch, to: workspace, target: "tabset-1" });
memberdescription
transfer(request: TransferRequest): string | undefinedmoves a tab from one model into another, as a drop would (the same tab.add and tab.close, the same meta.transfer). Returns the new tab's id, or undefined when it could not happen (a refusal, an unknown tab or target). Both models' roots must be mounted in the group
onTransfer(listener: TransferListener): () => voidcalls listener after each transfer (drops and transfer calls alike); returns the unsubscribe function
has(engine), engineOf(model)whether a main layout's engine belongs to the group; the main layout's engine of a model in it

TransferRequest, what transfer takes:

fieldtypedescription
tabIdstringthe tab's id in from
fromModelHandlethe source model
toModelHandlethe target model
targetstringa tabset, row, border or layout id of to
locationDockLocation"center" (default) goes into the target; an edge splits it
indexnumberfor a center drop: the insertion index; -1 (default) appends