# Groups and containment

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`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) is the facade used by a binding. Its `setGroups()` method reconciles group specifications with the model. For structural work, use the [`DiagramModel`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-engine-models-diagrammodel#diagrammodel) and [`DiagramEngine`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-engine-engine#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`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-engine-models-groupmodel#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:

| mode | Effect |
| --- | --- |
| `exact` | Use the fitted content rectangle. |
| `grow-only` | Expand to contain content without shrinking the current frame. |
| `shrink-only` | Do 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](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/commands-and-shared-history) for that distinction, or [build a dashboard](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/build-a-dashboard) for frameless container layouts.
