Marking Blocks & Ports
<VisualLinker> finds its blocks and ports in your own markup. There are three ways to mark them — the v-vl-block/v-vl-port directives, plain data-vl-* attributes, and the component's blocks prop — and all three feed the same registry, so they can be mixed freely in one diagram.
Registration
Register the component and both directives globally:
import { createApp } from 'vue'
import { VisualLinkerPlugin } from '@macrulez/visual-linker-vue'
createApp(App).use(VisualLinkerPlugin).mount('#app')…or import them per component — in <script setup>, vVlBlock/vVlPort become v-vl-block/v-vl-port automatically:
import { VisualLinker, vVlBlock, vVlPort } from '@macrulez/visual-linker-vue'With the Nuxt module both directives are registered for you, on the server too.
v-vl-block
Marks any element — at any depth, inside any wrapper component — as a block.
<div v-vl-block="'b13'">…</div>
<div
v-vl-block="{ id: 'b13', draggable: true, dragHandle: '.title', dragBounds: 'container' }"
>…</div>A bare string is shorthand for { id }.
Options
id
string · required
The block id connections refer to via from.blockId/to.blockId.
linker
string · default: — (the nearest enclosing <VisualLinker>)
Assigns the block to the <VisualLinker name="…"> with this name, wherever it sits on the page.
draggable
boolean · default: follows config.blocks.draggable
dragHandle
string | Ref | getter | HTMLElement · default: — (the whole block)
A CSS selector inside the block, or a template ref/getter/element.
dragBounds
'container' | string | Ref | getter | HTMLElement | DragBoundsInset · default: follows config.blocks.drag.bounds
See Drag & drop. A selector string is resolved against the whole document.
portSpread
boolean | { gap?: number; padding?: number } · default: follows config.ports.spread
Spreads the connections that share a port side of this block — see Port Spreading.
highlightable
boolean · default: follows config.interaction.highlight
Hovering this block highlights its connections. See Visual States.
v-vl-port
Marks an element as a connection port. The port's point is measured on this element; it belongs to the nearest ancestor element registered as a block, unless block names one explicitly.
<div v-vl-block="'b13'">
<div v-vl-port="{ id: 'row1', side: ['left', 'right'], anchorBlockId: 'b13' }">Row 1</div>
</div>
<!-- attached to a block explicitly, from anywhere in scope -->
<span v-vl-port="{ id: 'out', block: 'b13' }" />A bare string is shorthand for { id }.
Options
id
string · required
The port id connections refer to via from.portId/to.portId.
block
string · default: — (the nearest ancestor block)
Owning block id.
linker
string · default: — (the nearest enclosing <VisualLinker>)
Same as v-vl-block's linker.
side
'top' | 'right' | 'bottom' | 'left' | 'auto' | FixedSide[] · default: 'auto'
offset
number · default: 0.5
Position along the side, 0..1.
anchorBlockId
string · default: — (unset)
anchorEl
Ref | getter | HTMLElement · default: — (unset)
side, offset, anchorBlockId and anchorEl mean exactly what they do on a core PortDescriptor — see Blocks & Ports.
spread
boolean | { gap?: number; padding?: number } · default: follows the block's portSpread
The port's own spreading, as on a core PortDescriptor — see Port Spreading.
Data attributes
No JavaScript at all — handy for server-rendered or third-party markup:
| Attribute | On | Meaning |
|---|---|---|
data-vl-block="id" | block | registers the element as a block |
data-vl-draggable | block | ""/"true" → draggable, "false" → not |
data-vl-highlightable | block | ""/"true" → highlights its connections, "false" → not |
data-vl-drag-handle=".sel" | block | drag handle, a selector inside the block |
data-vl-drag-bounds="container" | block | container, or a CSS selector of the fence element |
data-vl-port="id" | port | registers a port on the nearest block around it |
data-vl-port-block="id" | port | …or on this block explicitly |
data-vl-side="left right" | port | one side, or a space/comma-separated candidate list |
data-vl-offset="0.3" | port | position along the side, 0..1 |
data-vl-anchor="id" | port | anchorBlockId — the point is drawn on that block's border |
data-vl-port-spread="24 8" | block | portSpread: "" on, "false" off, or gap [padding] |
data-vl-spread="false" | port | the port's own spread, same values |
data-vl-linker="name" | block/port | assigns the element to the <VisualLinker name="…"> with this name |
The directives write data-vl-block/data-vl-port (plus data-vl-port-block/data-vl-linker when set) onto the element themselves, so one DOM scan finds directive-marked and attribute-marked elements alike. Every attribute name is also exported as the VL_ATTR constant.
The blocks prop
For refs held in <script>, or elements you can't add attributes to:
const cardRef = useTemplateRef('card')
const blocks = [
{ id: 'a', el: cardRef, ports: [{ id: 'p', target: rowRef }] }, // a template ref
{ id: 'b', el: '#legacy-widget' }, // a CSS selector
{ id: 'c', draggable: false }, // no el: extra config for a block marked in the template
]See the blocks prop for every field.
Precedence
For the same block id, a field from the blocks prop wins over the directive's options, which win over data attributes. A port configured in the blocks prop keeps precedence over a marked port with the same id on that block.
Which <VisualLinker> an element belongs to
- an explicit
data-vl-linker/linkername wins; - otherwise the element belongs to the nearest enclosing
<VisualLinker>— so nested instances never take each other's blocks; - an element outside every
<VisualLinker>belongs to page-scoped instances (scope="page") only.
With several page-scoped instances on one page, give each a name and assign elements explicitly.
Live discovery
Blocks and ports added, removed, or re-marked later — a v-if, a v-for, third-party markup setting attributes — are picked up automatically: the component watches its area (the whole <body> in page scope) for data-vl-* changes, and the directives notify it when options change that no attribute reflects. A rescan that finds nothing new doesn't touch the engine.
Drag and inline styles
Drag offsets are applied through the standalone CSS translate property, so they compose with any transform a block already has. Avoid a string :style binding on a draggable block — Vue replaces the whole inline style whenever that string changes, wiping the drag offset with it. An object :style binding is fine.
SSR
The directives emit their data-vl-* attributes during server rendering too, so the markup is already discoverable before hydration. The engine and its drawing layer only exist client-side, after mount.