# Group nodes and containers

Create a visible container and add nodes to its membership.

Use a group when a set of nodes needs a real container relationship, not only a decorative rectangle: the engine tracks membership. This page shows the task in JavaScript, Angular, Qwik, Vue, and React.

Unlike the mounted workflow graph in [Build runnable workflows](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/build-runnable-workflows), this task adds explicit group membership: a node travels with a frame because its ID is a member, not because it overlaps the frame. See that page for framework mounting and instance setup.

## Create a frame and add members

In Angular, Vue, and React, mount ordinary node data, then use the mounted instance's engine to create a group, set its frame, and add node IDs to it. The renderer shows the frame behind the nodes. Membership is explicit: a node does not become a member merely because it overlaps the frame.

For groups declared with the diagram data, use [`GroupSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance#groupspec). The JavaScript and Qwik examples declare the group in their initial specifications; Angular, Vue, and React add it through the mounted engine.

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

const host = document.createElement('div');
host.style.height = '400px';
document.body.append(host);

const nodes = [
  { id: 'ingest', position: { x: 320, y: 160 }, size: { width: 120, height: 60 }, label: 'ingest' },
  { id: 'transform', position: { x: 520, y: 160 }, size: { width: 120, height: 60 }, label: 'transform' },
  { id: 'retry', position: { x: 420, y: 280 }, size: { width: 120, height: 60 }, label: 'retry' },
];
const edges = [
  { id: 'e1', source: 'ingest', target: 'transform' },
  { id: 'e2', source: 'ingest', target: 'retry' },
];
const groups = [
  {
    id: 'pipeline',
    label: 'Pipeline',
    children: ['ingest', 'transform', 'retry'],
    bounds: { x: 290, y: 120, width: 400, height: 240 },
  },
];

render({ nodes, edges, groups }, host);
```
```ts 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: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display:block; height:400px" />
  `,
})
export class PipelineComponent implements AfterViewInit {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  nodes: NodeSpec[] = [
    { id: 'ingest', position: { x: 320, y: 160 }, size: { width: 120, height: 60 }, label: 'ingest' },
    { id: 'transform', position: { x: 520, y: 160 }, size: { width: 120, height: 60 }, label: 'transform' },
    { id: 'retry', position: { x: 420, y: 280 }, size: { width: 120, height: 60 }, label: 'retry' },
  ];
  edges: EdgeSpec[] = [
    { id: 'e1', source: 'ingest', target: 'transform' },
    { id: 'e2', source: 'ingest', target: 'retry' },
  ];

  async ngAfterViewInit(): Promise<void> {
    const engine = this.canvas().activeEngine();
    if (!engine) return;
    const pipeline = await engine.addGroup({ name: 'Pipeline' });
    pipeline.setFrame({ x: 290, y: 120, width: 400, height: 240 });
    await engine.addToGroup(pipeline.id, 'ingest');
    await engine.addToGroup(pipeline.id, 'transform');
    await engine.addToGroup(pipeline.id, 'retry');
  }
}
```
```tsx title="Qwik"
import { component$ } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { EdgeSpec, GroupSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'ingest', position: { x: 320, y: 160 }, size: { width: 120, height: 60 }, label: 'ingest' },
  { id: 'transform', position: { x: 520, y: 160 }, size: { width: 120, height: 60 }, label: 'transform' },
  { id: 'retry', position: { x: 420, y: 280 }, size: { width: 120, height: 60 }, label: 'retry' },
];
const edges: EdgeSpec[] = [
  { id: 'e1', source: 'ingest', target: 'transform' },
  { id: 'e2', source: 'ingest', target: 'retry' },
];
const groups: GroupSpec[] = [
  {
    id: 'pipeline',
    label: 'Pipeline',
    children: ['ingest', 'transform', 'retry'],
    bounds: { x: 290, y: 120, width: 400, height: 240 },
  },
];

