Skip to content
D
Documentation

Customize nodes

how-to
3 min readUpdated

Render content keyed by nodeId while Grafloria keeps the node's identity, selection, ports, links, routing, and serialized data. Use this when a rectangle is not enough—for example, to show a service name, owner, and status inside a node.

The model

Put the node's stable key in id, the renderer key in type, and application data in data. A NodeSpec holds that data and geometry; an EdgeSpec holds a connection. Keep the node's geometry in position and size; keep connections in edges. The custom content is the inside of the box. The diagram still owns hit-testing, dragging, ports, selection, and link geometry.

The component receives the node's data and its live selection state. In React, describe those values with NodeProps; in plain JavaScript, use the HTML-layer representation supplied by the node spec. A node with type: 'card' is rendered by the card entry in the framework's node registry or slot.

Plain JavaScript

Use render with a data-backed HTML description. See the custom-nodes demo for the running result. metadata.html is sanitized and placed inside the node's transformed HTML layer, so moving the node moves its content with it. The id and data remain part of the ordinary node document.

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

const host = document.querySelector('#app');
if (!(host instanceof HTMLElement)) {
  throw new Error('The #app element is required');
}
host.style.height = '400px';
const nodeId = 'deploy';

const instance = render({
  nodes: [{
    id: nodeId,
    position: { x: 120, y: 100 },
    size: { width: 260, height: 140 },
    data: { nodeId, title: 'Deploy #4213', status: 'staging' },
    metadata: {
      html: {
        content: {
          tag: 'div',
          children: [
            { tag: 'strong', text: nodeId + ': Deploy #4213' },
            { tag: 'p', text: 'building → 72%' },
            { tag: 'span', className: 'badge', text: 'staging' },
          ],
        },
      },
    },
  }],
  edges: [],
}, host);

instance.fitView();

The mounted diagram shows a card containing “Deploy #4213”, “building → 72%”, and a “staging” badge. The card remains a diagram node, rather than a floating element, when the user pans or drags it. Give #app a real height; a host without height has no canvas area to paint.

Framework bindings

The following examples use the same two nodes and one link. Each custom node is keyed by type: 'card', while the application values are keyed in data.nodeId; the body can choose its content from that key. In the framework bindings, GrafloriaFlow mounts the diagram. The container has a height so the mounted diagram is visible.

tsx
import { useEffect } from 'react';
import { GrafloriaFlow, type NodeProps } from '@grafloria/react';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

type CardData = { nodeId: string; title: string; owner: string; status: string };

const nodes: NodeSpec[] = [
  { id: 'build', type: 'card', custom: true, position: { x: 80, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'build', title: 'Build', owner: 'CI', status: 'passing' } },
  { id: 'deploy', type: 'card', custom: true, position: { x: 430, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'deploy', title: 'Deploy', owner: 'CD', status: 'ready' } },
];
const edges: EdgeSpec[] = [{ id: 'build-deploy', source: 'build', target: 'deploy' }];

function Card({ data, selected }: NodeProps<CardData>) {
  return <div style={{ height: '100%', boxSizing: 'border-box', padding: 14, border: '2px solid #94A5F0', borderRadius: 12, background: selected ? '#eef2ff' : '#fff' }}>
    <strong>{data.title}</strong><div>{data.nodeId}</div><div>owner: {data.owner}</div><span>{data.status}</span>
  </div>;
}

export default function App() {
  useEffect(() => undefined, []);
  return <div style={{ height: '400px' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={{ card: Card }} /></div>;
}

React requires custom: true on the spec when the node is rendered by nodeTypes. Vue slots and Angular templates opt into the HTML layer through their bindings. In every case, the custom body remains inside the node host, so the link stays attached when the node moves and the node remains selectable and connectable.

Updating and persisting the content

Keep the node key stable when application data changes. For a live model obtained from the instance, update through the model's tracked setters and then call DiagramInstancerenderNow() when you need the repaint immediately; do not replace the host's geometry styles. In plain JavaScript, custom renderer content mounts once, so update DOM that your renderer owns or use a framework binding that re-renders from its data.

The node's data, id, geometry, and type are part of the document. Keep those values in your NodeSpec and edges in EdgeSpec; the custom component is presentation, not a second model. This lets the same diagram remain routable and round-trip through serialization.

Pitfalls

  • A plain JavaScript or React custom node without custom: true renders as a normal rectangle; the renderer or component is not consulted. Vue slots and Angular templates opt in automatically.
  • Register or provide the node type before mounting. An unknown custom type leaves an empty host rather than creating a built-in body.
  • Put geometry in position: { x, y } and size: { width, height }; top-level x and y are not node-spec fields.
  • Interpolate user-supplied values as text. Do not place them in innerHTML.
  • A custom node does not replace ports. Use ordinary edges and declared ports when connection direction or routing matters.

See also

Was this page helpful?