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