A few days ago phoenix live view went public. I was anticipating its release since first sneak peek at elixir conf. Despite being in such early development stage, it already have great introduction, documentation and examples. There are also already many other community blogs on it. A great resource on what community is building already with it is #liveview tag on Twitter.
Assumptions
- You know what live view is and have it set up.
Works on my machine
- elixir: 1.8.1
- phoenix: 1.4.1
We will create a world clock app, for user to select and see what time it is in what parts of the world.
Let's start by registering the live view directly in routes.
defmodule MyAppWeb.Router do
...
scope "/", MyAppWeb do
pipe_through :browser
# Registering live view handler for specific URL
live "/time", TimeLive
end
end
One gotcha for registering live views in routes is to provide default layout. Otherwise, you might get a case when view HTML itself on the initial request will be returned, but with no surrounding HTML. Most importantly js includes that handles live view updates from frontend side will be missing.
defmodule MyAppWeb.Router do
...
pipeline :browser do
plug Phoenix.LiveView.Flash
...
# Add default layout
plug :put_layout, {MyAppWeb.LayoutView, :app}
end
...
end
First tiny step, show UTC live time
defmodule MyAppWeb.TimeLive do
@moduledoc """
A module that renders and handles all interactions from ui
"""
use Phoenix.LiveView
@doc """
Renders this simple html with ony one plain varaible output
"""
def render(assigns) do
~L"<%= @time %>"
end
@doc """
First initial view state setup
- Registers tick every second
- Delegates work to handle tick
"""
def mount(_session, socket) do
:timer.send_interval(1000, self(), :tick)
{:ok, handle_tick(socket)}
end
@doc """
Delegates work to handle tick
"""
def handle_info(:tick, socket) do
{:noreply, handle_tick(socket)}
end
@doc """
Handles each tick.
- Gets current time in a human readable format
- saves it for view to use
"""
defp handle_tick(socket) do
time = NaiveDateTime.utc_now()
assign(socket, :time, time)
end
end
Workflow:
-
mount
callback is called an initial state (current time) for the view is prepared; -
render
callback with previously prepared data is called and HTML rendered; - HTML is sent to the browser;
- every second (each tick)
handle_info
callback with:tick
message is called, which triggers view to recalculate current time; - live view detects state changes and triggers
render
callback again and new HTML is generated - HTML is sent to the browser (i assume there are some optimizations, like sending the only diff, or something like that. Will have to look into that).
Allow the user to choose timezone
I still don't quite get timezones in elixir. By default, elixir supports only Etc/UTC
. Found out about tzdata. Setting it up is outside if this scope, but it was pretty easy.
defmodule MyAppWeb.TimeLive do
...
@doc """
Renders html via phoenix tempaltes
"""
def render(assigns) do
MyAppWeb.LiveView.render("time.html", assigns)
end
@doc """
Now when mounting we assign all timezones (for the user to choose from dropdown)
Assign changeset (using changeset, because will accept input from the user)
"""
def mount(_session, socket) do
:timer.send_interval(1000, self(), :tick)
socket =
socket
|> assign(:timezones, Tzdata.zone_list())
|> assign(:changeset, LocalTime.timezone_changeset(%{timezone: "Etc/UTC"}))
|> handle_tick()
{:ok, socket}
end
@doc """
Accepting dropdown value change.
Creates new changeset with newly selected timezone.
"""
def handle_event("update_timezone", %{"local_time" => new_timezone}, socket) do
socket =
socket
|> assign(:changeset, LocalTime.timezone_changeset(new_timezone))
|> handle_tick()
{:noreply, socket}
end
@doc """
Now handling tick means to update time, not with `NaiveDateTime`, but for selected timezone.
"""
defp handle_tick(socket) do
assign(socket, :changeset, LocalTime.update_time(socket.assigns.changeset))
end
end
Extracted view template lib/my_app_web/templates/live/time.html.leex
looks as follows:
<%= f = form_for @changeset, "#", [phx_change: "update_timezone"] %>
<%= select f, :timezone, @timezones, promt: "Select timezone" %>
<%= text_input f, :time, readonly: true %>
</form>
You probably noticed that there are multiple references to LocalTime
module. for changesets to work properly I had to create an embedded schema. It will also be responsible for validating user input.
defmodule LocalTime do
@moduledoc """
Handling data integrty
"""
use Ecto.Schema
alias Ecto.Changeset
embedded_schema do
field :timezone, :string
field :time, :string
end
@doc """
Changeset that does heavy lifting of validating if user input is correct
"""
def timezone_changeset(params \\ %{}) do
%__MODULE__{}
|> Changeset.cast(params, [:timezone])
|> Changeset.validate_inclusion(:timezone, Tzdata.zone_list())
|> Map.put(:action, :insert)
end
@doc """
Updates time for the specified changeset.
If a changeset is invalid, there is no point on updating time, as the user has specified the wrong timezone.
If timezone is correct, create calculate current datetime for specified timezone
"""
def update_time(%Changeset{valid?: false} = changeset) do
changeset
end
def update_time(%Changeset{changes: %{timezone: timezone}} = changeset) do
{:ok, time} = DateTime.now(timezone)
Changeset.force_change(changeset, :time, time)
end
end
Conclusion
The live view seems great technology. There are still many things I need to grasp (probably more phoenix framework wise). Manipulating changesets seemed odd. LocalTime
shouldn't update time via Changeset
. Or maybe it should, and I'm just wanting to make things more complex than they should be. In either case, after heavily using vuejs for a year. This seems so similar, yet so different.
I wonder, will live view stack also grow, like all those modern js frameworks. Router for when you want to go to different page, but reload only part of the page. How will live view template nesting pan out? Some shared state manager, like redux or vuex. Will there eventually be a division of live view components in smart and stupid ones.
With all modern js craze, it seems that headless cmss, and crmss are the trend. The live view seems like a sane alternative when you don't want to maintain two stacks (backend, frontend) and make everything communicate via a json API. There is a place for that, don't get me wrong, but I already have seen many places, where it was introduced because for trendiness. And all complexity overhead that comes along with it was unnecessary.
For me, live view have this sweet spot, where I can see how I can do everything I was able to do with jquery (data related dom manipulation wise, don't know about animations jet), 80% of what I was able to do with vuejs, but with minimal new technology stack overhead (It will come down to component reusability, it such thing even is in roadmap).
What are your thoughts on it? Will or maybe already have used it? Did you like it? Do you see the place for it, in your toolbox? Let me know.
Have a great day.
P.S. If i werent clear on some points. If there is a bug in my code, or just things that i have skipped over, please let me know as well.
Whole code without comments
defmodule MyAppWeb.TimeLive do
@moduledoc """
A module that renders and handles all interactions from ui
"""
use Phoenix.LiveView
@doc """
Renders this simple html with ony one plain varaible output
"""
def render(assigns) do
MyAppWeb.LiveView.render("time.html", assigns)
end
def mount(_session, socket) do
:timer.send_interval(1000, self(), :tick)
socket =
socket
|> assign(:timezones, Tzdata.zone_list())
|> assign(:changeset, LocalTime.timezone_changeset(%{timezone: "Etc/UTC"}))
|> handle_tick()
{:ok, socket}
end
def handle_info(:tick, socket) do
{:noreply, handle_tick(socket)}
end
def handle_event("update_timezone", %{"local_time" => new_timezone}, socket) do
socket =
socket
|> assign(:changeset, LocalTime.timezone_changeset(new_timezone))
|> handle_tick()
{:noreply, socket}
end
defp handle_tick(socket) do
assign(socket, :changeset, LocalTime.update_time(socket.assigns.changeset))
end
end
defmodule LocalTime do
use Ecto.Schema
alias Ecto.Changeset
embedded_schema do
field :timezone, :string
field :time, :string
end
def timezone_changeset(params \\ %{}) do
%__MODULE__{}
|> Changeset.cast(params, [:timezone])
|> Changeset.validate_inclusion(:timezone, Tzdata.zone_list())
|> Map.put(:action, :insert)
end
def update_time(%Changeset{valid?: false} = changeset) do
changeset
end
def update_time(%Changeset{changes: %{timezone: timezone}} = changeset) do
{:ok, time} = DateTime.now(timezone)
Changeset.force_change(changeset, :time, time)
end
end
#
<%= f = form_for @changeset, "#", [phx_change: "update_timezone"] %>
<%= select f, :timezone, @timezones, promt: "Select timezone" %>
<%= error_tag f, :timezone %>
<%= text_input f, :time, readonly: true %>
</form>
Top comments (0)