Packages

Canvas for Phoenix: nodes of your own HEEx on an endless sheet, joined by edges, with every change checked on the server and live collaboration.

Current section

Files

Jump to
hakushi guides quickstart.md
Raw

guides/quickstart.md

# Quickstart

A canvas in a LiveView, its graph from your database, and each change
saved back.

## Install

Follow the README's [Install](../README.md#install): the dependency, the
hook in the `LiveSocket`, and the style sheet. Then import the components
in the `html_helpers` of your web module:

```elixir
import Hakushi.Components
```

## A canvas

```elixir
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
end
```

The 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):

```elixir
schema "flows" do
  field :name, :string
  field :graph, Hakushi.Graph.Type, default: Hakushi.Graph.new()
end
```

```elixir
# the migration
add :graph, :map, null: false, default: %{}
```

```elixir
def update_graph(flow, graph) do
  flow |> Ecto.Changeset.change(graph: graph) |> Repo.update()
end
```

`on_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](changes.md).

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:

```heex
<.canvas_item for="flow" item="email">Email</.canvas_item>

<.canvas id="flow" graph={@graph} on_change={...} new_node={&new_step/2}>
```

```elixir
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](nodes.md) and [Edges](edges.md): what the canvas draws.
* [Collaboration](collaboration.md): one canvas for everyone on it.
* [Testing](testing.md): the canvas in `Phoenix.LiveViewTest`.