export default component$(() => (
  <div style={{ height: '400px' }}>
    <GrafloriaFlow nodes={nodes} edges={edges} groups={groups} />
  </div>
));
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance } from '@grafloria/vue';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'ingest', position: { x: 320, y: 160 }, size: { width: 120, height: 60 }, label: 'ingest' },
  { id: 'transform', position: { x: 520, y: 160 }, size: { width: 120, height: 60 }, label: 'transform' },
  { id: 'retry', position: { x: 420, y: 280 }, size: { width: 120, height: 60 }, label: 'retry' },
];
const edges: EdgeSpec[] = [
  { id: 'e1', source: 'ingest', target: 'transform' },
  { id: 'e2', source: 'ingest', target: 'retry' },
];

async function onInit(instance: DiagramInstance): Promise<void> {
  const engine = instance.getEngine();
  const pipeline = await engine.addGroup({ name: 'Pipeline' });
  pipeline.setFrame({ x: 290, y: 120, width: 400, height: 240 });
  await engine.addToGroup(pipeline.id, 'ingest');
  await engine.addToGroup(pipeline.id, 'transform');
  await engine.addToGroup(pipeline.id, 'retry');
  instance.renderNow();
}
</script>

<template>
  <div style="height:400px">
    <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';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'ingest', position: { x: 320, y: 160 }, size: { width: 120, height: 60 }, label: 'ingest' },
  { id: 'transform', position: { x: 520, y: 160 }, size: { width: 120, height: 60 }, label: 'transform' },
  { id: 'retry', position: { x: 420, y: 280 }, size: { width: 120, height: 60 }, label: 'retry' },
];
const edges: EdgeSpec[] = [
  { id: 'e1', source: 'ingest', target: 'transform' },
  { id: 'e2', source: 'ingest', target: 'retry' },
];

export function Pipeline() {
  const onInit = async (instance: DiagramInstance): Promise<void> => {
    const engine = instance.getEngine();
    const pipeline = await engine.addGroup({ name: 'Pipeline' });
    pipeline.setFrame({ x: 290, y: 120, width: 400, height: 240 });
    await engine.addToGroup(pipeline.id, 'ingest');
    await engine.addToGroup(pipeline.id, 'transform');
    await engine.addToGroup(pipeline.id, 'retry');
    instance.renderNow();
  };

  return (
    <div style={{ height: 400 }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit={onInit} />
    </div>
  );
}
```
:::

The examples render three ordinary nodes inside a labelled **Pipeline** frame, with two edges still connecting them. The frame's coordinates are in diagram world space.

For a nested container, create another group, add its node members, and add that group's ID to the outer group. Call `fitToContents()` from the inner group outward when the frames need to wrap their contents. Membership remains a model relationship rather than a rule inferred from current node positions.

## Group definition options

For groups declared in diagram data instead of added after mount, use [`GroupSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance#groupspec). A group spec defines the frame and its initial member node IDs.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `id` | `string` | — | Identifies the group. |
| `label` | `string` | — | Supplies the frame's visible label. |
| `children` | `string[]` | — | Names the node IDs that become members. |
| `bounds` | `{ x: number; y: number; width: number; height: number }` | — | Pins the frame to a world-space rectangle; without it, the frame fits around its children using `padding`. |
| `padding` | `number` | `20` | Sets the space between the children and a fitted frame. |
| `labelPlacement` | `GroupLabelPlacement` | `'top-left'` | Places the frame label. |
| `style` | `GroupFrameStyle` | — | Sets the frame's style. |

## What to watch for

- Set a real height on the canvas wrapper. The canvas fills its parent; an unresolved height leaves no drawing area.
- Use `addToGroup(groupId, entityId)` in that order. It returns a promise, so await it before treating membership as complete.
- A node positioned inside a frame is not necessarily a member. Add it to the group explicitly.
- Groups and membership are edits made through engine operations; use the engine's history APIs for undo and redo rather than looking for those methods on the instance.

## See it running

- [Group frames](https://grafloria.com/demos/grouping/group-frames.html) — visible labelled frames, including a nested frame.
- [Sub-flow](https://grafloria.com/demos/grouping/sub-flow.html) — nested group membership and fit-to-contents behavior.

For collapsing and expanding a group, see [Collapse and expand groups](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/collapse-and-expand-groups). For the model behind groups and membership, see [The graph model and document](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/graph-model-and-document).
