Declare connection points on each node to control their glyphs, labels, shared layout, and data-type rules; the same node specs work across Grafloria's framework bindings.
When to use declared ports
Use declared ports when a node needs named inputs and outputs, more than the four default connection points, or visible distinctions between connection types. The graph data belongs to the diagram model; the framework binding converts your node specs into live models, and the instance connects the rendered canvas to engine behavior.
Each node can declare a ports array on its NodeSpec. Give each port an id, then add a shape, a label, a group, or a dataType as needed. Nodes without ports keep their four deterministic default ports.
Declare and mount the ports
The samples below all render the same small data-flow diagram: a Source node has number and string outputs, and a Convert node has matching inputs. Both nodes arrange their ports in named side groups. The port glyphs have different shapes and labels; the initial number-to-number link is present in every sample. A registered type palette adds type colours and can declare compatibility beyond exact type-name matches.
The JavaScript and framework mounting patterns are covered in Build runnable workflows; this page adds port declarations and a registered type palette to the node data.
tsimport {
render,
portTypeRegistry,
type EdgeSpec,
type NodeSpec,
} from '@grafloria/element';
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
const nodes: NodeSpec[] = [
{
id: 'source',
position: { x: 80, y: 130 },
size: { width: 160, height: 120 },
label: 'Source',
metadata: {
portGroups: {
outputs: {
id: 'outputs',
side: 'right',
visibility: 'always',
layout: { strategy: 'sideLinear', args: { padding: 18 } },
},
},
},
ports: [
{
id: 'number-out',
group: 'outputs',
type: 'output',
dataType: 'number',
shape: { shape: 'circle', size: 14 },
label: { text: 'number', layout: 'outside' },
},
{
id: 'string-out',
group: 'outputs',
type: 'output',
dataType: 'string',
shape: { shape: 'square', size: 14 },
label: { text: 'string', layout: 'outside' },
},
],
},
{
id: 'convert',
position: { x: 430, y: 130 },
size: { width: 160, height: 120 },
label: 'Convert',
metadata: {
portGroups: {
inputs: {
id: 'inputs',
side: 'left',
visibility: 'always',
layout: { strategy: 'sideLinear', args: { padding: 18 } },
},
},
},
ports: [
{
id: 'number-in',
group: 'inputs',
type: 'input',
dataType: 'number',
shape: { shape: 'diamond', size: 14 },
label: { text: 'number', layout: 'outside' },
},
{
id: 'string-in',
group: 'inputs',
type: 'input',
dataType: 'string',
shape: { shape: 'triangle', size: 14 },
label: { text: 'string', layout: 'outside' },
},
],
},
];
const edges: EdgeSpec[] = [
{ id: 'number-link', source: 'source', sourceHandle: 'number-out', target: 'convert', targetHandle: 'number-in' },
];
const host = document.createElement('div');
host.style.width = '100%';
host.style.height = '420px';
document.body.append(host);
requestAnimationFrame(() => render({ nodes, edges }, host));
tsimport { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { portTypeRegistry } from '@grafloria/element';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
@Component({
selector: 'app-port-example',
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<grafloria-diagram-canvas
[(nodes)]="nodes"
[(edges)]="edges"
style="display: block; height: 420px"
/>
`,
})
export class PortExampleComponent {
nodes: NodeSpec[] = [
{
id: 'source',
position: { x: 80, y: 130 },
size: { width: 160, height: 120 },
label: 'Source',
metadata: {
portGroups: {
outputs: {
id: 'outputs',
side: 'right',
visibility: 'always',
layout: { strategy: 'sideLinear', args: { padding: 18 } },
},
},
},
ports: [
{
id: 'number-out', group: 'outputs', type: 'output', dataType: 'number',
shape: { shape: 'circle', size: 14 },
label: { text: 'number', layout: 'outside' },
},
{
id: 'string-out', group: 'outputs', type: 'output', dataType: 'string',
shape: { shape: 'square', size: 14 },
label: { text: 'string', layout: 'outside' },
},
],
},
{
id: 'convert',
position: { x: 430, y: 130 },
size: { width: 160, height: 120 },
label: 'Convert',
metadata: {
portGroups: {
inputs: {
id: 'inputs',
side: 'left',
visibility: 'always',
layout: { strategy: 'sideLinear', args: { padding: 18 } },
},
},
},
ports: [
{
id: 'number-in', group: 'inputs', type: 'input', dataType: 'number',
shape: { shape: 'diamond', size: 14 },
label: { text: 'number', layout: 'outside' },
},
{
id: 'string-in', group: 'inputs', type: 'input', dataType: 'string',
shape: { shape: 'triangle', size: 14 },
label: { text: 'string', layout: 'outside' },
},
],
},
];
edges: EdgeSpec[] = [
{ id: 'number-link', source: 'source', sourceHandle: 'number-out', target: 'convert', targetHandle: 'number-in' },
];
}
tsximport { $, component$ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance, type EdgeSpec, type NodeSpec } from '@grafloria/qwik';
import { portTypeRegistry } from '@grafloria/element';
const nodes: NodeSpec[] = [
{
id: 'source', position: { x: 80, y: 130 }, size: { width: 160, height: 120 }, label: 'Source',
metadata: { portGroups: { outputs: {
id: 'outputs', side: 'right', visibility: 'always',
layout: { strategy: 'sideLinear', args: { padding: 18 } },
} } },
ports: [
{ id: 'number-out', group: 'outputs', type: 'output', dataType: 'number', shape: { shape: 'circle', size: 14 }, label: { text: 'number', layout: 'outside' } },
{ id: 'string-out', group: 'outputs', type: 'output', dataType: 'string', shape: { shape: 'square', size: 14 }, label: { text: 'string', layout: 'outside' } },
],
},
{
id: 'convert', position: { x: 430, y: 130 }, size: { width: 160, height: 120 }, label: 'Convert',
metadata: { portGroups: { inputs: {
id: 'inputs', side: 'left', visibility: 'always',
layout: { strategy: 'sideLinear', args: { padding: 18 } },
} } },
ports: [
{ id: 'number-in', group: 'inputs', type: 'input', dataType: 'number', shape: { shape: 'diamond', size: 14 }, label: { text: 'number', layout: 'outside' } },
{ id: 'string-in', group: 'inputs', type: 'input', dataType: 'string', shape: { shape: 'triangle', size: 14 }, label: { text: 'string', layout: 'outside' } },
],
},
];
const edges: EdgeSpec[] = [
{ id: 'number-link', source: 'source', sourceHandle: 'number-out', target: 'convert', targetHandle: 'number-in' },
];
export default component$(() => (
<div style={{ height: '420px' }}>
<GrafloriaFlow
defaultNodes={nodes}
defaultEdges={edges}
onInit$={$((instance: DiagramInstance) => {
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
instance.renderNow();
})}
/>
</div>
));
vue<script setup lang="ts"> import { GrafloriaFlow } from '@grafloria/vue'; import { portTypeRegistry } from '@grafloria/element'; import type { EdgeSpec, NodeSpec } from '@grafloria/vue'; portTypeRegistry.registerAll([ { name: 'number', color: '#2563eb', compatibleWith: ['number'] }, { name: 'string', color: '#9333ea', compatibleWith: ['string'] }, ]); const nodes: NodeSpec[] = [ { id: 'source', position: { x: 80, y: 130 }, size: { width: 160, height: 120 }, label: 'Source', metadata: { portGroups: { outputs: { id: 'outputs', side: 'right', visibility: 'always', layout: { strategy: 'sideLinear', args: { padding: 18 } }, }, }, }, ports: [ { id: 'number-out', group: 'outputs', type: 'output', dataType: 'number', shape: { shape: 'circle', size: 14 }, label: { text: 'number', layout: 'outside' }, }, { id: 'string-out', group: 'outputs', type: 'output', dataType: 'string', shape: { shape: 'square', size: 14 }, label: { text: 'string', layout: 'outside' }, }, ], }, { id: 'convert', position: { x: 430, y: 130 }, size: { width: 160, height: 120 }, label: 'Convert', metadata: { portGroups: { inputs: { id: 'inputs', side: 'left', visibility: 'always', layout: { strategy: 'sideLinear', args: { padding: 18 } }, }, }, }, ports: [ { id: 'number-in', group: 'inputs', type: 'input', dataType: 'number', shape: { shape: 'diamond', size: 14 }, label: { text: 'number', layout: 'outside' }, }, { id: 'string-in', group: 'inputs', type: 'input', dataType: 'string', shape: { shape: 'triangle', size: 14 }, label: { text: 'string', layout: 'outside' }, }, ], }, ]; const edges: EdgeSpec[] = [ { id: 'number-link', source: 'source', sourceHandle: 'number-out', target: 'convert', targetHandle: 'number-in' }, ]; </script> <template> <div style="height: 420px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" /> </div> </template>
When the canvas mounts, you see two labelled nodes, four always-visible ports, and the initial number-to-number connection. The source and target groups inherit their side and sideLinear layout; each port supplies its own glyph, label, and type. In every sample, the palette colors number ports blue and string ports purple. Drag number-out to string-in to see the mismatch refused; both registrations allow only the same named type.
Port fields that shape the result
| Option | Type | Default | What it does |
|---|---|---|---|
ports | PortSpec[] | Omitted: four deterministic default ports | Declares the node's own connection points. |
shape | Shape object | Circle | Chooses circle, square, diamond, triangle, or an SVG path; size sets the glyph box in pixels. |
label | Label object | No label; when present, layout defaults to outside | Adds text near the port. Choose inside, outside, orthogonal, or radial placement. |
group | string | No group | Inherits shared settings from the same id in the node's metadata.portGroups; a port's own settings override group values. |
metadata.portGroups | Node metadata object | No groups | Defines shared port settings such as side, visibility, shape, and layout. A sideLinear layout distributes group members along that side. |
dataType | string | Untyped | Associates the port with a registered type for glyph colour and connection compatibility. |
The default visibility mode shows ports on hover. The examples set each group's visibility to always so the ports and their labels appear immediately. shape: 'path' accepts caller-provided SVG path data; use it when the built-in glyphs do not distinguish your port.
See the port behaviors
The port shapes demo compares circle, square, diamond, triangle, and custom-path glyphs as distinct SVG primitives.
The port labels demo shows labels placed inside, outside, and orthogonal to their glyphs.
The port groups and layouts demo compares a side column, a line segment, and an ellipse spread.
The typed ports demo registers number and string types, then shows a matching connection and a mismatched target.
Pitfalls
- If the node spec omits
ports, it keeps the four deterministic defaults; add aportsarray to replace that set with your declared ports. - Put shared configuration under the node's
metadata.portGroupsand match each port'sgroupvalue to that entry's id. A port-level value takes precedence over the inherited group value. - Register named data types to assign colours or declare compatibility beyond exact matches. Compatibility is directional when
compatibleWithis used; an untyped endpoint remains unconstrained. - A
dataTypealone does not pick a colour. Register a color for the type, then the renderer uses it for that type's port glyph.
Related
- Validate connections for connection rules beyond a port's direction and data type.
- Handle connection interactions for responding to connection gestures.
- The diagram model and document for how diagram specs become the live model.
Was this page helpful?