Phoenix LiveView plays fantastically well with SVG elements, making really easy to render dynamic images, without writing any JavaScript code.
There are different bindings we can use to register and react to user interaction, one of these is phx-click
. By adding phx-click="clicked"
attribute to an element in our LiveView, when user clicks the element, a "clicked"
event is sent to the LiveView process.
def render(assigns) do
~L"""
<button phx-click="clicked">click me</button>
"""
end
def handle_event("clicked", event, socket) do
...
end
We can use the phx-click
binding on all kind of DOM elements, even <svg>
!
Let’s consider a large empty <svg>
element, where we want to draw user’s clicks as SVG <circle>
elements. When handling "clicked"
events, we need the coordinates of these clicks.
I made a tiny tiny contribution to the LiveView project (just two lines of code available from release 0.5.0), which adds offsetX
and offsetY
coordinates to the click event metadata. These coordinates are useful to know exactly where the click happened inside the element.
def render(assigns) do
~L"""
<svg phx-click="clicked" width="500" height="500" style="border: 1px solid blue">
</svg>
"""
end
def handle_event("clicked", %{"offsetX" => x, "offsetY" => y} = _event, socket) do
...
end
When clicking on a large <svg>
element, each click is sent to the LiveView process, along with event
metadata. offsetX
and offsetY
are the coordinates of the click, relative to the svg origin (top-left corner).
With these coordinates it becomes really easy to implement drawing functionalities using only SVG and LiveView. A simple example is to render clicks as <circle>
elements.
defmodule DemoWeb.SVGLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, :points, [])}
end
def render(assigns) do
~L"""
<svg phx-click="clicked" width="500" height="500" style="border: 1px solid blue">
<%= for {x, y} <- @points do %>
<circle cx="<%= x %>" cy="<%= y %>" r="3" fill="purple" />
<% end %>
</svg>
"""
end
def handle_event("clicked", %{"offsetX" => x, "offsetY" => y} = _event, socket) do
socket = update(socket, :points, fn points -> [{x, y} | points] end)
{:noreply, socket}
end
end
In mount/3
we initialize an empty points
list where we are going to save each click pair of coordinates as {x, y}
tuple.
In render/1
, we render an <svg>
element with a phx-click
binding. When we click on any part of this svg, a clicked
event is sent to LiveView and a {x, y}
tuple is prepended to the points
list. LiveView re-renders the view, adding the new <circle>
element for the added point.