Skip to content
D
Documentation

Add collaboration

how-to
3 min readUpdated

Connect two mounted diagrams to the same room and watch edits, presence, comments, reconnects, and local undo converge.

When to use this

Use the collab prop on GrafloriaFlow when each mounted diagram has a transport and an actor id. The canvas joins when it mounts and leaves when it unmounts. Local edits become CRDT operations; incoming operations merge per property, so a move and a rename on the same node can both survive.

BroadcastChannelTransport is the shipped no-server choice for two tabs in one browser. Use WebSocketTransport when your application supplies a server-backed channel. Grafloria supplies convergence, presence, comments, and the operation log; your application supplies rooms, authentication, and storage.

Connect two diagrams

Use the same channel name for both transports and a different stable actor for each peer. Give each canvas a real height so both mounted diagrams render.

js
import { render } from '@grafloria/element';
import { BroadcastChannelTransport, createSyncSession } from '@grafloria/engine';

document.body.innerHTML = '<div id="left" style="height:400px"></div><div id="right" style="height:400px"></div>';

const nodes = [
  { id: 'ingest', label: 'Ingest', position: { x: 60, y: 60 }, size: { width: 150, height: 66 } },
  { id: 'publish', label: 'Publish', position: { x: 320, y: 60 }, size: { width: 150, height: 66 } },
];
const edges = [{ id: 'e1', source: 'ingest', target: 'publish' }];
const room = 'diagram-' + Math.random().toString(36).slice(2);

const left = render({ nodes, edges }, '#left');
const right = render({ nodes: nodes.map((node) => ({ ...node, position: { ...node.position }, size: { ...node.size } })), edges: edges.map((edge) => ({ ...edge })) }, '#right');
createSyncSession(left.getModel(), new BroadcastChannelTransport({ name: room, actor: 'ana' }), { actor: 'ana' }).join();
createSyncSession(right.getModel(), new BroadcastChannelTransport({ name: room, actor: 'ben' }), { actor: 'ben' }).join();

left.getModel().getNode('ingest')?.setPosition(100, 180);
right.fitView();

Each pair renders the same connected diagram. Drag a node in either pane: the other pane follows and displays the other actor's presence. Camera changes remain local to each pane; document edits synchronize.

See the live two-tabs demo and its source.

Add comments

Set comments to true to create the canvas's comment store. Read that store from the mounted instance and give it to the ready-made panel. This sample mounts the panel with an empty store, so it displays “Comments — No comments yet.”

For React, Vue, and Qwik, use the binding's GrafloriaCommentPanel; for Angular, use GrafloriaCommentPanelComponent as <grafloria-comment-panel>.

tsx
import { useState } from 'react';
import { GrafloriaFlow, GrafloriaCommentPanel } from '@grafloria/react';
import type { CommentStore } from '@grafloria/engine';

export function Comments() {
  const [store, setStore] = useState<CommentStore | null>(null);
  return <div style={{ display: 'flex', height: 400 }}>
    <GrafloriaFlow defaultNodes={[{ id: 'review', label: 'Review', position: { x: 80, y: 100 } }]} comments onInit={(instance) => setStore(instance.getCommentStore())} style={{ flex: 1 }} />
    {store && <GrafloriaCommentPanel store={store} />}
  </div>;
}

The Angular shape is:

html
<grafloria-diagram-canvas #canvas [(nodes)]="nodes" [comments]="true" style="display:block;height:400px" />
@if (canvas.getCommentStore(); as store) {
  <grafloria-comment-panel [store]="store" (threadSelect)="selectedThread = $event" />
}
ts
import { Component } from '@angular/core';
import { DiagramCanvasComponent, GrafloriaCommentPanelComponent } from '@grafloria/angular';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent, GrafloriaCommentPanelComponent],
  template: `<div style="display:flex;height:400px"><grafloria-diagram-canvas #canvas [(nodes)]="nodes" [comments]="true" style="display:block;flex:1"></grafloria-diagram-canvas>@if (canvas.getCommentStore(); as store) {<grafloria-comment-panel [store]="store" (threadSelect)="selectedThread = $event"></grafloria-comment-panel>}</div>`,
  styles: [':host { display: block; height: 400px; }'],
})
export class CommentsComponent {
  nodes = [{ id: 'review', label: 'Review', position: { x: 80, y: 100 } }];
  selectedThread: string | null = null;
}

Use the equivalent GrafloriaFlow and GrafloriaCommentPanel bindings in Vue and Qwik. In Qwik, mark the live store and transport with noSerialize(); they hold subscribers and channel state, not JSON data.

Reconnect without losing edits

Use a transport that reports connection status. When it reconnects, the session runs anti-entropy and exchanges the operations each peer missed. A transport that never reports status cannot be caught up automatically.

With MemoryHub, you can test this without a server: disconnect both MemoryTransport ports, edit both mounted models, then reconnect and run the session's sync round. Both diagrams then contain both offline edits. See the offline and reconnect demo.

Undo your own edit

Collaboration keeps local history separate from remote history. At the engine level, Replica captures local edits and applies remote operations without adding them to your undo stack. undo() therefore undoes your last edit, not the most recent edit made by another peer; redo() reapplies your last undone edit. Use transact() when several model mutations must become one undo step.

For a mounted diagram, obtain its model through DiagramInstance and keep the rendered instance as the shared handle. The rendered peers converge again when the undo operation crosses the transport.

Choose the layer

  • Use GrafloriaFlow when the framework owns the mounted canvas; use its Vue, Qwik, or Angular binding in those frameworks.
  • Use DiagramCanvasComponent in Angular.
  • Use render when your application mounts diagrams directly.
  • Use MemoryHub for deterministic in-page or test peers.

Pitfalls

  • Give every peer a different actor id. The actor id participates in deterministic ordering.
  • Create a transport and room once per mounted session. In React and Qwik, do not create them during every render; Qwik transports are live objects, so wrap them with noSerialize().
  • Collaboration does not synchronize viewport changes.
  • A canvas with no resolved height appears blank; see Style a diagram.

Live examples

Related: Commands, events, and undo, Model and document, and Save and restore diagrams.

Was this page helpful?

Add collaboration — Grafloria