JavaScript is required

Custom Node Expand Button Slot

This example shows how to replace relation-graph's default expand or collapse control with a custom slot-rendered badge attached to each node. It loads a local hierarchy, collapses one branch on startup, and uses the slot-provided toggle callback to support both click and touch interaction.

Custom Node Expand Controls With a Slot-Rendered Badge

What This Example Builds

This example builds a full-height hierarchy viewer whose main lesson is not the dataset itself, but the replacement of relation-graph’s default expand/collapse affordance. The graph opens as a centered rectangular tree of subsystem-like nodes, uses straight border-anchored links, and renders a pink pill-shaped control on the right side of each expandable node.

Users can inspect the hierarchy and toggle branches through that custom holder. One branch is collapsed immediately after initialization, so the custom control is visible in both collapsed and expanded states instead of only after manual interaction.

How the Data Is Organized

The data comes from a local async helper that returns one rooted JSON object with rootId, nodes, and lines. The node records are simple id and text pairs, while the line records describe parent-child relationships with from, to, and text, making the structure easy to swap with equipment trees, product breakdowns, subsystem maps, or other hierarchical dependency data.

Before the graph is loaded, the code performs one small preprocessing step: it maps over lines and assigns a fallback id when a relation record does not already have one. After that normalization, the example passes the finished object to setJsonData.

How relation-graph Is Used

The example wraps the graph in RGProvider, then reads the instance through RGHooks.useGraphInstance() so initialization can stay inside the React component rather than using an external ref. On mount, it loads the local JSON, injects it with setJsonData, centers the canvas, fits the viewport, and collapses node 6 to create an intentionally progressive first view.

The graph options are tuned to keep the visual context simple while highlighting the custom holder. layout.layoutName is set to center, reLayoutWhenExpandedOrCollapsed is enabled, and the default expand-holder position is moved to the right. The nodes use RGNodeShape.rect, the lines use RGLineShape.StandardStraight, and line junctions are anchored on node borders through RGJunctionPoint.border.

The key customization is nodeExpandButtonSlot={NodeExpandHolder} on RelationGraph. That slot replaces the built-in branch control with a React component that receives the current node plus the provided expandOrCollapseNode callback. The stylesheet imported by the example does not currently add meaningful overrides, so most of the holder appearance comes directly from the slot-rendered markup and utility classes.

Key Interactions

The main interaction is branch toggling through the custom holder rather than through relation-graph’s default expand button. The holder binds the same callback to both onClick and onTouchEnd, which makes the demo usable on desktop and touch devices without a separate mobile implementation.

The holder label also reflects branch state. When node.expanded === false, the control shows Open; otherwise it shows Close. Combined with the initial collapse of node 6, that gives developers a clear reference for building state-aware replacement controls.

Key Code Fragments

This fragment shows that the graph data is normalized before loading, then centered, fit to the viewport, and initialized with one collapsed branch.

const initializeGraph = async () => {
    const myJsonData: RGJsonData = await loadMockJsonData();
    if (myJsonData.lines) {
        myJsonData.lines = myJsonData.lines.map((line, index) => ({
            ...line,
            id: line.id || `line-${index}`
        }));
    }
    await graphInstance.setJsonData(myJsonData);
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
    graphInstance.collapseNode(graphInstance.getNodeById('6'));
};

This fragment shows the graph configuration that keeps the hierarchy readable while making expansion or collapse trigger a relayout.

const graphOptions: RGOptions = {
    debug: false,
    reLayoutWhenExpandedOrCollapsed: true,
    defaultExpandHolderPosition: 'right',
    defaultNodeShape: RGNodeShape.rect,
    defaultLineShape: RGLineShape.StandardStraight,
    defaultJunctionPoint: RGJunctionPoint.border,
    layout: {
        layoutName: 'center'
    }
};

This fragment proves that the example replaces the built-in expand control with a custom slot renderer.

<RelationGraph
    options={graphOptions}
    nodeExpandButtonSlot={NodeExpandHolder}
/>

This fragment shows the actual slot contract: the custom holder reads node.expanded and uses the supplied toggle callback for both click and touch input.

const NodeExpandHolder: React.FC<RGNodeExpandHolderProps> = ({
    node,
    expandOrCollapseNode
}) => {
    return (
        <div className="absolute left-[100%] top-[50%] transform translate-y-[-50%] translate-x-[5px]">
            <div
                className="pointer-events-auto cursor-pointer rounded-lg border px-2 h-8 bg-pink-400 hover:bg-pink-800 hover:text-white flex place-items-center justify-center"
                onClick={expandOrCollapseNode}
                onTouchEnd={expandOrCollapseNode}
            >
                {node.expanded === false ? "Open" : "Close"}
            </div>
        </div>
    );
};

What Makes This Example Distinct

Compared with nearby examples, this one is unusually focused on the nodeExpandButtonSlot contract itself. The comparison data shows that graph-instance-api also demonstrates branch control through instance APIs, but that example teaches external imperative control, while this one teaches how to replace the node-local affordance inside the graph surface.

It is also narrower than built-in-slots. Instead of surveying several overlay slot layers, this example concentrates on one slot with immediate behavioral consequences: it reads node.expanded, renders state-specific text, and calls the slot-provided expandOrCollapseNode callback directly.

Another distinguishing detail is the combination of a right-offset custom badge, onClick plus onTouchEnd binding, and an immediate post-load collapse of node 6. That means the custom holder appears in a real collapsed state as soon as the graph loads, which makes the example a stronger starting point for custom branch-affordance work than a fully expanded demo.

Where Else This Pattern Applies

This pattern transfers well to product-structure viewers, equipment or subsystem diagrams, organizational hierarchies, and knowledge trees where the default expand marker is too small, too abstract, or visually inconsistent with the rest of the application.

It is also useful when a project needs a branch control with explicit wording, touch-friendly hit targets, or custom branding. The same slot approach can be extended to icon buttons, status-aware badges, permission-gated expand controls, or holders that trigger analytics while still delegating the actual branch state change to relation-graph’s provided callback.