A canvas in a LiveView, its graph from your database, and each change saved back.
Install
Follow the README's Install: the dependency, the
hook in the LiveSocket, and the style sheet. Then import the components
in the html_helpers of your web module:
import Hakushi.ComponentsA canvas
defmodule MyAppWeb.FlowLive do
use MyAppWeb, :live_view
alias Hakushi.Graph
def mount(%{"id" => id}, _session, socket) do
flow = Flows.get!(id)
{:ok, assign(socket, flow: flow, graph: flow.graph)}
end
def render(assigns) do
~H"""
<.canvas id="flow" graph={@graph} on_change={&save(&1, &2, @flow)} class="h-[calc(100vh-4rem)]">
<:node type="step" :let={node}>
<div class="rounded-lg border bg-white px-4 py-3 shadow-sm">
<.handle kind={:target} side={:left} />
{node.data["title"]}
<.handle kind={:source} side={:right} />
</div>
</:node>
</.canvas>
"""
end
defp save(_change, %{result: graph}, flow) do
Flows.update_graph(flow, graph)
end
endThe canvas fills its box: give it a height. It opens on all of its
nodes (fit), on dotted paper (background).
The graph in one column
Hakushi.Graph.Type keeps a whole canvas in one column of a table, as a
map (jsonb on PostgreSQL):
schema "flows" do
field :name, :string
field :graph, Hakushi.Graph.Type, default: Hakushi.Graph.new()
end# the migration
add :graph, :map, null: false, default: %{}def update_graph(flow, graph) do
flow |> Ecto.Changeset.change(graph: graph) |> Repo.update()
endon_change returns :ok, {:ok, anything} (as Repo.update/1 does),
{:error, reason} with the words to show, or {:error, changeset},
whose first error is shown. A change that on_change refuses does not
happen: the browser takes it back. See Changes.
A canvas whose nodes are rows of their own (the tables of a schema, the
steps of a workflow with their settings) keeps them in its own tables,
makes its graph of them in mount, and changes the rows in on_change
by the kind of change.
New nodes
People make nodes by dragging an item onto the canvas, by pressing
Enter on it, or by a double click on the paper. The canvas asks
new_node for the node:
<.canvas_item for="flow" item="email">Email</.canvas_item>
<.canvas id="flow" graph={@graph} on_change={...} new_node={&new_step/2}>defp new_step(kind, {x, y}),
do: %{type: "step", x: x, y: y, data: %{"kind" => kind || "note", "title" => "New step"}}A node without an id gets one. new_node returning nil makes none.
Next
- Nodes and Edges: what the canvas draws.
- Collaboration: one canvas for everyone on it.
- Testing: the canvas in
Phoenix.LiveViewTest.