This tutorial walks you through building a flow diagram from scratch using WireFlow's Blade components. By the end, you'll have an interactive diagram with nodes, edges, a background grid, controls, and server-side event handling.
php artisan make:livewire FlowEditor
Define your nodes and edges as arrays on the component. Each node needs an id, position, and optionally data for display content. Each edge needs an id, source, and target.
<?php
namespace App\Livewire;
use Livewire\Component;
class FlowEditor extends Component
{
public array $nodes = [
[
'id' => '1',
'position' => ['x' => 0, 'y' => 0],
'data' => ['label' => 'Input'],
],
[
'id' => '2',
'position' => ['x' => 250, 'y' => 120],
'data' => ['label' => 'Process'],
],
[
'id' => '3',
'position' => ['x' => 500, 'y' => 0],
'data' => ['label' => 'Output'],
],
];
public array $edges = [
['id' => 'e1-2', 'source' => '1', 'target' => '2'],
['id' => 'e2-3', 'source' => '2', 'target' => '3'],
];
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.flow-editor');
}
}
Use <x-flow> to render the canvas and <x-slot:node> to define the node template. Inside the slot, node is the Alpine reactive object for each node.
{{-- resources/views/livewire/flow-editor.blade.php --}}
<div>
<x-flow :nodes="$nodes" :edges="$edges" style="height: 400px;">
<x-slot:node>
<x-flow-handle type="target" position="top" />
<span x-text="node.data.label"></span>
<x-flow-handle type="source" position="bottom" />
</x-slot:node>
</x-flow>
</div>
This gives you:
::demo
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 50, y: 50 }, data: { label: 'Node A' } },
{ id: 'b', position: { x: 250, y: 50 }, data: { label: 'Node B' } },
{ id: 'c', position: { x: 150, y: 180 }, data: { label: 'Node C' } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
],
fitViewOnInit: true,
})" class="flow-container" style="height: 250px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
::enddemo
| Element | Purpose |
|---|---|
<x-flow> |
Renders the flow canvas with flowCanvas() Alpine data |
:nodes / :edges |
Pass initial data from the Livewire component |
<x-slot:node> |
Default node template — rendered for every node |
<x-flow-handle> |
Connection handle Blade component |
node.data.label |
Alpine expression — access the node's data in the template |
Enable built-in canvas features by adding props to <x-flow>:
<x-flow
:nodes="$nodes"
:edges="$edges"
background="dots"
:controls="true"
:minimap="true"
:fit-view-on-init="true"
:history="true"
style="height: 500px;"
>
<x-slot:node>
<x-flow-handle type="target" position="top" />
<span x-text="node.data.label"></span>
<x-flow-handle type="source" position="bottom" />
</x-slot:node>
</x-flow>
| Prop | Effect |
|---|---|
background="dots" |
Dot-grid background pattern (also "lines", "cross", "none") |
:controls="true" |
Zoom in/out and fit-view buttons |
:minimap="true" |
Minimap overview in the corner |
:fit-view-on-init="true" |
Automatically fits all nodes in view on load |
:history="true" |
Enables Ctrl+Z / Ctrl+Y undo/redo |
::demo
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 50, y: 50 }, data: { label: 'Input' } },
{ id: 'b', position: { x: 250, y: 50 }, data: { label: 'Process' } },
{ id: 'c', position: { x: 450, y: 50 }, data: { label: 'Output' } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
{ id: 'e2', source: 'b', target: 'c' },
],
background: 'dots',
controls: true,
minimap: true,
fitViewOnInit: true,
})" class="flow-container" style="height: 280px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
::enddemo
React to canvas events on the server by adding event attributes to <x-flow>:
<x-flow
:nodes="$nodes"
:edges="$edges"
[@connect](https://github.com/connect)="onConnect"
[@node-click](https://github.com/node-click)="onNodeClick"
[@node-drag-end](https://github.com/node-drag-end)="onNodeDragEnd"
style="height: 500px;"
>
<x-slot:node>
<x-flow-handle type="target" position="top" />
<span x-text="node.data.label"></span>
<x-flow-handle type="source" position="bottom" />
</x-slot:node>
</x-flow>
Then add the WithWireFlow trait and define handler methods on your component:
use ArtisanFlow\WireFlow\Concerns\WithWireFlow;
class FlowEditor extends Component
{
use WithWireFlow;
public array $nodes = [
['id' => '1', 'position' => ['x' => 0, 'y' => 0], 'data' => ['label' => 'Input']],
['id' => '2', 'position' => ['x' => 250, 'y' => 120], 'data' => ['label' => 'Process']],
['id' => '3', 'position' => ['x' => 500, 'y' => 0], 'data' => ['label' => 'Output']],
];
public array $edges = [
['id' => 'e1-2', 'source' => '1', 'target' => '2'],
['id' => 'e2-3', 'source' => '2', 'target' => '3'],
];
public function onConnect(
string $source,
string $target,
?string $sourceHandle,
?string $targetHandle,
): void {
$this->edges[] = [
'id' => "e-{$source}-{$target}",
'source' => $source,
'target' => $target,
];
}
public function onNodeClick(string $nodeId, array $node): void
{
// Handle node click — e.g., open a detail panel
}
public function onNodeDragEnd(string $nodeId, array $position): void
{
// Persist the new position
}
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.flow-editor');
}
}
The [@event](https://github.com/event) syntax on <x-flow> maps canvas events to your Livewire methods. See Event Handlers for the full list.
By default, nodes and edges are passed as initial data and managed client-side. Enable two-way sync to keep them entangled with your Livewire component:
<x-flow :nodes="$nodes" :edges="$edges" :sync="true">
<x-slot:node>
<x-flow-handle type="target" position="top" />
<span x-text="node.data.label"></span>
<x-flow-handle type="source" position="bottom" />
</x-slot:node>
</x-flow>
With sync enabled, node positions, edge connections, and other changes are automatically entangled with your Livewire component's $nodes and $edges properties via $wire.entangle().
There are three sync modes:
| Mode | Prop | Behavior |
|---|---|---|
| Static | (default) | Nodes/edges passed as initial data. Client-side only. |
| Sync | :sync="true" |
Two-way $wire.entangle(). Changes sync to server. |
| Listen | :listen="true" |
Server pushes updates, client is non-interactive. |
See Core Concepts for more on how sync works.
How can I help you explore Laravel packages today?