LiveView click event and offsetX, offsetY coordinates

LiveView click event and offsetX, offsetY coordinates

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).

liveview click offset coordinates
LiveView click offset coordinates

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.

LiveView rendering svg circle elements
LiveView rendering svg circle elements