LiveFlow

Interactive node-based flow diagrams for Phoenix LiveView.

This is an independently maintained fork of rocket4ce/live_flow. It is not affiliated with or sponsored by the original project.

Build visual node editors, workflow builders, and interactive diagrams — similar to React Flow, but for Phoenix LiveView.

Features

Installation

Add live_flow to your dependencies in mix.exs:

def deps do
[
{:live_flow, "~> 0.4", hex: :ex_live_flow}
]
end

ex_live_flow and the original live_flow Hex package are mutually exclusive. Both provide the :live_flow OTP application and the LiveFlow.* modules, so only one may be installed.

JavaScript Setup

In your assets/js/app.js, import and register the hook:

import { LiveFlowHook, FileImportHook, setupDownloadHandler } from "live_flow"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
LiveFlow: LiveFlowHook,
FileImport: FileImportHook // optional
}
})
// Optional: enable JSON file download
setupDownloadHandler()

CSS Setup

Import the LiveFlow stylesheet in your assets/css/app.css:

@import "../../deps/live_flow/assets/css/live_flow.css";

Theme Setup (Optional)

To use the built-in themes, add the Tailwind v4 plugin:

@plugin "../../deps/live_flow/assets/js/live_flow/liveflow-theme" {
name: "light";
default: true;
}
@plugin "../../deps/live_flow/assets/js/live_flow/liveflow-theme" {
name: "dark";
prefersdark: true;
}

Quick Start

defmodule MyAppWeb.FlowLive do
use MyAppWeb, :live_view
alias LiveFlow.{State, Node, Edge}
def mount(_params, _session, socket) do
flow = State.new(
nodes: [
Node.new("1", %{x: 100, y: 100}, %{label: "Start"}),
Node.new("2", %{x: 300, y: 200}, %{label: "Process"}),
Node.new("3", %{x: 500, y: 100}, %{label: "End"})
],
edges: [
Edge.new("e1", "1", "2"),
Edge.new("e2", "2", "3")
]
)
{:ok, assign(socket, flow: flow)}
end
def render(assigns) do
~H"""
<.live_component
module={LiveFlow.Components.Flow}
id="my-flow"
flow={@flow}
opts={%{controls: true, background: :dots}}
/>
"""
end
# Handle node position changes
def handle_event("lf:node_change", params, socket) do
flow = LiveFlow.Changes.NodeChange.apply(socket.assigns.flow, params)
{:noreply, assign(socket, flow: flow)}
end
# Handle edge changes (add/remove)
def handle_event("lf:edge_change", params, socket) do
flow = LiveFlow.Changes.EdgeChange.apply(socket.assigns.flow, params)
{:noreply, assign(socket, flow: flow)}
end
# Handle new connections
def handle_event("lf:connect_end", params, socket) do
case LiveFlow.Validation.Connection.validate_and_create(
socket.assigns.flow, params
) do
{:ok, flow} -> {:noreply, assign(socket, flow: flow)}
{:error, _reason} -> {:noreply, socket}
end
end
# Handle viewport changes (pan/zoom)
def handle_event("lf:viewport_change", params, socket) do
viewport = LiveFlow.Viewport.from_params(params)
flow = LiveFlow.State.update_viewport(socket.assigns.flow, viewport)
{:noreply, assign(socket, flow: flow)}
end
# Handle selection changes
def handle_event("lf:selection_change", params, socket) do
flow = LiveFlow.State.update_selection(socket.assigns.flow, params)
{:noreply, assign(socket, flow: flow)}
end
# Handle delete selected
def handle_event("lf:delete_selected", _params, socket) do
flow = LiveFlow.State.delete_selected(socket.assigns.flow)
{:noreply, assign(socket, flow: flow)}
end
end

Flow Options

The opts map supports the following options:

OptionTypeDefaultDescription
pan_on_dragbooleantruePan by dragging the canvas
pan_on_scrollbooleanfalsePan freely with unmodified wheel or trackpad input
zoom_on_scrollbooleantrueZoom with unmodified wheel input when pan_on_scroll is disabled
zoom_on_pinchboolean | nilnilEnable trackpad pinch, Ctrl/Cmd+wheel zoom, and touchscreen pinch; nil inherits zoom_on_scroll
min_zoomnumber0.1Minimum zoom level
max_zoomnumber4.0Maximum zoom level
controlsbooleanfalseShow zoom controls (zoom in/out, fit view)
background:dots | :lines | :cross | nilnilBackground pattern
minimapbooleanfalseShow minimap overlay
snap_to_gridbooleanfalseSnap node positions to grid
snap_grid{number, number}{15, 15}Horizontal and vertical grid size in pixels
helper_linesbooleanfalseShow alignment guides during drag
cursorsbooleanfalseShow remote cursors (for collaboration)
themestringnilTheme name (e.g., "dark", "ocean")
fit_view_on_initbooleanfalseAuto-fit all nodes on mount

Modifier-wheel zoom takes precedence over unmodified wheel behavior. Safari's native gesture events and Chromium/Firefox Ctrl+wheel pinch streams use zoom_on_pinch; unmodified wheel input uses pan_on_scroll first, then falls back to zoom_on_scroll for legacy behavior.

Custom Node Types

You can define custom node types using function components:

defp my_custom_node(assigns) do
~H"""
<div class="bg-white rounded-lg shadow-lg p-4 border-2 border-blue-500">
<LiveFlow.Components.Handle.handle type={:target} position={:top} />
<div class="font-bold"><%= @node.data[:label] %></div>
<div class="text-sm text-gray-500"><%= @node.data[:description] %></div>
<LiveFlow.Components.Handle.handle type={:source} position={:bottom} />
</div>
"""
end

Pass custom node types to the Flow component:

<.live_component
module={LiveFlow.Components.Flow}
id="my-flow"
flow={@flow}
node_types={%{custom: &my_custom_node/1}}
/>

Collaboration

Enable real-time collaboration with PubSub:

def mount(_params, _session, socket) do
socket =
socket
|> assign(flow: initial_flow())
|> LiveFlow.Collaboration.join("flow:room-1",
pubsub: MyApp.PubSub,
presence: MyAppWeb.Presence # optional
)
{:ok, socket}
end
# Add to your LiveView:
def handle_info(msg, socket) do
LiveFlow.Collaboration.handle_info(msg, socket)
end

Documentation

Full documentation is available at HexDocs.

License

MIT License. See LICENSE.md for details.