LiveFlow

Interactive node-based flow diagrams for Phoenix LiveView.

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.2.3"}
  ]
end

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:

Option Type Default Description
controls boolean false Show zoom controls (zoom in/out, fit view)
background:dots | :lines | :cross | nilnil Background pattern
minimap boolean false Show minimap overlay
snap_to_grid boolean false Snap node positions to grid
grid_size integer 20 Grid size in pixels
helper_lines boolean false Show alignment guides during drag
cursors boolean false Show remote cursors (for collaboration)
theme string nil Theme name (e.g., "dark", "ocean")
fit_view boolean false Auto-fit all nodes on mount
default_edge_type atom :bezier Default edge path type

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.