Upgrading WireFlow to v0.2.1-alpha

This is the single upgrade path from the v0.1.x line. It resyncs the bundled AlpineFlow engine to v0.2.1-alpha — a broad correctness-and-performance pass spanning the measurement/layout lifecycle, edge routing, viewport culling, undo/redo history, interaction fixes, and an observable fitView. Those shifts live inside the vendored bundle (dist/alpineflow.bundle.esm.js), so WireFlow users receive them the moment they upgrade and republish assets — you don't opt into them separately.

On the WireFlow side there are a few native things to know about: enum-prop validation now throws, the graph-mutating trait methods keep server state in sync, and <x-flow> renders wire:ignore by default — plus a correction to the record on flowClear(). Everything else in the WithWireFlow trait and the <x-flow> component is unchanged. The particle-emission and bulk-animation trait methods that were staged as a separate (never-tagged) v0.2.0-alpha are folded in below under Animation & particles.

For the full engine-side rationale and per-entry before/after, see the companion AlpineFlow v0.2.1-alpha migration guide — this guide summarizes what reaches you through the bundle and points there rather than duplicating every entry.

What WireFlow users get automatically

After upgrading + php artisan wireflow:install --force + npm run build:

  • Nodes with rich templated content render correctlyResizeObserver tracks rendered dimensions, eliminating the class of bugs where node heights stayed locked to a stale pre-render measurement and clipped content that grew after Alpine templates populated.
  • Child layouts update reactively — mutating node.childLayout.columns (or gap, padding, headerHeight, direction, stretch) triggers layoutChildren on the parent automatically. Same for addNodes with parentId.
  • $flow.batch(fn) available in Blade templates — wrap bulk mutations to consolidate many addNodes / addEdges calls into a single layout pass.

New FlowNode properties

All optional. You may add any of these to the array you return in public array $nodes = [...]:

  • fixedDimensions — set true to force inline height on a leaf node (otherwise content drives height)
  • resizeObserver — set false to exclude a node from the shared observer
  • minDimensions / maxDimensions — bounds applied by the observer before writing node.dimensions
public array $nodes = [
    [
        'id' => 'stats',
        'position' => ['x' => 0, 'y' => 0],
        'data' => ['label' => 'Stats'],
        'fixedDimensions' => true,           // opt-in to locked height
        'minDimensions' => ['width' => 200], // Partial — height unconstrained
        'maxDimensions' => ['width' => 400, 'height' => 300],
    ],
];

Alpha-breaking behavior changes (inherited from AlpineFlow)

Leaf nodes no longer receive inline style.height

Before v0.2.1-alpha, every node got style.height applied from node.dimensions.height. Now only nodes matching any of three container signals receive inline height:

  1. The node has childLayout (auto-layout container)
  2. The node is a parent of other nodes — some other node references it via parentId (e.g., group nodes with manually-positioned children)
  3. The node has fixedDimensions: true

Plain leaf nodes let their content determine height.

WireFlow-side impact: if you were setting explicit dimensions: ['width' => X, 'height' => Y] on a leaf node in your $nodes array and expected the height to persist, add 'fixedDimensions' => true to that node. Otherwise the value will be overwritten by measurement.

Auto-promotion of fixedDimensions

Three server-triggered paths automatically set fixedDimensions = true on affected nodes so the intended height persists:

  • flowAnimate() targeting dimensions.height
  • Resize drag (client-side, user-initiated)
  • compute() output (when compute writes a height)

Layout dedup

At most one layoutChildren call per parent per animation frame. Any tests or tooling that counted duplicate layout calls per mutation should now expect lower counts.

Client-local dimensions

node.dimensions updates reactively from the ResizeObserver — different clients may measure different values (fonts, zoom, DPI). Dimensions are excluded from collab sync if you're using Yjs collaboration; they're client-local state, not part of the canonical document.

The rest of the engine pass (through the bundle)

