# Group and collapse subflows

Use a group when a frame has meaning, not only appearance. The group owns its members, so a drag of the frame moves the members with it; groups can contain other groups; collapsing hides the members and expanding restores their saved geometry.

## When to use it

Use this pattern for a pipeline, stage, swimlane, or any subflow that readers need to inspect as a unit. Membership is explicit rather than inferred from overlap: moving a member does not redraw the hierarchy. To change membership in code, call `addToGroup()` or `removeFromGroup()` on the engine.

Use [`render`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-element-core#render) to mount the data and return a [`DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance#diagraminstance); use `instance.getModel()` for the document and `instance.getEngine()` for group behavior.

## Build the hierarchy

Create the outer group, add nodes to it, then create the inner group and add the inner group to the outer group. Fit the inner frame first and the outer frame second. The result is a `Pipeline` frame around the three stages, with a `Retry handler` frame around `retry` inside it. `outside` remains outside.

:::code-group
```js title="JavaScript"
import { render } from '@grafloria/element';

const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '100vh';

const instance = 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);

void (async () => {
  const engine = instance.getEngine();
  const diagram = instance.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);
  instance.renderNow();

  // The group frame is draggable, and its members travel with it.
  await engine.collapseGroup(pipeline.id);
  instance.renderNow();
  await engine.expandGroup(pipeline.id);
  instance.renderNow();
})();
```

```ts title="Angular"
import { AfterViewInit, Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: '<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block;height:100vh" />',
})
export class SubflowComponent implements AfterViewInit {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  readonly 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' },
  ];
  readonly edges = [
    { id: 'e1', source: 'n1', target: 'n2' },
    { id: 'e2', source: 'n1', target: 'n3' },
  ];

  async ngAfterViewInit(): Promise<void> {
    const engine = this.canvas().activeEngine();
    if (!engine) return;
    const diagram = engine.getDiagram();
    if (!diagram) return;
    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);
  }
}
```

```tsx title="Qwik"
import { component$ } from '@builder.io/qwik';

const 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' },
];

export default component$(() => (
  <div style={{ height: '100vh' }}>
    <grafloria-flow data-nodes={JSON.stringify(nodes)} data-groups="Pipeline: n1,n2,n3; Retry handler: n3" />
  </div>
));
```

```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance } from '@grafloria/vue';

const 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' },
];
const edges = [
  { id: 'e1', source: 'n1', target: 'n2' },
  { id: 'e2', source: 'n1', target: 'n3' },
];

async function onInit(instance: DiagramInstance): Promise<void> {
  const engine = instance.getEngine();
  const diagram = engine.getDiagram();
  if (!diagram) return;
  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);
}
</script>

<template>
  <div style="height:100vh"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" /></div>
</template>
```

```tsx title="React"
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/react';

const 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' },
];
const edges = [
  { id: 'e1', source: 'n1', target: 'n2' },
  { id: 'e2', source: 'n1', target: 'n3' },
];

export default function Subflow() {
  const onInit = (instance: DiagramInstance): void => {
    const engine = instance.getEngine();
    const diagram = engine.getDiagram();
    if (!diagram) return;
    void (async () => {
      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);
      instance.renderNow();
    })();
  };
  return <div style={{ height: '100vh' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit={onInit} /></div>;
}
```
:::

## Collapse and restore the subflow

Call `collapseGroup(groupId, options?)` on the engine. The members disappear, a collapsed placeholder represents the group, and links crossing the boundary point to that placeholder; parallel boundary links can appear as one aggregated proxy link. Call `expandGroup(groupId)` to restore the members, links, and the geometry captured before collapse.

The `proxyLabel` option controls the text supplied for an aggregated proxy link:

| Option | Type | Default | What it does |
|---|---|---|---|
| `proxyLabel` | `(info: { count: number }) => string` | not specified | Returns the label for an aggregated proxy link. |

For a visible control, put the two engine calls behind buttons rather than collapsing immediately after setup. This complete browser example renders the group and wires both buttons:

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

const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '100vh';
const collapseButton = document.createElement('button');
collapseButton.textContent = 'Collapse group';
const expandButton = document.createElement('button');
expandButton.textContent = 'Expand group';
document.body.prepend(expandButton);
document.body.prepend(collapseButton);

const instance = render({
  nodes: [
    { id: 'outside', position: { x: 80, y: 150 }, size: { width: 120, height: 60 }, label: 'outside' },
    { id: 'member', position: { x: 400, y: 150 }, size: { width: 120, height: 60 }, label: 'member' },
  ],
  edges: [{ id: 'link', source: 'outside', target: 'member' }],
}, host);
void (async () => {
  const engine = instance.getEngine();
  const pipeline = await engine.addGroup({ name: 'Pipeline' });
  pipeline.setFrame({ x: 360, y: 100, width: 220, height: 160 });
  await engine.addToGroup(pipeline.id, 'member');
  instance.renderNow();

  collapseButton.addEventListener('click', async () => {
    await engine.collapseGroup(pipeline.id, { proxyLabel: (info) => `${info.count}×` });
    instance.renderNow();
  });

  expandButton.addEventListener('click', async () => {
    await engine.expandGroup(pipeline.id);
    instance.renderNow();
  });
})();
```

After `collapse()`, the canvas shows the collapsed group placeholder instead of its stages. After `expand()`, the stages and their pre-collapse positions return. Drag the group frame between those calls to move the subflow as a unit.

## Pitfalls

- A group is not a decorative rectangle: adding a member records containment, so moving the group moves its members.
- Fit nested groups from the inside out. `fitToContents()` uses the current member geometry, so fit the child before the parent.
- Keep the diagram host at a real height; a canvas whose host has no resolved height renders blank. See [Edit Mermaid diagrams](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/edit-mermaid-diagrams).
- Collapse after the instance is initialized and repaint with `renderNow()` when the next line needs the new pixels. See [The DiagramInstance](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/instance-and-lifecycle).

## Live demo

Try the [sub-flow demo](https://grafloria.com/demos/grouping/sub-flow.html), then the [collapse and expand demo](https://grafloria.com/demos/grouping/collapse-expand.html). For compound layout across nested containers, see the [nested containers demo](https://grafloria.com/demos/layout/nested-containers.html).

## Related

- [Groups and containment](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/groups-and-containment)
- [Apply auto-layout](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/apply-auto-layout)
- [Commands and shared history](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/commands-and-shared-history)
