JavaScript is required

HTML renderers, layers, and plugins

The HTML platform does not use framework slots. Use renderers for node and line internals, layers for background/canvas/view content, and plugins for reusable behavior.

Renderers

type RGHtmlRenderers = {
  node?: RGHtmlRenderer<RGHtmlNodeRenderContext>;
  line?: RGHtmlRenderer<RGHtmlLineRenderContext>;
  lineLabel?: RGHtmlRenderer<RGHtmlLineRenderContext>;
  nodeExpandButton?: RGHtmlRenderer<RGHtmlExpandRenderContext>;
};

Custom nodes

renderers: {
  node({node, checked, graph}) {
    const root = document.createElement('div');
    root.className = checked ? 'service service-checked' : 'service';

    const title = document.createElement('strong');
    title.textContent = node.text || '';
    root.append(title);
    return root;
  }
}

A renderer may return a Node, DocumentFragment, text, trustedHTML(), or an empty value. Plain strings are always text:

node() {
  return '<strong>This remains text</strong>';
}

node() {
  return trustedHTML('<strong>Trusted static content only</strong>');
}

Never pass user input to trustedHTML().

Custom lines

Create SVG namespace nodes and use pathInfo and lineConfig from the context:

const SVG_NS = 'http://www.w3.org/2000/svg';

function renderLine({pathInfo, lineConfig}) {
  const path = document.createElementNS(SVG_NS, 'path');
  path.setAttribute('d', pathInfo.pathData);
  path.setAttribute('fill', 'none');
  path.setAttribute('stroke', lineConfig.line.color || '#888');
  path.setAttribute('stroke-width', String(lineConfig.line.lineWidth || 1));
  return path;
}

lineLabel replaces label content only. nodeExpandButton receives expanded, position, and toggle(event); bind toggle from the custom button.

Lifecycle renderers

Reuse DOM for complex content that updates frequently:

const nodeRenderer = {
  create(context) {
    const element = document.createElement('article');
    element.innerHTML = '<strong></strong><small></small>';
    return element;
  },
  update(element, context) {
    element.querySelector('strong').textContent = context.node.text || '';
    element.querySelector('small').textContent = context.node.data?.status || '';
  },
  destroy(element) {
    // Release renderer-owned listeners or external resources.
  }
};

Call setRenderers(nextRenderers) to replace renderers at runtime.

Layers

Available layers are background, canvasBehind, canvasAbove, and view:

layers: {
  view({element, graph}) {
    const badge = document.createElement('div');
    badge.className = 'graph-status';
    badge.textContent = 'Online';
    element.append(badge);
    return () => badge.remove();
  },
  canvasAbove({element}) {
    const overlay = document.createElement('div');
    element.append(overlay);
    return () => overlay.remove();
  }
}

background and view do not scale with the canvas. canvasBehind and canvasAbove use canvas coordinates. A layer factory returns its cleanup function.

Plugins

const statusPlugin = ({graph, host, getLayerElement, subscribe}) => {
  const output = document.createElement('output');
  getLayerElement('view').append(output);
  const stop = subscribe('selection', state => {
    output.textContent = state.checkedNodeId || state.checkedLineId || 'none';
  });
  return () => {
    stop();
    output.remove();
  };
};

const graph = await createRelationGraph('#graph', {
  data,
  plugins: [statusPlugin]
});

At runtime, call const unuse = graph.use(plugin). Plugins should return idempotent cleanup functions; graph.destroy() cleans up any plugin still in use.

Next: Toolbar, MiniView, and editing.