The measurement/layout shifts above are the ones most likely to touch a Livewire app, but the resynced bundle carries the whole v0.2.1-alpha engine pass. The rest are summarized here; each links to the companion AlpineFlow guide for the before/after and full rationale. None require WireFlow-side code changes.

  • History & undo/redo — selection is no longer undoable; identical consecutive states are deduped; drag captures on commit (not pointer-down); arrow-key nudges capture once per physical press. undo()/redo() now emit the flow-restore DOM event (e.detail = { nodes, edges, origin: 'undo' | 'redo' }).
  • Change & restore eventsflow-restore payload renamed its interim source tag to origin ('undo' | 'redo' | 'load'); flow-nodes-change / flow-edges-change gained origin: 'drop' | 'paste' | 'api' | 'load'. Your onNodesChange / onEdgesChange handlers receive origin automatically.
  • Edge routing — avoidant edges use rounded corners instead of a Catmull-Rom spline (every avoidant d string changes); routes tie-break deterministically; obstacles burying an endpoint are excluded; hidden nodes stop being obstacles; edges simplify to a bezier during a node drag and snap back on drop; animate() settle re-runs edge geometry.
  • Viewport culling — defaults to 'auto': at ≥150 nodes, off-screen nodes and edges get display:none.
  • Canvas lifecycledestroy() now actually runs (fullscreen exit, store unregister, listener/observer disposal — a long-dormant onDestroy handler fires for the first time); handle pointerdown is delegated in the capture phase (markup inside a handle no longer gets its own pointerdown).
  • Interaction & input — double-click zoom can be a 'toggle'; wheel zoom works over a node (.nopan gates panning only; .nowheel gates wheel); keyboard shortcuts no longer hijack contenteditable node bodies; drops over panels/controls/minimap/devtools cancel instead of adding a node.
  • Schema, export, fitView — schema-edge geometry computes arithmetically by default (2.3–3.8× faster); toImage() now includes edges (and gains scale / format / quality); fitView() returns Promise<boolean>.

Config escape hatches you can pass through <x-flow>

Every engine-side default flip has an escape hatch you set in the canvas config your component hands to <x-flow> — no client JS required:

// In your Livewire component
public array $config = [
    'viewportCulling' => false,          // always render (default 'auto' culls ≥150 nodes)
    'avoidantSimplifyOnDrag' => false,   // full routing during drags (default simplifies)
    'delegatedHandleEvents' => false,    // per-handle pointerdown listeners (default delegated)
    'zoomOnDoubleClick' => 'step',       // 'step' (default) | 'toggle' | false
    'schemaHandleGeometry' => 'dom',     // legacy DOM measurement (default 'auto')
    'cullingAutoThreshold' => 500,       // raise the auto-cull trip point (default 150)
];
<x-flow :config="$config" :nodes="$nodes" :edges="$edges" />

$flow.batch(fn) in Blade templates

For bulk client-side mutations from Alpine scope:

<x-flow ...>
    <button x-on:click="$flow.batch(() => {
        $flow.addNodes(newNodes);
        $flow.addEdges(newEdges);
    })">Load preset</button>
</x-flow>

Server-driven mutations via the WithWireFlow trait don't need explicit batching — they already go through a single dispatch per call.

Trait methods now keep server state in sync

Before v0.2.1-alpha, flowAddNodes, flowRemoveNodes, flowAddEdges, and flowRemoveEdges only dispatched to the client — your server-side $this->nodes and $this->edges arrays stayed untouched. This forced consumers to manually filter those arrays before/after each call to keep server and client in sync. Without the manual work, the next Livewire re-render would morph the stale server state back over the canvas (e.g., a removed node reappearing).

Now the trait mutates server-side arrays automatically (when they exist as public arrays), mirroring the client-side cascade behavior:

  • flowRemoveNodes removes the targeted IDs, recursively removes all descendants (via parentId chain), and cascade-removes any edges whose source or target is a removed node
  • flowAddNodes appends to $this->nodes
  • flowAddEdges appends to $this->edges
  • flowRemoveEdges removes matching id entries from $this->edges

Before (manual):

#[On('node-deleted')]
public function onNodeDeleted(string $nodeId): void
{
    $deletedEdgeIds = $this->deletedEdgesFor($nodeId);

    $this->edges = collect($this->edges)
        ->filter(fn ($e) => $e['source'] !== $nodeId && $e['target'] !== $nodeId)
        ->values()
        ->all();
    $this->flowRemoveEdges($deletedEdgeIds);

    $this->nodes = collect($this->nodes)
        ->filter(fn ($n) => $n['id'] !== $nodeId)
        ->values()
        ->all();
    $this->flowRemoveNodes([$nodeId]);
}

After (the trait handles it):

#[On('node-deleted')]
public function onNodeDeleted(string $nodeId): void
{
    $this->flowRemoveNodes([$nodeId]);
}

Migration: if your existing code manually filters $nodes/$edges before calling these methods, you can delete those manual filters. Keep them only if you need different cascade semantics than the client-side default. Fallback: if your component doesn't have $nodes/$edges as public array properties, the trait gracefully skips the server-side mutation and just dispatches (current behavior, fully backwards compatible).

