7 GUIs: Implementing a Counter in LiveView

My co-worker introduced me to the 7 GUIs: a series of seven challenges for building UIs.

Here’s a description from the website:

The tasks were selected by the following criteria. The task set should be as small as possible yet reflect as many typical (or fundamental or representative) challenges in GUI programming as possible. Each task should be as simple and self-contained as possible yet not too artificial. Preferably, a task should be based on existing examples as that gives the task more justification to be useful and there already will be at least one reference implementation.

Though LiveView isn’t a GUI library, I wanted to see how it would handle those typical GUI challenges. So I started working on them.

Here are some highlights for the first task: the Counter.

The Counter

A counter in LiveView. Increments count when clicking "Count" button.

The point of the first exercise is to understand the basic ideas of the language or toolkit being used. With LiveView, these are the basics.

Mount

We mount a live view. The function gets called twice: once for static rendering (the initial HTTP request) and then once when the connection is upgraded to a websocket.

mount is the place to assign data needed for rendering the page. In this example, we only set the :count assign to its initial value 0.

  def mount(_, _, socket) do
    {:ok, assign(socket, :count, 0)}
  end

Render

Every LiveView needs to render a template. I chose to do it in line with the ~L sigil, but we could also have rendered it in a colocated .leex template or used the Phoenix.View.render/3 function.

  def render(assigns) do
    ~L"""
    <h1>Counter</h1>

    <label id="counter"><%= @count %></label>
    <button phx-click="count" id="count">Count</button>
    """
  end

At the bottom of that template, you can also see how we use phx-click to bind a click to the "count" event — now whenever we click that button, our LiveView process receives a message with the "count" event.

Handling events

Finally, we handle the "count" event by increasing the :count assign in memory. LiveView will then re-render the screen with the updated count.

  def handle_event("count", _, socket) do
    socket
    |> update(:count, fn count -> count + 1 end)
    |> no_reply()
  end

  defp no_reply(socket), do: {:noreply, socket}

Tests

I also have tests to validate the behavior. Here you can see two representative tests:

  • testing rendering (disconnected and connected states), and
  • testing interactions with the page.
# testing rendering in disconnected and connected states

  test "renders Counter title", %{conn: conn} do
    {:ok, view, html} = live(conn, "/counter")

    assert html =~ "Counter"
    assert render(view) =~ "Counter"
  end
# testing increasing the count by 1

  test "clicking on Count button increases the counter", %{conn: conn} do
    {:ok, view, _html} = live(conn, "/counter")

    view
    |> element("#count")
    |> render_click()

    assert has_element?(view, "#counter", "1")
  end

Sources

You can find the repo with my examples and the commit for the Counter.

The LiveView docs are great. Give them a read.

And you can find full descriptions of the tasks on the 7 GUIs website.