Skip to content

Instantly share code, notes, and snippets.

@knewter
Created November 11, 2024 17:49
Show Gist options
  • Save knewter/0d9936ba92def77d1920c9c5214b1855 to your computer and use it in GitHub Desktop.
Save knewter/0d9936ba92def77d1920c9c5214b1855 to your computer and use it in GitHub Desktop.
single-file liveview collaborative drawing grid

Just run this like elixir grid.exs and you have a liveview collaborative grid huzzah

Mix.install([
{:plug_cowboy, "~> 2.5"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7.0"},
{:phoenix_live_view, "~> 0.19.0"}
])
defmodule Example.LayoutView do
use Phoenix.HTML
import Phoenix.Component
def render(assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LiveView Grid</title>
<script defer src="https://unpkg.com/[email protected]/priv/static/phoenix.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/priv/static/phoenix_live_view.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const Socket = window.Phoenix.Socket;
const LiveSocket = window.LiveView.LiveSocket;
const csrfToken = null;
const liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } });
liveSocket.connect();
});
</script>
<style>
.grid-container { display: grid; grid-template-columns: repeat(<%= @grid_size %>, 10px); }
.grid-item { width: 10px; height: 10px; border: 1px solid #ccc; }
</style>
</head>
<body>
<main role="main">
<%= @inner_content %>
</main>
</body>
</html>
"""
end
end
defmodule Example.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule Example.Application do
use Application
def start(_type, _args) do
IO.puts("starting app")
children = [
Example.GridState,
Example.Endpoint
]
opts = [strategy: :one_for_one, name: Example.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule Example.GridState do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def init(_) do
{:ok, %{}}
end
def get_grid() do
GenServer.call(__MODULE__, :get_grid)
end
def toggle_cell(x, y, color) do
GenServer.call(__MODULE__, {:toggle_cell, x, y, color})
end
def handle_call(:get_grid, _from, grid) do
{:reply, {:ok, grid}, grid}
end
def handle_call({:toggle_cell, x, y, color}, _from, grid) do
new_grid = Map.put(grid, {x, y}, color)
broadcast_grid_update(new_grid)
{:reply, color, new_grid}
end
defp broadcast_grid_update(new_grid) do
Phoenix.PubSub.broadcast(Example.PubSub, "grid:update", {:grid_updated, new_grid})
end
end
defmodule Example.GridLive do
use Phoenix.LiveView
alias Example.GridState
@grid_size 100
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Example.PubSub, "grid:update")
end
{:ok, grid} = GridState.get_grid()
{:ok,
socket
|> assign(:grid, grid)
|> assign(:grid_size, @grid_size)
# Default color is black
|> assign(:color, "#000000")}
end
@impl true
def render(assigns) do
~H"""
<div>
<form phx-change="color_changed">
<label for="color-picker">Choose Color:</label>
<input type="color" id="color-picker" name="color-picker" value={@color} />
</form>
</div>
<div class="grid-container">
<%= for y <- 0..(@grid_size - 1) do %>
<%= for x <- 0..(@grid_size - 1) do %>
<div class="grid-item"
phx-click="toggle_cell"
phx-value-x={x}
phx-value-y={y}
style={"background-color: #{Map.get(assigns.grid, {x, y}, "#FFFFFF")}"}></div>
<% end %>
<% end %>
</div>
"""
end
@impl true
def handle_event("color_changed", %{"color-picker" => color}, socket) do
{:noreply, assign(socket, :color, color)}
end
@impl true
def handle_event("toggle_cell", %{"x" => x, "y" => y}, socket) do
x = String.to_integer(x)
y = String.to_integer(y)
color = socket.assigns.color
GridState.toggle_cell(x, y, color)
{:noreply, socket}
end
@impl true
def handle_info({:grid_updated, new_grid}, socket) do
{:noreply, assign(socket, grid: new_grid)}
end
end
defmodule Example.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug(:accepts, ["html"])
plug(:put_root_layout, {Example.LayoutView, :render})
end
scope "/", Example do
pipe_through(:browser)
live("/", GridLive, :index)
end
end
defmodule Example.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug(Example.Router)
end
Application.put_env(:sample, Example.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64),
pubsub_server: Example.PubSub
)
{:ok, _} =
Supervisor.start_link(
[
{Phoenix.PubSub, name: Example.PubSub},
Example.Endpoint,
Example.GridState
],
strategy: :one_for_one
)
Process.sleep(:infinity)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment