Skip to content
D
Documentation

Group nested diagrams

how-to
3 min readUpdated

Use a group as a real container: add nodes to it, add another group as a member, fit the frames around their contents, and collapse or expand the group without losing its links.

When to use groups

Use groups when membership has meaning. A node remains a node after it joins a group, and its links remain part of the diagram. A nested group is a member of its parent, so fitting the parent includes the nested group's outer frame rather than only the nested node positions.

The examples below use a DiagramInstance as the live handle to the mounted diagram. From it, get the DiagramEngine and call the group operations there; use the GroupModel for fitToContents() and nesting.

Create and nest containers

Start with four nodes and two links. render mounts them, after which you add three nodes to Pipeline, add retry to Retry handler, then add the inner group to the outer group. Fit the inner group first and the outer group second. The rendered result shows two frames around the stages, with Retry handler inside Pipeline; outside remains beyond both frames.

js
import { render } from '@grafloria/element';

async function main() {
const host = document.getElementById('app');
if (!host) throw new Error('Missing #app');
host.setAttribute('style', 'display:block;width:800px;height:400px;');

const api = render({
  nodes: [
    { id: 'n1', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 1' },
    { id: 'n2', position: { x: 560, y: 150 }, size: { width: 120, height: 60 }, label: 'stage 2' },
    { id: 'n3', position: { x: 480, y: 275 }, size: { width: 120, height: 60 }, label: 'retry' },
    { id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' },
  ],
  edges: [
    { id: 'e1', source: 'n1', target: 'n2' },
    { id: 'e2', source: 'n1', target: 'n3' },
  ],
}, host);

const engine = api.getEngine();
const diagram = api.getModel();
const pipeline = await engine.addGroup({ name: 'Pipeline' });
await engine.addToGroup(pipeline.id, 'n1');
await engine.addToGroup(pipeline.id, 'n2');
await engine.addToGroup(pipeline.id, 'n3');

const retryHandler = await engine.addGroup({ name: 'Retry handler' });
await engine.addToGroup(retryHandler.id, 'n3');
pipeline.addMember(retryHandler.id, diagram);

retryHandler.fitToContents(diagram);
pipeline.fitToContents(diagram);
api.renderNow();
}

void main();

The group membership is explicit: moving a node visually does not detach it. Use removeFromGroup(groupId, entityId) when the relationship itself must change. fitToContents() uses member bounds plus the group's padding and header; fit descendants before their parent when nesting.

The Pipeline frame encloses its stages and the smaller Retry handler frame, while outside remains separate.

See the sub-flow live demo.

Collapse and expand without breaking relationships

For a group with external links, call collapseGroup(). Members become hidden and boundary links are represented by a collapsed proxy; parallel boundary links can be represented by one aggregated proxy. expandGroup() restores the members, geometry, and original links.

Add controls to the mounted instance. The buttons show the collapsed placeholder and then restore the complete graph.

js
import { render } from '@grafloria/element';

async function main() {
const host = document.getElementById('app');
if (!host) throw new Error('Missing #app');
host.style.height = '400px';
const api = render({
  nodes: [
    { id: 'ext', position: { x: 60, y: 120 }, size: { width: 120, height: 60 }, label: 'external' },
    { id: 'm1', position: { x: 420, y: 80 }, size: { width: 120, height: 60 }, label: 'member 1' },
    { id: 'm2', position: { x: 420, y: 200 }, size: { width: 120, height: 60 }, label: 'member 2' },
    { id: 'm3', position: { x: 420, y: 320 }, size: { width: 120, height: 60 }, label: 'member 3' },
  ],
  edges: [
    { id: 'a', source: 'ext', target: 'm1' },
    { id: 'b', source: 'ext', target: 'm2' },
  ],
}, host);

const collapseButton = document.createElement('button');
collapseButton.textContent = 'collapse group';
const expandButton = document.createElement('button');
expandButton.textContent = 'expand group';
document.body.prepend(collapseButton, expandButton);

const service = await api.getEngine().addGroup({ name: 'Service' });
service.setFrame({ x: 400, y: 60, width: 180, height: 340 });
for (const id of ['m1', 'm2', 'm3']) await api.getEngine().addToGroup(service.id, id);

collapseButton.addEventListener('click', () => void api.getEngine().collapseGroup(service.id, {
  proxyLabel: (item) => `${item.count}×`,
}));
expandButton.addEventListener('click', () => void api.getEngine().expandGroup(service.id));
api.renderNow();
}

void main();

The same instance calls work in Angular, Qwik, Vue, and React: keep the instance from each binding's ready or init callback, then call getEngine(). The collapse operation is asynchronous, so await it before reading the model or triggering a synchronous repaint.

The collapsed Service placeholder replaces hidden members and boundary links, and the expanded state restores the four original links.

See the collapse and expand live demo.

Make drag-and-drop change membership

Interactive membership is separate from explicit addToGroup(). Enable it on the engine when dropping a node into a frame must join that group:

ts
import { render } from '@grafloria/element';

const host = document.getElementById('app');
if (!host) throw new Error('Missing #app');
host.style.height = '400px';
const api = render({
  nodes: [{ id: 'invoice', position: { x: 100, y: 100 }, label: 'invoice' }],
  edges: [],
}, host);
const engine = api.getEngine();
engine.setInteractionConfig({
  enableGroupMembershipOnDrop: true,
  enableGroupDrag: true,
});

With these settings, dropping invoice into Billing makes it a member; dragging the frame moves the member with it; dropping the node on empty canvas removes the membership. Use a membership predicate when a group must reject some candidates.

See the drop-to-contain live demo.

Options that matter

Option or callTypeDefaultWhat it does
GroupModel.paddingGroupPadding—Adds space inside the frame when it fits around members.
fitToContents() modeGroupFitModeexactReconciles the fitted rectangle by snapping to content, growing only, or shrinking only.
fitToContents() deepRecursiveboolean—Fits descendant groups deepest-first before fitting the current group.
collapseGroup() proxyLabelcallback receiving { count: number }—Supplies the label used by the collapse example for an aggregated proxy.
enableGroupMembershipOnDropbooleantrueControls whether a drop changes group membership.
enableGroupDragbooleantrueControls whether dragging a group moves its contents with it.

Pitfalls

  • Fit the child before the parent. Otherwise the parent can calculate bounds from a child whose frame has not yet been fitted.
  • Do not use frame overlap as your membership test. Membership is stored in the group, and a node can remain a member after it is dragged within the frame.
  • Do not call collapseGroup() on a group with an unmounted or missing diagram; the engine operation requires a current diagram.
  • Keep the container's CSS height non-zero. A mounted diagram in a heightless container has no drawable area.

Was this page helpful?