<x-flow> renders wire:ignore by default

The <x-flow> root now carries wire:ignore, so Livewire's DOM morphing skips the canvas subtree. Before, a Livewire re-render could morph — and tear down — the live canvas AlpineFlow had built on the client.

{{-- BEFORE: Livewire morphs could tear down the canvas subtree --}}
{{-- AFTER: subtree ignored by morph; canvas survives re-renders --}}
<x-flow :nodes="$nodes" :edges="$edges" />

Escape hatch. If you deliberately relied on Livewire morphing the canvas internals, opt out with :wire-ignore="false":

<x-flow :wire-ignore="false" :nodes="$nodes" :edges="$edges" />

flowClear() is destructive — a correction to the record

Some earlier notes described $clear() as a declared no-op. That is false for v0.2.1-alpha — the real implementation is present in the vendored bundle. $this->flowClear() dispatches flow:clear, which empties nodes and edges and resets the viewport to the origin. Treat it as destructive.

$this->flowClear();  // dispatches flow:clear → canvas $clear()
// v0.2.1-alpha: ALREADY DESTRUCTIVE — empties nodes/edges, resets viewport.
// (A future engine resync layers a restore event with origin:'load' on top;
//  the destructive behavior itself is already live in this bundle.)

New trait methods — runState (D2)

Two new methods for driving node visual state from the server:

Method Description
flowSetNodeState(string|array $ids, string $state) Set runState on one or more nodes. Mutates $this->nodes + dispatches flow:setNodeState
flowResetStates() Clear runState from all nodes. Mutates $this->nodes + dispatches flow:resetStates

Valid states: pending (baseline, no class) running, completed, failed, skipped.

// Drive a subscription workflow from the server
#[On('job-queued')]
public function onJobQueued(string $nodeId): void
{
    $this->flowSetNodeState($nodeId, 'running');
}

#[On('job-finished')]
public function onJobFinished(string $nodeId, bool $success): void
{
    $this->flowSetNodeState($nodeId, $success ? 'completed' : 'failed');
}

The theme automatically provides visual defaults — violet pulse (running), teal flash (completed), red pulse (failed), dim + grayscale (skipped). No per-project CSS required.

Nothing changed in the rest of the trait

For clarity:

  • All other WithWireFlow trait methods — unchanged (same signatures, same events)
  • <x-flow> Blade component — unchanged apart from the wire:ignore default above
  • Wire-bridge dispatches — unchanged
  • #[Renderless] / event listener patterns — unchanged

Your existing WireFlow code continues to work without modification. The bundled AlpineFlow engine improvements surface entirely on the client side.

Tier B — API improvements (also included)

The same bundle includes six independent API additions from AlpineFlow's Tier B chapter. No WireFlow-side changes — these surface in the <x-flow> canvas directly.

B1 — text-align: center moved to themes

Structural CSS no longer carries the centering opinion. Each shipped theme (default, flux) opts in. If you use a bare custom theme, add text-align: center to your .flow-node rule.

B2 — getNodeElement(id) + getNodeIdFromElement(el)

Public accessors on the canvas. Replace document.querySelector('[data-flow-node-id="..."]') with $flow.getNodeElement(id) and $flow.getNodeIdFromElement(el).

B3 — Enhanced drop zone

  • dropMimeTypes config — accept custom MIME types (default ['application/alpineflow'])
  • Target detection now returns the deepest node under the cursor (not outermost)
  • .flow-canvas-drag-over class auto-applied during valid drag-over
  • $flow.getNodeAtPoint(clientX, clientY) public utility

B4 — .flow-node-dragging class

Auto-applied during drag, removed on drop/cancel. Mirrors the existing .flow-node-selected pattern.

B5 — defaultEdgeType config

Canvas-level default for edge type: flowCanvas({ defaultEdgeType: 'smoothstep' }). Per-edge type wins.

B6 — Edge class forwarding to label

An edge's class now applies to both the SVG <g> and the edge label <div>.

Tier C — Convenience & Polish (also included)

Four additional items from AlpineFlow's Tier C chapter. No WireFlow-side API changes — all surface directly in the <x-flow> canvas.

C1 — data-flow-node-type attribute

Node DOM elements now carry data-flow-node-type="..." sourced from node.type. Style nodes by type with plain CSS: [data-flow-node-type="page"] { ... } — no Alpine class bindings required.

C2 — Auto-layout excludes parented nodes (alpha-breaking default flip)

