In this article I share how I was able to create a Baby Age Calculator using Phoenix/Elixir LiveView.
If you just want to know how old your child is, jump over to babyweeks.herokuapp.com. If you want to download the source code, goto github.com/monkseal/babyweeks-phx-liveview
LiveView - Phoenix/Elixir’s Javascript killer?
As described here and here — “Phoenix LiveView is an exciting new library which enables rich, real-time user experiences with server-rendered HTML. LiveView powered applications are stateful on the server with bidrectional communication via WebSockets, offering a vastly simplified programming model compared to JavaScript alternatives.”
If you don’t want to write Javascript to have a real-time web application, Phoenix LiveView provides an interesting alternative with server-rendered HTML.
Being one to avoid bold pronouments, I won’t commit myself to say that LiveView will deal a death blow to javascript in your app. For one, you may already have a lot Javascript in your app. Maybe some if you even like. However, if you are interested in managing state on the server, LiveView is quite compelling.
Project Setup
I don’t want to write a tutorial on setting up your enviroment, you can find a good intro at elixirschool’s Walk-Through of Phoenix LiveView and the phoenix_live_view README. After following those project seetup examples, I ended up with the following files/that we different than a regular elixir setup:
lib/babyweeks_web/live/babyweeks_live_view.ex
- Our LIVE view implemenation.
lib/babyweeks_web/templates/babyweeks/index.html.leex
- Our live view template. The
.leex
extension needs some custom configuration in your Phoenix app to work, see guides above .
- Our live view template. The
lib/babyweeks_web/templates/page/index.html.eex
- The template calls our live view.
lib/babyweeks/calculator.ex
- This is the meat of application, where all the functional and state transition logic resides.
Let’s take a look at this one by one.
Live view template
Our live view template references two variables. The @error
variable contains an error message (or nil). This will render if there is input error. The @calc
contains a struct defined in our Babyweeks.Calculator
. We have 3 different events we are interacting with up
, down
and compute
. The up
/down
was added to make the interface a little easier to use on mobile.
The remaining template looks like a regular view.
<div>
<div>
This app tells you how old your baby is. Enter the birthday and click 'Compute'
</div>
<form action="#" phx-submit="compute">
<%= if @error do %>
<h3 class="alert-danger"><%= %></h3>
<% end %>
<table>
<tbody>
<tr>
<th>Month</th>
<th>Day</th>
<th>Year</th>
</tr>
<tr class="calendar-row">
<td>
<button phx-click="up" phx-value="months"><span>↑</span></button>
<%= number_input :bday, :m, size: '5', value: @calc.bday.month %>
<button phx-click="down" phx-value="months"><span>↓</span></button>
</td>
<td>
<button phx-click="up" phx-value="days"><span>↑</span></button>
<%= number_input :bday, :d, size: '5', value: @calc.bday.day %>
<button phx-click="down" phx-value="days"><span>↓</span></button>
</td>
<td>
<button phx-click="up" phx-value="years"><span>↑</span></button>
<%= number_input :bday, :y, size: '5', value: @calc.bday.year %>
<button phx-click="down" phx-value="years"><span>↓</span></button>
</td>
</tr>
</tbody>
</table>
<%= submit "Compute", phx_disable_with: "Adding..." %>
<%= if !@error do %>
<h3><%= @calc.weeks %> weeks, <%= @calc.days %> days old</h3>
<table>
<tbody>
<tr><td>Today is:</td><td><%= Timex.format!(@calc.today, "%B %d, %Y", :strftime) %></td></tr>
<tr><td>Age in days:</td><td><%= @calc.age_in_days %></td></tr>
<tr><td>Age in months:</td><td><%= @calc.age_in_months %></td></tr>
<tr><td>Age in years:</td><td><%= @calc.age_in_years %></td></tr>
</tbody>
</table>
<% end %>
</form>
</div>
The baby weeks Liveview
My first day playing with Liveview resulted in a massive LiveView file with different functions all over the place and business logic scattered throuhgout the different handle_event
functions. This became a smell to me. I created a rule for myself that only display and update logic would be present in my BabyweeksWeb.BabyweeksLiveView
. This approach seems to work well. As you can see, our handle_event methods are small and easy to reason about.
defmodule BabyweeksWeb.BabyweeksLiveView do
use Phoenix.LiveView
alias Babyweeks.Calculator
alias BabyweeksWeb.BabyweeksView
def render(assigns) do
BabyweeksView.render("index.html", assigns)
end
def mount(_session, socket) do
calc = Calculator.default_bday_to_today(%Calculator{})
{:ok, assign(socket, calc: calc, error: nil)}
end
def handle_event("compute", %{"bday" => bday}, %{assigns: assigns} = socket) do
case Calculator.from_params(assigns.calc, bday) do
{:ok, updated_calc} ->
{:noreply,
assign(socket, error: nil, calc: Calculator.compute_weeks_and_days(updated_calc))
}
{:error, message} ->
{:noreply,
assign(socket, error: message)}
end
end
def handle_event("up", value, socket) do
interval = String.to_existing_atom(value)
{:noreply,
update(socket, :calc, fn calc ->
calc |> Calculator.handle_up(interval) |> Calculator.compute_weeks_and_days()
end)}
end
def handle_event("down", value, socket) do
interval = String.to_existing_atom(value)
{:noreply,
update(socket, :calc, fn calc ->
calc |> Calculator.handle_down(interval) |> Calculator.compute_weeks_and_days()
end)}
end
end
The baby weeks Calculator.
If you come from the React/Redux school of UI state management (as I clearly have), our Babyweeks.Calculator
module acts like our action reducers. It takes one version of a Babyweeks.Calculator
struct and spits out a new one.
Our Calculator struct take a simple flat shape with today and bday being the promenient values. A design goal is that our above BabyweeksWeb.BabyweeksLiveView
does not need to know about the shape of this struct. However, our template does as it is displaying the values.
defmodule Babyweeks.Calculator do
defstruct bday: nil,
age_in_weeks: 0,
age_in_weeks_additional_days: 0,
age_in_months: 0,
age_in_years: 0,
age_in_days: 0,
today: nil
# ...
end
Our important state transtion function is Calculator.compute_weeks_and_days
which returns a new calculator struct with computed values for the above age_in_x
fields.
In pure functional fashion, we can use composition and chain our function calls together. This is where Elixir shines:
def compute_weeks_and_days(calc, end_date \\ today()) do
calc
|> compute_weeks(end_date, calc.bday)
|> compute_additional_days(end_date, calc.bday)
|> compute_age_in_days(end_date, calc.bday)
|> compute_age_in_months(end_date, calc.bday)
|> compute_age_in_years(end_date, calc.bday)
end
The smaller function make use of Elixir Timex library for data calculation, here’s one example:
def compute_weeks(calc, end_date, start_date) do
%{calc | age_in_weeks: Timex.diff(end_date, start_date, :weeks)}
end
We also have functions to handle the up and down buttons over the month/day/year input values:
def handle_up(calc, interval \\ :days) do
options = [{interval, 1}]
shift_bday(calc, options)
end
def handle_down(calc, interval \\ :days) do
options = [{interval, -1}]
shift_bday(calc, options)
end
def shift_bday(calc, shift_options) do
shifted = Timex.shift(calc.bday, shift_options)
%{calc | bday: shifted}
end
Closing thoughts
If you are designing a LiveView application, model the application state transition separate from the liveview mechanics. This will save you a lot of trouble and allow you to think about
I’m very excited about liveview and looking forward to using in future applications. In particular, even though Liveview has been dubbed a replacement for javascript, it will be interesting to see how we can use it integrate with existing javascript. It is a good abstraction over websockets that could have other uses.