Skip to content
D
Documentation

Custom node content

how-to
3 min readUpdated

Render custom content inside nodes

Use custom node content when the inside of a node is application UI rather than a stock SVG shape. Grafloria keeps the node's geometry, hit testing, ports, links, selection, and serialization; your framework renders the node body.

The canvas needs a parent with a real height. The examples below render a pair of cards and a link between them, so you can drag the cards and see the link follow their geometry.

Choose the rendering surface

  • Plain JavaScript uses the HTML metadata renderer or a registered node renderer.
  • Angular uses an ng-template for a node type.
  • Qwik and React use a component mapped by nodeTypes.
  • Vue uses a #node-<type> slot.

Set a node's size, and make the custom content fill that box with height: 100% and box-sizing: border-box. Ports remain part of the node specification; they do not belong in the custom component.

Render HTML in plain JavaScript

Use render with HTML metadata when you want a sanitised, declarative HTML subtree. Grafloria mounts it inside the node's transformed HTML layer, so it moves with the node.

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

const target = document.getElementById('canvas');
if (!target) {
  throw new Error('Missing #canvas');
}
target.style.height = '400px';

const instance = render({
  nodes: [
    {
      id: 'build',
      position: { x: 80, y: 90 },
      size: { width: 230, height: 110 },
      metadata: {
        html: {
          content: {
            tag: 'div',
            children: [
              { tag: 'strong', text: 'Build' },
              { tag: 'div', text: 'owner: CI' },
              { tag: 'span', className: 'badge', text: 'passing' },
            ],
          },
        },
      },
    },
    {
      id: 'deploy',
      position: { x: 430, y: 90 },
      size: { width: 230, height: 110 },
      label: 'Deploy',
    },
  ],
  edges: [{ id: 'build-deploy', source: 'build', target: 'deploy' }],
}, target);

instance.fitView();

The first node shows a bold title, owner text, and a badge inside a foreignObject; the second node remains a stock node. The HTML description is written as text and elements, not injected as raw innerHTML. A later change to the model does not re-run a one-time custom mount; update DOM you own or use a framework component with its own reactive source.

html
<div id="canvas" style="height: 400px"></div>

Use a custom component

Give the custom node a type, provide its data, and use the binding's custom-content mechanism. The following examples all render the same two cards and edge.

ts
import { Component } from '@angular/core';
import { DiagramCanvasComponent, GrafloriaNodeDefDirective } from '@grafloria/angular';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent, GrafloriaNodeDefDirective],
  template: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display:block; height:400px">
      <ng-template grafloriaNode="card" let-data="data">
        <div style="height:100%; box-sizing:border-box; padding:10px 14px;
                    border:1.5px solid #94A5F0; border-radius:12px;
                    background:#fff; font-family:inherit">
          <div style="font-weight:700">{{ data['title'] }}</div>
          <div style="font-size:12px; color:#5A6478">owner: {{ data['owner'] }}</div>
          <span style="font-size:11px; font-weight:600; color:#059669">{{ data['status'] }}</span>
        </div>
      </ng-template>
    </grafloria-diagram-canvas>
  `,
})
export class CustomNodeContentComponent {
  nodes = [
    { id: 'build', type: 'card', position: { x: 80, y: 90 }, size: { width: 230, height: 110 }, data: { title: 'Build', owner: 'CI', status: 'passing' } },
    { id: 'deploy', type: 'card', position: { x: 430, y: 90 }, size: { width: 230, height: 110 }, data: { title: 'Deploy', owner: 'CD', status: 'ready' } },
  ];
  edges = [{ id: 'build-deploy', source: 'build', target: 'deploy' }];

}
The rendered cards show application content inside the node boxes while the connecting edge remains attached.

Angular's template, Qwik's nodeTypes, React's nodeTypes, and Vue's named slot are the framework-specific registrations. In each case the rendered content occupies the node box while the engine continues to draw and hit-test the edge and ports.

Keep content and model data separate

The node payload is data for the content; it is not a second geometry system. Use a live DiagramInstance when you need to reconcile nodes or repaint after a model mutation. In a custom component, use the framework's reactive state for frequently changing application content. A mounted custom host is not recreated during a drag, so component state survives movement; selection state is supplied by the binding where that binding exposes it.

Use a stock shape instead of custom content when you only need a terminal, document, or styled rectangle. A per-node shape keeps that node on the SVG path and avoids a component registration.

Check the result

Open the custom nodes demo to see custom shapes retain their links while moving. The HTML nodes demo shows rich content moving and scaling with the camera.

Pitfalls

  • A JavaScript custom renderer must be registered before render(); an unknown custom type gets an empty host rather than a late replacement.
  • In React, custom: true and an exact nodeTypes key are both required.
  • In Vue, an exact #node-card slot opts type: 'card' into custom rendering; a wildcard slot does not opt a node in by itself.
  • Do not put untrusted values into innerHTML. The declarative HTML path uses text content; use text nodes or framework bindings for user-supplied values.
  • Do not style the host's geometry. Style a child inside it; the engine rewrites the host's position and size.

Was this page helpful?