layout(), forceLayout(), treeLayout(), and elkLayout() now filter out nodes with parentId before passing to the layout engine. Their positions are managed by childLayout and were never meant to participate in top-level auto-layout. Pass { includeChildren: true } to restore the previous behavior.

C3 — fitView vertical centering

Regression guard added for correct vertical centering with varying-height nodes. (No behavioral change — investigation confirmed the math was already correct, likely resolved by Tier A's border-box fix.)

C4 — Node-type connection rules

New canvas-level connectionRules config filters connections by node type. Runs BEFORE handle-level validation (x-flow-handle-validate) — both must pass. Supports both map-based rules and a custom validate function:

// In your Livewire component's flowCanvas config
$config = [
    'connectionRules' => [
        'byType' => [
            'page' => ['page'],          // pages can only connect to pages
            'field' => [],               // fields cannot connect to anything
        ],
        // Or function-based for complex rules (JS evaluation via JsRaw, if exposed)
    ],
];

Also applies to addEdges — programmatic edge additions are now filtered too, so server-side flowAddEdges() calls respect the same rules.

Workflow addon

The AlpineFlow workflow addon (@getartisanflow/alpineflow/workflow) is now available through WireFlow. The install command will prompt you to add it:

# Interactive — prompts for addon selection
php artisan wireflow:install --force

# Non-interactive — explicitly include workflow
php artisan wireflow:install --force --no-interaction --with=workflow

# Non-interactive — core only (no addons)
php artisan wireflow:install --force --no-interaction

The addon adds $flow.run() for structured workflow execution. See the AlpineFlow workflow addon reference for the complete API.

Toolbar components validate enum props (alpha-breaking)

<x-flow-toolbar> and <x-flow-edge-toolbar> now validate their enum props in the constructor and throw InvalidArgumentException (naming the valid values) on an unrecognized value, instead of silently passing it through and falling back to the default.

Prop Component Valid values
position <x-flow-toolbar> top, bottom, left, right
align <x-flow-toolbar> center, start, end
show both selected, always

Most likely break — align. Earlier docs incorrectly listed align="left" / align="right"; those values were never recognized by the underlying directive and silently centered. They now throw. Alignment is flow-relative — for a top/bottom toolbar, start = left and end = right.

Before (silently centered — and now throws):

<x-flow-toolbar position="bottom" align="right">

After:

<x-flow-toolbar position="bottom" align="end">

If toolbars were rendering centered when you expected left/right alignment, this is why: switch leftstart and rightend.

Animation & particles (staged as v0.2.0-alpha)

These server-callable additions — particle emission, bulk animation control, the flowHighlightPath option pass-through, and the animation behavior changes inherited from AlpineFlow — were originally staged as a separate v0.2.0-alpha that was never tagged. They reach you as part of this single v0.1.x → v0.2.1-alpha upgrade.

New trait methods

Four new particle firing methods and three new bulk-control methods are available on WithWireFlow:

Particle emission

$this->flowSendParticleAlongPath(string $path, array $options = []);
$this->flowSendParticleBetween(string $source, string $target, array $options = []);
$this->flowSendParticleBurst(string $edgeId, array $options);
$this->flowSendConverging(array $edgeIds, array $options);
  • flowSendParticleAlongPath — fires along an SVG d string; no edge required
  • flowSendParticleBetween — fires along a straight line between two node centers
  • flowSendParticleBurst — fires N particles on one edge with count + stagger options
  • flowSendConverging — fires from several edges arriving at a target simultaneously

See Particles → v0.2.0-alpha additions for examples.

Bulk animation control

$this->flowCancelAll(array $filter = [], array $options = []);
$this->flowPauseAll(array $filter = []);
$this->flowResumeAll(array $filter = []);

$filter accepts ['tag' => 'name'] or ['tags' => ['a', 'b']]. For flowCancelAll, $options accepts ['mode' => 'jump-end' | 'rollback' | 'freeze'] — matching the new AlpineFlow stop-mode contract.

Use case: start a tagged ambient animation (flowAnimate(..., ['tag' => 'ambient', 'loop' => true])), then pause/resume/cancel the whole group from server event handlers.

Option pass-through fix for flowHighlightPath

What changed. Prior to v0.2.0-alpha, flowHighlightPath() silently dropped any option other than color, size, duration, and delay. Beam-renderer options (renderer, gradient, followThrough, length, width) and anything else passed in $options didn't reach the particle.

After v0.2.0-alpha. All options pass through transparently. Defaults are still applied for unset fields.

Why. Livewire users couldn't use the new beam gradient from flowHighlightPath without the fix — a visible gap between the advertised AlpineFlow capability and what Wireflow exposed.

// Before: renderer and gradient silently ignored
$this->flowHighlightPath(['a', 'b', 'c'], [
    'renderer' => 'beam',
    'gradient' => [...],  // dropped 😞
]);

// After: full pass-through
$this->flowHighlightPath(['a', 'b', 'c'], [
    'renderer' => 'beam',
    'length' => 50,
    'width' => 3,
    'gradient' => [
        ['offset' => 0, 'color' => '#8b5cf6', 'opacity' => 0],
        ['offset' => 1, 'color' => '#fff',    'opacity' => 1],
    ],
    'duration' => 900,
    'delay' => 200,
]);

No migration step needed — code that was using the (previously-ignored) extra options will start working.

Breaking changes inherited from AlpineFlow

WireFlow ships the updated AlpineFlow bundle (vendor/wireflow/dist/alpineflow.bundle.esm.js), so the following AlpineFlow v0.2.0-alpha breaking changes apply to any WireFlow canvas. Full details with before/after in the AlpineFlow migration guide:

  • Beam duration now includes follow-throughonComplete fires after the tail exits. Escape hatch: 'followThrough' => false
  • loop: 'reverse' renamed to loop: 'ping-pong''reverse' kept as alias
  • handle.reverse() on finished handles now plays backward — was no-op (canvas-level, not directly relevant server-side)
  • Superseded animations no longer fire onComplete — if you were relying on onComplete firing when an animation was interrupted by a new one on the same keys, it won't anymore
  • structuredClonesafeClone with warn-once fallback — fixes Alpine reactive proxy errors; no action needed
  • ParticleHandle.getCurrentPosition() reads internal state — no user-facing change
  • bounceStiffness/bounceDamping recomputation — inertia motion bouncing behavior adjusted to match Framer Motion's bounciness × (1 - damping/100) model

New AlpineFlow capabilities now reachable from PHP

All through the existing flowAnimate() and flowUpdate() — no new trait methods required, pass via $options:

  • Physics motion'motion' => 'spring.wobbly' or ['type' => 'spring', 'stiffness' => 200]
  • State-aware cancellation'while' predicate (but since predicates are JS-only, the server-friendly form is 'tag' + flowCancelAll() from another handler)
  • Start state'startAt' => 'end' for reverse-from-target animations
  • On-lifecycle hooksonStart / onProgress / onComplete are JS-only and stripped when serialized server→client; use tag + flowCancelAll() or a delayed dispatch for server-side sequencing
  • Tagging'tag' => 'name' or 'tags' => ['a', 'b'] for grouping animations under flowCancelAll/flowPauseAll/flowResumeAll

Not yet reachable from server (planned)

These AlpineFlow capabilities don't yet have server-side wrappers. Tracked for a future release:

  • $flow.timeline() — client-only fluent builder. The server surface would need a JSON-serializable timeline description (steps, parallel, await/when/else, loop, tag inheritance) that the wire-bridge replays into canvas.timeline(). Use it from Alpine inside <x-flow> for now; server orchestration is possible today by chaining flowAnimate() calls with staggered durations + tag for group cancel.
  • $flow.transaction() — server-coordinated transaction with rollback (harder to express; needs architectural work on how server observes transaction state)
  • $flow.group('name') — fluent group builder; can be approximated today via manual 'tag' => 'name' on every animate call + flowCancelAll(['tag' => 'name'])
  • Per-handle controlshandle.pause()/resume()/reverse() on a specific handle needs handle-by-id addressing; tracked as the "id-based controls" initiative
  • $flow.record() / $flow.replay() — data-transfer pattern not yet defined (client → server? server → client as Recording JSON for demo loops?)

Installation / upgrade

composer require getartisanflow/wireflow:^0.2.1-alpha
php artisan wireflow:install --force
npm run build

The install command now prompts for optional addons (workflow). In CI or automated environments, use --no-interaction --with=workflow to include addons without prompts.

The bundled AlpineFlow dist (dist/alpineflow.bundle.esm.js) now contains v0.2.1-alpha. The --force flag republishes JS/CSS assets into public/vendor/alpineflow/ so your site picks up the new bundle.

If you import AlpineFlow directly via npm alongside WireFlow, bump @getartisanflow/alpineflow to ^0.2.1-alpha and rebuild.

Questions or issues?

File an issue at github.com/getartisanflow/wireflow/issues.