Current section
Files
Jump to
Current section
Files
ex_flow_graph
INSTALLATION.md
INSTALLATION.md
# ExFlowGraph Installation & Setup Guide
Complete guide to installing and using ExFlowGraph in your Phoenix LiveView application.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Installation](#installation)
3. [Configuration](#configuration)
4. [Basic Setup](#basic-setup)
5. [Usage Examples](#usage-examples)
6. [Advanced Features](#advanced-features)
7. [Troubleshooting](#troubleshooting)
---
## Prerequisites
- **Elixir**: >= 1.16
- **Phoenix**: >= 1.8
- **Phoenix LiveView**: >= 1.0
- **Tailwind CSS**: v4.x (for styling)
- **DaisyUI**: (optional, for enhanced UI)
---
## Installation
### Step 1: Add Dependency
Add ExFlowGraph to your `mix.exs` dependencies:
```elixir
# mix.exs
defp deps do
[
{:ex_flow_graph, path: "../ExFlowGraph"}, # For local development
# OR for production (when published to Hex):
# {:ex_flow_graph, "~> 0.1.0"},
# Required dependencies (if not already present)
{:phoenix_live_view, "~> 1.0"},
{:libgraph, "~> 0.16"},
{:jason, "~> 1.2"}
]
end
```
### Step 2: Install Dependencies
```bash
mix deps.get
mix compile
```
---
## Configuration
### Step 1: Import Components
In your `lib/your_app_web.ex` file, import the ExFlowGraph components:
```elixir
# lib/your_app_web.ex
defmodule YourAppWeb do
# ... existing code ...
defp html_helpers do
quote do
use Gettext, backend: YourAppWeb.Gettext
import Phoenix.HTML
import YourAppWeb.CoreComponents
# Import ExFlowGraph components
import ExFlowGraphWeb.ExFlow.Canvas
import ExFlowGraphWeb.ExFlow.Node
import ExFlowGraphWeb.ExFlow.Edge
alias Phoenix.LiveView.JS
unquote(verified_routes())
end
end
end
```
### Step 2: Configure JavaScript Hook
Add the ExFlowCanvas hook to your `assets/js/app.js`:
```javascript
// assets/js/app.js
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
// Import ExFlowCanvas hook
import ExFlowCanvas from "../../deps/ex_flow_graph/assets/js/hooks/ex_flow"
// OR for local path dependency:
// import ExFlowCanvas from "../../../ExFlowGraph/assets/js/hooks/ex_flow"
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: {ExFlowCanvas} // Register the hook
})
// ... rest of your app.js
```
### Step 3: Configure Tailwind CSS
Update your `assets/css/app.css` to include ExFlowGraph styles:
```css
/* assets/css/app.css */
@import "tailwindcss";
@source "../css";
@source "../js";
@source "../../lib/your_app_web";
/* Add ExFlowGraph component source */
@source "../../deps/ex_flow_graph/lib/ex_flow_graph_web";
/* OR for local path dependency: */
/* @source "../../../ExFlowGraph/lib/ex_flow_graph_web"; */
/* ... rest of your CSS config ... */
```
**Important for Tailwind v4**: Create a safelist file to ensure all component classes are generated:
```elixir
# lib/your_app_web/components/exflow_safelist.ex
defmodule YourAppWeb.Components.ExFlowSafelist do
use Phoenix.Component
def safelist(assigns) do
~H"""
<div class="hidden">
<!-- Canvas classes -->
<div class="relative w-full overflow-hidden rounded-2xl border border-base-300 bg-gradient-to-br from-base-200/40 to-base-100"></div>
<div class="exflow-container absolute inset-0"></div>
<!-- Node classes -->
<div class="exflow-node absolute select-none rounded-lg border bg-base-100/90 shadow-sm backdrop-blur px-3 py-2 text-sm text-base-content cursor-grab active:cursor-grabbing pointer-events-auto transition-all border-primary border-2 ring-2 ring-primary/30 shadow-lg border-base-300 font-medium mt-2 flex gap-2"></div>
<span class="exflow-handle exflow-handle-source inline-flex size-2 rounded-full bg-primary cursor-crosshair hover:scale-150 transition-transform"></span>
<span class="exflow-handle exflow-handle-target inline-flex size-2 rounded-full bg-base-content/40 cursor-crosshair hover:scale-150 transition-transform"></span>
<!-- Edge classes -->
<svg><path class="exflow-edge stroke-base-content/50 hover:stroke-base-content/80 transition-colors stroke-primary stroke-2 stroke-4"></path></svg>
</div>
"""
end
end
```
### Step 4: Rebuild Assets
```bash
mix assets.deploy
```
---
## Basic Setup
### Step 1: Create a LiveView Module
```elixir
# lib/your_app_web/live/flow_live.ex
defmodule YourAppWeb.FlowLive do
use YourAppWeb, :live_view
alias ExFlow.Core.Graph, as: FlowGraph
alias ExFlow.Storage.InMemory
@storage_id "my_workflow"
@impl true
def mount(_params, _session, socket) do
# Load existing graph or create new one
graph = case InMemory.load(@storage_id) do
{:ok, graph} -> graph
{:error, _} -> create_initial_graph()
end
socket =
socket
|> assign(:graph, graph)
|> assign(:selected_node_ids, MapSet.new())
|> assign(:selected_edge_ids, MapSet.new())
{:ok, socket}
end
@impl true
def render(assigns) do
nodes = nodes_for_ui(assigns.graph)
edges = edges_for_ui(assigns.graph, nodes)
assigns = assign(assigns, nodes: nodes, edges: edges)
~H"""
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">My Workflow</h1>
<div class="rounded-2xl border border-base-300 bg-base-100 shadow-lg overflow-hidden">
<ExFlowGraphWeb.ExFlow.Canvas.canvas
id="flow-canvas"
nodes={@nodes}
edges={@edges}
selected_node_ids={@selected_node_ids}
selected_edge_ids={@selected_edge_ids}
/>
</div>
</div>
"""
end
# Event handlers
@impl true
def handle_event("update_position", %{"id" => id, "x" => x, "y" => y}, socket) do
case FlowGraph.update_node_position(socket.assigns.graph, id, %{x: x, y: y}) do
{:ok, graph} ->
:ok = InMemory.save(@storage_id, graph)
{:noreply, assign(socket, :graph, graph)}
{:error, _} ->
{:noreply, socket}
end
end
@impl true
def handle_event("create_edge", params, socket) do
%{
"source_id" => source_id,
"source_handle" => source_handle,
"target_id" => target_id,
"target_handle" => target_handle
} = params
edge_id = "edge-#{System.unique_integer([:positive])}"
case FlowGraph.add_edge(
socket.assigns.graph,
edge_id,
source_id,
source_handle,
target_id,
target_handle
) do
{:ok, graph} ->
:ok = InMemory.save(@storage_id, graph)
{:noreply, assign(socket, :graph, graph)}
{:error, _} ->
{:noreply, socket}
end
end
@impl true
def handle_event("select_node", %{"id" => id, "multi" => multi}, socket) do
selected =
if multi == "true" do
if MapSet.member?(socket.assigns.selected_node_ids, id) do
MapSet.delete(socket.assigns.selected_node_ids, id)
else
MapSet.put(socket.assigns.selected_node_ids, id)
end
else
MapSet.new([id])
end
{:noreply, assign(socket, :selected_node_ids, selected)}
end
@impl true
def handle_event("select_edge", %{"id" => id, "multi" => multi}, socket) do
selected =
if multi == "true" do
if MapSet.member?(socket.assigns.selected_edge_ids, id) do
MapSet.delete(socket.assigns.selected_edge_ids, id)
else
MapSet.put(socket.assigns.selected_edge_ids, id)
end
else
MapSet.new([id])
end
{:noreply, assign(socket, :selected_edge_ids, selected)}
end
# Helper functions
defp create_initial_graph do
graph = FlowGraph.new()
{:ok, graph} =
FlowGraph.add_node(graph, "node-1", :task, %{
position: %{x: 100, y: 100},
label: "Start",
metadata: %{}
})
{:ok, graph} =
FlowGraph.add_node(graph, "node-2", :task, %{
position: %{x: 400, y: 100},
label: "Process",
metadata: %{}
})
{:ok, graph} =
FlowGraph.add_edge(graph, "edge-1", "node-1", "out", "node-2", "in")
:ok = InMemory.save(@storage_id, graph)
graph
end
defp nodes_for_ui(graph) do
for node <- FlowGraph.get_nodes(graph) do
%{
id: node.id,
title: node.label || node.id,
label: node.label,
metadata: node.metadata,
x: node.position.x,
y: node.position.y
}
end
end
defp edges_for_ui(graph, nodes) do
node_by_id = Map.new(nodes, fn n -> {n.id, n} end)
graph
|> FlowGraph.get_edges()
|> Enum.map(fn edge ->
source = Map.get(node_by_id, edge.source)
target = Map.get(node_by_id, edge.target)
if source && target do
%{
id: edge.id,
source_id: edge.source,
target_id: edge.target,
source_x: source.x + 12,
source_y: source.y + 12,
target_x: target.x + 12,
target_y: target.y + 12
}
else
nil
end
end)
|> Enum.reject(&is_nil/1)
end
end
```
### Step 2: Add Route
```elixir
# lib/your_app_web/router.ex
scope "/", YourAppWeb do
pipe_through :browser
live "/flow", FlowLive
end
```
### Step 3: Run Your Application
```bash
mix phx.server
```
Visit `http://localhost:4000/flow` to see your graph!
---
## Usage Examples
### Creating Nodes
```elixir
# Add a node with label and metadata
{:ok, graph} = FlowGraph.add_node(graph, "my-node", :agent, %{
position: %{x: 200, y: 150},
label: "ML Processor",
metadata: %{
model: "gpt-4",
temperature: 0.7
}
})
```
### Creating Edges
```elixir
# Connect two nodes
{:ok, graph} = FlowGraph.add_edge(
graph,
"my-edge",
"source-node-id",
"out",
"target-node-id",
"in",
%{
label: "data flow",
metadata: %{bandwidth: "high"}
}
)
```
### Updating Nodes
```elixir
# Update node position
{:ok, graph} = FlowGraph.update_node_position(
graph,
"my-node",
%{x: 250, y: 200}
)
# Update node label and metadata
{:ok, graph} = FlowGraph.update_node(
graph,
"my-node",
%{
label: "Updated Label",
metadata: %{new_field: "value"}
}
)
```
### Deleting Elements
```elixir
# Delete a node
{:ok, graph} = FlowGraph.delete_node(graph, "my-node")
# Delete an edge
{:ok, graph} = FlowGraph.delete_edge(graph, "my-edge")
```
---
## Advanced Features
### 1. Undo/Redo with History Manager
```elixir
alias ExFlow.HistoryManager
alias ExFlow.Commands.CreateNodeCommand
# Initialize history
history = HistoryManager.new()
# Execute command with history
command = CreateNodeCommand.new("node-1", :task, %{position: %{x: 100, y: 100}})
{:ok, history, graph} = HistoryManager.execute(history, command, graph)
# Undo
case HistoryManager.undo(history, graph) do
{:ok, history, graph} ->
# Node removed
{:error, :nothing_to_undo} ->
# At beginning of history
end
# Redo
case HistoryManager.redo(history, graph) do
{:ok, history, graph} ->
# Node restored
{:error, :nothing_to_redo} ->
# At end of history
end
```
### 2. Custom Option+Click Actions
Handle Option/Alt + Click events on nodes and edges:
```elixir
@impl true
def handle_event("option_click_node", %{"id" => id}, socket) do
node = FlowGraph.get_nodes(socket.assigns.graph)
|> Enum.find(&(&1.id == id))
socket =
socket
|> assign(:show_details_modal, true)
|> assign(:selected_node, node)
{:noreply, socket}
end
@impl true
def handle_event("option_click_edge", %{"id" => id}, socket) do
edge = FlowGraph.get_edges(socket.assigns.graph)
|> Enum.find(&(&1.id == id))
# Your custom logic here
{:noreply, socket}
end
```
### 3. Keyboard Shortcuts
The JavaScript hook automatically handles:
- **Drag node**: Click and drag
- **Multi-select**: Shift/Ctrl/Cmd + Click
- **Create edge**: Drag from source handle to target handle
- **Pan canvas**: Drag on empty canvas
- **Custom action**: Option/Alt + Click
### 4. Persistent Storage
#### In-Memory Storage (Development)
```elixir
alias ExFlow.Storage.InMemory
# Save
:ok = InMemory.save("my-workflow", graph)
# Load
{:ok, graph} = InMemory.load("my-workflow")
# List all
graphs = InMemory.list()
# Delete
:ok = InMemory.delete("my-workflow")
```
#### Database Storage (Production)
For production, implement your own storage module:
```elixir
defmodule MyApp.FlowStorage do
@behaviour ExFlow.Storage
def save(id, graph) do
# Serialize graph and save to database
MyApp.Repo.insert_or_update(%Workflow{
id: id,
graph_data: :erlang.term_to_binary(graph)
})
end
def load(id) do
case MyApp.Repo.get(Workflow, id) do
nil -> {:error, :not_found}
workflow ->
graph = :erlang.binary_to_term(workflow.graph_data)
{:ok, graph}
end
end
def delete(id) do
MyApp.Repo.delete(%Workflow{id: id})
end
def list() do
MyApp.Repo.all(Workflow)
|> Enum.map(& &1.id)
end
end
```
### 5. Custom Styling
Override default styles in your CSS:
```css
/* Make nodes larger */
.exflow-node {
padding: 1rem;
font-size: 1rem;
}
/* Custom edge colors */
.exflow-edge {
stroke: #3b82f6 !important;
stroke-width: 3;
}
/* Custom selection style */
.exflow-node.selected {
border: 3px solid #10b981;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
}
```
---
## Troubleshooting
### Issue: Graph Not Displaying
**Problem**: Canvas shows but nodes/edges are invisible.
**Solution**: Check if Tailwind is scanning library files:
1. Verify `@source` directive in `assets/css/app.css`
2. Add safelist file (see Configuration Step 3)
3. Rebuild assets: `mix assets.deploy`
### Issue: JavaScript Hook Not Working
**Problem**: Can't drag nodes or create edges.
**Solution**:
1. Verify hook import in `app.js`
2. Check hook registration: `hooks: {ExFlowCanvas}`
3. Ensure `phx-hook="ExFlowCanvas"` attribute on canvas div
4. Check browser console for JavaScript errors
### Issue: Edges Not Clickable
**Problem**: Can't select or Option+Click edges.
**Solution**:
1. Ensure SVG doesn't have `pointer-events-none` class
2. Edge component should have invisible hit area (20px stroke)
3. Check z-index layering (edges at z-0, nodes at z-10)
### Issue: Canvas Has Zero Height
**Problem**: Canvas is invisible or very small.
**Solution**:
Add inline style to canvas:
```heex
<ExFlowGraphWeb.ExFlow.Canvas.canvas
style="height: 70vh;"
...
/>
```
### Issue: Module Not Found Errors
**Problem**: Can't find ExFlowGraph modules.
**Solution**:
1. Run `mix deps.get`
2. Run `mix compile`
3. Verify dependency in `mix.exs`
4. Check import statements in `your_app_web.ex`
---
## Demo Application Reference
The complete demo application at `/demo` provides:
- Full-featured workflow editor
- Undo/Redo functionality
- Save/Load graphs
- Node and edge editing modals
- Selection management
- Keyboard shortcuts
- Test graph generation
**Key Demo Files to Reference**:
1. **`demo/lib/demo_web/live/home_live.ex`** - Complete LiveView implementation
2. **`demo/assets/js/app.js`** - JavaScript hook setup
3. **`demo/assets/css/app.css`** - CSS configuration
4. **`demo/lib/demo_web.ex`** - Component imports
---
## Next Steps
1. ✅ Install ExFlowGraph in your project
2. ✅ Configure Tailwind and JavaScript
3. ✅ Create your first LiveView with a graph
4. 📚 Read `LABELS_AND_METADATA.md` for data handling
5. 📚 Read `OPTION_CLICK.md` for custom interactions
6. 🚀 Build your workflow application!
---
## Support
- **Issues**: Report bugs at the repository
- **Examples**: See the `/demo` application
- **Documentation**: Check `.md` files in the project root
## License
MIT License - See LICENSE file for details.