Skip to content
D
Documentation

Groups and containment

concept
3 min readUpdated

A group is a semantic container: it owns membership, moves its contents together, can contain other groups, and can collapse to a reversible snapshot.

How containment works

The rendered DiagramInstance is the facade used by a binding. Its setGroups() method reconciles group specifications with the model. For structural work, use the DiagramModel and DiagramEngine: the diagram stores nodes and groups, while the engine performs group operations.

mermaid
flowchart TD
  E["DiagramEngine"] --> D["DiagramModel"]
  D --> G["GroupModel"]
  G --> N1["NodeModel: intake"]
  G --> C["GroupModel: review"]
  C --> N2["NodeModel: approve"]
  G -. collapse snapshot .-> P["proxy node and proxy links"]

The GroupModel keeps member IDs in members. When a group is itself a member, the child group's parentGroupId points back to its parent. Adding a group as a member therefore creates containment, not a visual overlap. The model rejects self-membership and ancestor cycles.

Add members and nest groups

Create nodes and groups in the diagram, then add members through the engine or group model. The engine's addToGroup(groupId, entityId) method takes the group first. The group model's addMember(entityId, diagram) is useful when you already hold the group.

ts
import { DiagramEngine, GroupModel, NodeModel } from '@grafloria/engine';

const engine = new DiagramEngine();
const diagram = engine.createDiagram('order-flow');

const intake = new NodeModel({
  id: 'intake',
  type: 'task',
  position: { x: 40, y: 80 },
  size: { width: 120, height: 48, depth: 0 },
});
const approve = new NodeModel({
  id: 'approve',
  type: 'task',
  position: { x: 220, y: 80 },
  size: { width: 120, height: 48, depth: 0 },
});

diagram.addNode(intake);
diagram.addNode(approve);

const pipeline = new GroupModel({ id: 'pipeline', name: 'Pipeline' });
const review = new GroupModel({ id: 'review', name: 'Review' });
diagram.addGroup(pipeline);
diagram.addGroup(review);

pipeline.addMember('intake', diagram);
review.addMember('approve', diagram);
pipeline.addMember('review', diagram);

const ancestors = diagram.getAncestors('review');
const descendants = diagram.getDescendants('pipeline');
console.log(ancestors.map((group) => group.name));
console.log(descendants.map((group) => group.name));

After these calls, intake belongs directly to pipeline, approve belongs to review, and review is nested inside pipeline. getAncestors() returns the parent chain nearest first; getDescendants() returns nested groups below the requested group. Removing a member returns true when membership existed and clears a nested group's parent pointer when appropriate.

Interactive bindings use the same membership model: dropping a node into a group adds it, and dragging it out removes it. To make frames decorative instead, configure the engine interaction settings with enableGroupDrag: false and enableGroupMembershipOnDrop: false.

Collective movement and layout

Membership gives a group collective behavior. Moving a group moves its members as a unit, and links connected to those members follow their new positions. A nested group contributes its outer frame when its parent computes content bounds, so a parent can fit around a complete child container rather than only around the child's raw nodes.

Groups can also own a flexbox or grid layout. Set the layout on the group through setLayout('flexbox', config) or setLayout('grid', config), then call applyLayout() when you need to apply it immediately. Membership changes and frame changes request layout for configured containers.

Collapse is a reversible snapshot

Collapse a group through the engine when you want the rendered diagram to treat the group as one endpoint:

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

async function run(): Promise<void> {
  const host = document.createElement('div');
  host.style.height = '320px';
  document.body.append(host);
  const api = render({
    nodes: [{
    id: 'first',
    type: 'task',
    position: { x: 40, y: 80 },
    size: { width: 120, height: 48 },
    }, {
    id: 'second',
    type: 'task',
    position: { x: 220, y: 80 },
    size: { width: 120, height: 48 },
    }],
    groups: [{ id: 'review', label: 'Review', children: ['first', 'second'] }],
  }, host);
  await api.getEngine().collapseGroup('review');
}

void run();

While collapsed, the members are hidden and the group is represented by a proxy node. Boundary links re-anchor to that proxy; parallel boundary links can be aggregated. The group's collapsedState records the proxy, the member positions, hidden-node visibility, removed links, and proxy-link information. The snapshot is serialized with the group, so saving and loading a collapsed diagram preserves the information needed to expand it and restore the prior geometry and links.

Use the engine methods for a complete collapse operation. Calling GroupModel.collapse() or expand() changes the group's collapsed flag and emits the corresponding group event, but the engine operation coordinates the diagram's hidden nodes and links.

Fit a group to its contents

Call fitToContents() after positioning members when the frame should wrap them. The computed frame includes group padding and the header band. With nested groups, pass deepRecursive: true so descendants fit first and the parent then fits around their resulting outer frames.

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

const host = document.createElement('div');
host.style.height = '320px';
document.body.append(host);
const api = render({
  nodes: [{
    id: 'step',
    type: 'task',
    position: { x: 40, y: 80 },
    size: { width: 120, height: 48 },
  }],
  groups: [{ id: 'pipeline', label: 'Pipeline', children: ['step'] }],
}, host);

const renderedDiagram = api.getModel();
renderedDiagram.getGroup('pipeline')?.fitToContents(renderedDiagram, { deepRecursive: true, mode: 'grow-only' });

The mode option controls how the new content rectangle reconciles with the current frame:

modeEffect
exactUse the fitted content rectangle.
grow-onlyExpand to contain content without shrinking the current frame.
shrink-onlyDo not grow beyond the current frame.

The group writes the resulting rectangle to its authoritative position and size and to its hit-test bounds. With no positioned members, fitting is a no-op. The default padding for a code-authored group resolves to 16 on each side, and the default header band is 24 pixels; set padding or headerHeight when the frame needs different spacing.

For dashboards or other layouts that need containment without visible group chrome, use the model metadata convention frameChrome: 'none'. The group still participates in membership, movement, layout, fitting, and serialization; only its frame presentation changes.

What to remember

  • A member ID creates a relationship in the document; it is not merely a background rectangle.
  • Nesting is represented by both the parent's member set and the child's parent pointer.
  • Collapse stores enough geometry and link information to expand after a round trip.
  • Fit descendants before parents when nested content determines the outer frame.

For undoable user edits, use the engine's command-facing operations rather than mutating the model as a substitute for an edit command. Continue with commands and shared history for that distinction, or build a dashboard for frameless container layouts.

Was this page helpful?

Groups and containment — Grafloria · GPT-5.6 Luna