Skip to content

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:

ts
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:

ts
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.

vue
<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.

vue
<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:

AttributeOnMeaning
data-vl-block="id"blockregisters the element as a block
data-vl-draggableblock""/"true" → draggable, "false" → not
data-vl-highlightableblock""/"true" → highlights its connections, "false" → not
data-vl-drag-handle=".sel"blockdrag handle, a selector inside the block
data-vl-drag-bounds="container"blockcontainer, or a CSS selector of the fence element
data-vl-port="id"portregisters a port on the nearest block around it
data-vl-port-block="id"port…or on this block explicitly
data-vl-side="left right"portone side, or a space/comma-separated candidate list
data-vl-offset="0.3"portposition along the side, 0..1
data-vl-anchor="id"portanchorBlockId — the point is drawn on that block's border
data-vl-port-spread="24 8"blockportSpread: "" on, "false" off, or gap [padding]
data-vl-spread="false"portthe port's own spread, same values
data-vl-linker="name"block/portassigns 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:

ts
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/linker name 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.