# Collapse and expand groups

Collapse a group into a compact placeholder, then expand it to restore its members and connections.

## When to use this

Use collapse when readers need to focus on the surrounding graph without losing a group's contents. The diagram engine treats the group as a container: collapsing hides its members, moves crossing links to a placeholder, and merges parallel crossings; expanding restores the saved state.

## Set up a group and add collapse controls

1. Render nodes and edges, then add a group around the member nodes. The instance gives you the [`DiagramEngine`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-engine#diagramengine) through `getEngine()`; `addGroup()` creates the group, and `addToGroup()` assigns its members.
2. Call `collapseGroup(groupId, options?)` or `expandGroup(groupId)` from your controls. Both calls return promises, so await them before treating the operation as complete.

The example places three member nodes inside the Service group. On load, all five nodes and four links are visible. Click **collapse group**: the three members disappear, the group becomes a Service placeholder, and links crossing the group boundary attach to the placeholder. The two links from `ext 1` merge into one proxy link labelled `2×`; the internal member link disappears while collapsed. Click **expand group** to restore the three members, their original positions, and all four links.

This page adds collapse and expand controls to the mounted diagram; for mounting the canvas and retrieving its instance in each framework, see [Build runnable workflows](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/build-runnable-workflows).

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

const toolbar = document.createElement('div');
const collapseButton = document.createElement('button');
const expandButton = document.createElement('button');
const host = document.createElement('div');
collapseButton.textContent = 'collapse group';
expandButton.textContent = 'expand group';
host.style.height = '480px';
toolbar.append(collapseButton, expandButton);
document.body.append(toolbar, host);

const nodes = [
  { id: 'ext1', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, label: 'ext 1' },
  { id: 'ext2', position: { x: 60, y: 300 }, size: { width: 120, height: 60 }, label: 'ext 2' },
  { 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' },
];
const edges = [
  { id: 'a', source: 'ext1', target: 'm1' },
  { id: 'b', source: 'ext1', target: 'm2' },
  { id: 'c', source: 'ext2', target: 'm3' },
  { id: 'd', source: 'm1', target: 'm2' },
];

async function mountDiagram() {
  const instance = render({ nodes, edges }, host);
  const engine = instance.getEngine();
  const group = await engine.addGroup({ name: 'Service' });
  group.setFrame({ x: 400, y: 60, width: 180, height: 340 });
  for (const id of ['m1', 'm2', 'm3']) {
    await engine.addToGroup(group.id, id);
  }
  instance.fitView(40);
  instance.renderNow();

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

void mountDiagram();
```
```tsx title="Angular"
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <div style="display:flex;gap:8px;padding:10px 24px">
      <button type="button" (click)="collapse()">collapse group</button>
      <button type="button" (click)="expand()">expand group</button>
    </div>
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display:block;height:480px" />
  `,
})
export class CollapseExpandComponent implements AfterViewInit {
  @ViewChild(DiagramCanvasComponent) private canvas!: DiagramCanvasComponent;
  nodes: NodeSpec[] = [
    { id: 'ext1', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, label: 'ext 1' },
    { id: 'ext2', position: { x: 60, y: 300 }, size: { width: 120, height: 60 }, label: 'ext 2' },
    { 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: EdgeSpec[] = [
    { id: 'a', source: 'ext1', target: 'm1' },
    { id: 'b', source: 'ext1', target: 'm2' },
    { id: 'c', source: 'ext2', target: 'm3' },
    { id: 'd', source: 'm1', target: 'm2' },
  ];
  private groupId: string | undefined;

  async ngAfterViewInit(): Promise<void> {
    const engine = this.canvas.activeEngine();
    if (!engine) return;
    const group = await engine.addGroup({ name: 'Service' });
    group.setFrame({ x: 400, y: 60, width: 180, height: 340 });
    for (const id of ['m1', 'm2', 'm3']) {
      await engine.addToGroup(group.id, id);
    }
    this.groupId = group.id;
  }

  async collapse(): Promise<void> {
    const engine = this.canvas.activeEngine();
    if (engine && this.groupId) {
      await engine.collapseGroup(this.groupId, { proxyLabel: (info) => `${info.count}×` });
    }
  }

  async expand(): Promise<void> {
    const engine = this.canvas.activeEngine();
    if (engine && this.groupId) await engine.expandGroup(this.groupId);
  }
}
```
```tsx title="Qwik"
import { $, component$ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'ext1', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, label: 'ext 1' },
  { id: 'ext2', position: { x: 60, y: 300 }, size: { width: 120, height: 60 }, label: 'ext 2' },
  { 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' },
];
const edges: EdgeSpec[] = [
  { id: 'a', source: 'ext1', target: 'm1' },
  { id: 'b', source: 'ext1', target: 'm2' },
  { id: 'c', source: 'ext2', target: 'm3' },
  { id: 'd', source: 'm1', target: 'm2' },
];

export default component$(() => (
  <div id="qwik-collapse-demo" style={{ height: '520px' }}>
    <GrafloriaFlow
      defaultNodes={nodes}
      defaultEdges={edges}
      style={{ height: '480px' }}
      onInit$={$(async (api: DiagramInstance) => {
        const engine = api.getEngine();
        const group = await engine.addGroup({ name: 'Service' });
        group.setFrame({ x: 400, y: 60, width: 180, height: 340 });
        for (const id of ['m1', 'm2', 'm3']) {
          await engine.addToGroup(group.id, id);
        }

        const toolbar = document.createElement('div');
        const collapseButton = document.createElement('button');
        const expandButton = document.createElement('button');
        toolbar.style.display = 'flex';
        toolbar.style.gap = '8px';
        toolbar.style.padding = '10px 24px';
        collapseButton.type = 'button';
        collapseButton.textContent = 'collapse group';
        expandButton.type = 'button';
        expandButton.textContent = 'expand group';
        toolbar.append(collapseButton, expandButton);
        const root = document.getElementById('qwik-collapse-demo');
        root?.prepend(toolbar);

        collapseButton.addEventListener('click', async () => {
          await engine.collapseGroup(group.id, { proxyLabel: (info) => `${info.count}×` });
          api.renderNow();
        });
        expandButton.addEventListener('click', async () => {
          await engine.expandGroup(group.id);
          api.renderNow();
        });
        api.renderNow();
      })}
    />
  </div>
));
```
```tsx title="React"
import { useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'ext1', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, label: 'ext 1' },
  { id: 'ext2', position: { x: 60, y: 300 }, size: { width: 120, height: 60 }, label: 'ext 2' },
  { 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' },
];
const edges: EdgeSpec[] = [
  { id: 'a', source: 'ext1', target: 'm1' },
  { id: 'b', source: 'ext1', target: 'm2' },
  { id: 'c', source: 'ext2', target: 'm3' },
  { id: 'd', source: 'm1', target: 'm2' },
];

export default function CollapseExpandDemo() {
  const instance = useRef<DiagramInstance | null>(null);
  const groupId = useRef<string | undefined>(undefined);

  const collapse = async () => {
    const api = instance.current;
    const id = groupId.current;
    if (!api || !id) return;
    await api.getEngine().collapseGroup(id, { proxyLabel: (info) => `${info.count}×` });
    api.renderNow();
  };
  const expand = async () => {
    const api = instance.current;
    const id = groupId.current;
    if (!api || !id) return;
    await api.getEngine().expandGroup(id);
    api.renderNow();
  };

  const onInit = async (api: DiagramInstance): Promise<void> => {
    instance.current = api;
    const engine = api.getEngine();
    const group = await engine.addGroup({ name: 'Service' });
    group.setFrame({ x: 400, y: 60, width: 180, height: 340 });
    for (const id of ['m1', 'm2', 'm3']) {
      await engine.addToGroup(group.id, id);
    }
    groupId.current = group.id;
    api.renderNow();
  };

  return (
    <div style={{ height: '520px' }}>
      <div style={{ display: 'flex', gap: 8, padding: '10px 24px' }}>
        <button type="button" onClick={() => void collapse()}>collapse group</button>
        <button type="button" onClick={() => void expand()}>expand group</button>
      </div>
      <GrafloriaFlow
        defaultNodes={nodes}
        defaultEdges={edges}
        onInit={onInit}
        style={{ height: '480px' }}
      />
    </div>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'ext1', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, label: 'ext 1' },
  { id: 'ext2', position: { x: 60, y: 300 }, size: { width: 120, height: 60 }, label: 'ext 2' },
  { 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' },
];
const edges: EdgeSpec[] = [
  { id: 'a', source: 'ext1', target: 'm1' },
  { id: 'b', source: 'ext1', target: 'm2' },
  { id: 'c', source: 'ext2', target: 'm3' },
  { id: 'd', source: 'm1', target: 'm2' },
];

let instance: DiagramInstance | null = null;
let groupId: string | undefined;

async function onInit(api: DiagramInstance): Promise<void> {
  instance = api;
  const engine = api.getEngine();
  const group = await engine.addGroup({ name: 'Service' });
  group.setFrame({ x: 400, y: 60, width: 180, height: 340 });
  for (const id of ['m1', 'm2', 'm3']) {
    await engine.addToGroup(group.id, id);
  }
  groupId = group.id;
  api.renderNow();
}

async function collapse(): Promise<void> {
  if (!instance || !groupId) return;
  await instance.getEngine().collapseGroup(groupId, { proxyLabel: (info) => `${info.count}×` });
  instance.renderNow();
}

async function expand(): Promise<void> {
  if (!instance || !groupId) return;
  await instance.getEngine().expandGroup(groupId);
  instance.renderNow();
}
</script>

<template>
  <div style="height:520px">
    <div style="display:flex;gap:8px;padding:10px 24px">
      <button type="button" @click="collapse">collapse group</button>
      <button type="button" @click="expand">expand group</button>
    </div>
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges"
      @init="onInit" :style="{ height: '480px' }" />
  </div>
</template>
```
:::

Each sample renders the same five nodes and four links. Its setup creates the Service group and assigns `m1`, `m2`, and `m3`; the buttons await the engine operation. The JavaScript, Qwik, React, and Vue samples call `renderNow()` after the change. The Angular sample gets the active engine from its canvas view.

## Options that affect the result

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `groupId` | `string` | Required | Identifies the group to collapse or expand. |
| `options` | `CollapseOptions` | `undefined` | Optional collapse settings. |
| `CollapseOptions.proxyLabel` | `(info: ProxyLabelInfo) => string` | The crossing count as a string when greater than one; no synthetic label for a single crossing | Supplies the label for each aggregated proxy link. Returning an empty string suppresses the label. |

`collapseGroup()` resolves after the engine executes the collapse command; `expandGroup()` resolves after it executes the expand command. Both calls operate on the live diagram through its instance's engine. The collapse snapshot retains member positions and prior group geometry; expanding restores those positions and geometry, restores removed links, and removes the placeholder. Internal links are removed while collapsed. Boundary links are grouped by external node and direction, with one proxy link retained per group.

An empty group has no member nodes to hide, so it collapses without creating a placeholder. For the compact visual representation shown here, add member nodes before collapsing.

## Try the live demo

Open the [Collapse & expand demo](https://grafloria.com/demos/grouping/collapse-expand.html) to see the member nodes hide, boundary links re-home to the placeholder, and the parallel links merge into one labelled proxy. The demo source is [collapse-expand.html](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/grouping/collapse-expand.html).

## Related

- [Group nodes and containers](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/group-nodes-and-containers) — create groups and manage membership.
- [Command history](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/command-history) — understand the engine commands behind edits.
