Skip to content
D
Documentation

Events and interaction

concept
2 min readUpdated

Grafloria uses one event map beneath its framework bindings: the mounted DiagramInstance reports model, selection, pointer, and viewport changes, while each framework exposes those changes in its own idiom.

One event map

The instance is the facade over the diagram model and engine. Subscribe to it when application code needs the same behavior in JavaScript, React, Vue, Angular, or Qwik.

ts
import type {
  DiagramInstance,
  Unsubscribe,
} from '@grafloria/renderer';

export function connectEventHandlers(instance: DiagramInstance): Unsubscribe[] {
  const stopNodes = instance.on('nodes:change', ({ nodes }) => {
    console.log('nodes changed', nodes);
  });
  const stopEdges = instance.on('edges:change', ({ edges }) => {
    console.log('edges changed', edges);
  });
  const stopSelection = instance.on('selection:change', ({ nodes, edges }) => {
    console.log('selection changed', nodes, edges);
  });
  const stopConnect = instance.on('connect', ({ link }) => {
    console.log('connection completed', link);
  });
  const stopReconnect = instance.on('reconnect', ({ link, endpoint }) => {
    console.log('connection endpoint moved', link, endpoint);
  });
  const stopNodeClick = instance.on('node:click', ({ node, world }) => {
    console.log('node clicked', node, world);
  });
  const stopDoubleClick = instance.on('node:doubleclick', ({ node, world }) => {
    console.log('node double-clicked', node, world);
  });
  const stopEdgeClick = instance.on('edge:click', ({ edge, world }) => {
    console.log('edge clicked', edge, world);
  });
  const stopViewport = instance.on('viewport:change', ({ viewport, zoom }) => {
    console.log('viewport changed', viewport, zoom);
  });

  return [
    stopNodes,
    stopEdges,
    stopSelection,
    stopConnect,
    stopReconnect,
    stopNodeClick,
    stopDoubleClick,
    stopEdgeClick,
    stopViewport,
  ];
}

Each on() call returns an unsubscribe function. Keep those functions with the mounted component and call them when that component unmounts; off() is the alternative when you retain the original handler. The world value in pointer events uses diagram coordinates, not screen pixels.

The event names are represented by DiagramEventName, and the payload handler shape by DiagramEventHandler. The returned cleanup function is Unsubscribe.

mermaid
flowchart LR
  A[User action] --> B[DiagramInstance event map]
  B --> C[Framework callback or output]
  B --> D[Application state or persistence]
  A --> E[DiagramEngine connection lifecycle]
  E --> F[Guidance UI]

Framework surfaces

The event meaning stays the same; only the binding surface changes.

SurfaceUse for user-facing eventsInstance access
Plain JavaScriptapi.on(...), or bubbling grafloria-* DOM eventsThe object returned by the JavaScript mount
ReactonConnect, onNodeClick, onSelectionChange, and onNodesChange callback propsThe instance supplied by the binding's initialization callback
Vue@connect, @node-click, @selection-change, and v-model:nodesThe instance supplied by @init
Angular[(nodes)], (viewportChanged), and (layoutDone); use the engine bus for clicksThe Angular canvas instance
QwikThe binding's onInit$ callback receives the same instanceThe DiagramInstance argument to onInit$

The Vue binding receives the instance through @init, while the Qwik binding receives it through the serializable onInit$ callback. Use the component's callback or output first when the binding provides one. Reach through the instance for events that the binding does not expose, such as reconnect and node:doubleclick.

Connection lifecycle

The instance's connect event reports a completed wire. For guidance while the user is dragging, use the DiagramEngine event bus:

ts
import type { DiagramInstance } from '@grafloria/renderer';

export function watchConnectionDrag(instance: DiagramInstance): () => void {
  const bus = instance.getEngine().eventBus;
  const names = [
    'connection:start',
    'connection:update',
    'connection:port-enter',
    'connection:port-leave',
    'connection:complete',
    'connection:cancel',
  ] as const;
  const unsubscribers = names.map((name) => {
    const handler = (payload: object): void => {
      console.log(name, payload);
    };
    return bus.on(name, handler);
  });

  return (): void => {
    for (const unsubscribe of unsubscribers) {
      unsubscribe();
    }
  };
}

The lifecycle starts when a connection drag begins, updates as the pointer moves, announces port entry and exit, and ends as either connection:complete or connection:cancel. Use these notifications to tint legal targets or explain refusals; use connect when the completed link is the application result.

Interaction configuration

Interaction configuration controls what gestures the user may perform. Set common options at mount, or update engine-level interaction configuration at runtime with instance.getEngine().setInteractionConfig({ portVisibility: 'always' }).

Framework-level switches cover read-only mode, panning, zooming, zoom limits, zoom sensitivity, and fit-to-view. Angular also exposes switches for snapping, proximity connections, keyboard navigation, in-place editing, and canvas bounds. Built-in keyboard support handles history, deletion, arrow-key nudging, and focus-visible navigation without event wiring.

The renderer's InteractionController owns pointer and keyboard interaction logic, but it does not decide how a framework re-renders. Angular marks for check, React updates state or its external-store subscription, Vue touches a ref, and a vanilla host calls its own render path. Use the instance and its binding before reaching for this lower layer.

Was this page helpful?

Events and interaction — Grafloria · GPT-5.6 Luna