Building Compute Nodes
A compute node combines a visual template with a registered compute function. The template defines the ports (handles) and display, while the compute function defines the data transformation.
A constant node feeds its configured value to a formatter that outputs a string:
<template id="demo-const-node">
<div style="min-width: 110px;">
<div x-flow-handle:target.left></div>
<div style="font-weight: 600; font-size: 12px;" x-text="node.data.label"></div>
<div style="font-size: 11px; opacity: 0.5;" x-text="'value: ' + node.data.value"></div>
<div style="font-size: 12px; font-family: monospace; color: #22c55e;"
x-text="node.data.$outputs ? Object.values(node.data.$outputs)[0] : ''"></div>
<div x-flow-handle:source.right="'value'"></div>
</div>
</template>
<template id="demo-format-node">
<div style="min-width: 130px;">
<div x-flow-handle:target.left="'input'"></div>
<div style="font-weight: 600; font-size: 12px;" x-text="node.data.label"></div>
<div style="font-size: 11px; opacity: 0.5;" x-text="'prefix: ' + node.data.prefix"></div>
<div style="font-size: 12px; font-family: monospace; color: #22c55e;"
x-text="node.data.$outputs ? node.data.$outputs.text : ''"></div>
<div x-flow-handle:source.right="'text'"></div>
</div>
</template>
<div x-data="flowCanvas({
nodes: [
{ id: 'n1', type: 'const', position: { x: 0, y: 0 }, data: { label: 'Constant', value: 42 } },
{ id: 'n2', type: 'format', position: { x: 250, y: 0 }, data: { label: 'Formatter', prefix: 'Result: ' } },
],
edges: [
{ id: 'e1', source: 'n1', sourceHandle: 'value', target: 'n2', targetHandle: 'input' },
],
nodeTypes: { 'const': '#demo-const-node', 'format': '#demo-format-node' },
background: 'dots',
fitViewOnInit: true,
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 220px;"
x-init="
registerCompute('const', { compute: (inputs, data) => ({ value: data.value ?? 0 }) });
registerCompute('format', { compute: (inputs, data) => ({ text: (data.prefix ?? '') + (inputs.input ?? '') }) });
document.getElementById('demo-build-compute').addEventListener('click', () => compute());
">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node"></div>
</template>
</div>
</div>
Anatomy of a compute node
A compute node has three parts:
- Node data — configuration stored in
node.data(constants, labels, settings) - Handles — named input/output ports defined in the template
- Compute function — transforms inputs + node data into outputs
// 1. Node data
{ id: 'n1', type: 'multiplier', data: { label: 'x3', factor: 3 } }
// 2. Handles in the template
<div x-flow-handle:target.left="'input'"></div>
<div x-flow-handle:source.right="'output'"></div>
// 3. Compute function
registerCompute('multiplier', {
compute(inputs, nodeData) {
return { output: (inputs.input ?? 0) * (nodeData.factor ?? 1) };
}
});
Using node data for configuration
The second argument to compute() is the node's data object. Use it to make nodes configurable without changing the compute function:
registerCompute('threshold', {
compute(inputs, nodeData) {
const value = inputs.value ?? 0;
const limit = nodeData.limit ?? 100;
return {
pass: value >= limit,
value: value,
};
}
});
// Two threshold nodes with different limits
{ id: 'low', type: 'threshold', data: { label: 'Low Check', limit: 10 } }
{ id: 'high', type: 'threshold', data: { label: 'High Check', limit: 100 } }
Multiple input ports
Nodes can have any number of named input handles. Each handle name maps to a key in the inputs object:
<!-- Three input ports -->
<div x-flow-handle:target.left="'a'" style="top: 20%;"></div>
<div x-flow-handle:target.left="'b'" style="top: 50%;"></div>
<div x-flow-handle:target.left="'c'" style="top: 80%;"></div>
registerCompute('mixer', {
compute(inputs) {
return {
mixed: (inputs.a ?? 0) + (inputs.b ?? 0) + (inputs.c ?? 0),
};
}
});
Multiple output ports
Similarly, a node can produce multiple named outputs routed to different downstream nodes:
<!-- Two output ports -->
<div x-flow-handle:source.right="'pass'" style="top: 30%;"></div>
<div x-flow-handle:source.right="'fail'" style="top: 70%;"></div>
registerCompute('splitter', {
compute(inputs) {
const value = inputs.input ?? 0;
return {
pass: value >= 50 ? value : null,
fail: value < 50 ? value : null,
};
}
});
// Route each output to different downstream nodes
{ source: 'splitter', sourceHandle: 'pass', target: 'success-handler', targetHandle: 'input' }
{ source: 'splitter', sourceHandle: 'fail', target: 'error-handler', targetHandle: 'input' }
Combining with nodeTypes
For reusable compute nodes, pair registerCompute with nodeTypes templates:
<!-- Template: handles + display -->
<template id="adder-node">
<div>
<div x-flow-handle:target.left="'a'" style="top: 30%;"></div>
<div x-flow-handle:target.left="'b'" style="top: 70%;"></div>
<div x-text="node.data.label"></div>
<div x-text="node.data.$outputs?.sum ?? '—'"></div>
<div x-flow-handle:source.right="'sum'"></div>
</div>
</template>
<!-- Config: register both type and compute -->
<div x-data="flowCanvas({
nodeTypes: { adder: '#adder-node' },
})" x-init="
registerCompute('adder', {
compute: (inputs) => ({ sum: (inputs.a ?? 0) + (inputs.b ?? 0) })
});
">
Then use it:
{ id: 'add1', type: 'adder', position: { x: 300, y: 0 }, data: { label: 'Add' } }
The template handles the visual layout, handles define the ports, and the compute function defines the math. All three are decoupled.
See also
- Overview -- registerCompute and port routing
- Reactive Data -- displaying $inputs and $outputs
- Node Types -- template registration
- Named Handles -- multiple handle ports