Phoenix 1.5 LiveView & PubSub Basics
Create a Dynamic Web GUI without JavaScript!
I have been watching Phoenix and Elixir for a while, and the idea of writing dynamic Web Applications without needing a ton of JavaScript is very interesting. I recently saw this video by Chris McCord:
which is very cool. I wanted to learn more and found this Phoenix 1.4 tutorial:
- https://www.youtube.com/watch?v=2bipVjOcvdI
- https://dennisbeatty.com/2019/03/19/how-to-create-a-counter-with-phoenix-live-view.html
and decided to translate that into Phoenix 1.5. This is what follows.
NOTE: Since I am just learning the Phoenix Framework and will need to refer to this for my self to remember how to do basic things – I’ve documented every little detail.
Step 0 - setup environment
Setup environment & newest version of elixir:
exenv install 1.10.3
exenv global
exenv local 1.10.3
Install the 1.5.1 phx_new generator:
mix archive.install hex phx_new 1.5.1
Step 1: Create a Phoenix Project with LiveView
Create the project (notice the --live
- that enables LiveView, --no-ecto
- keeps the project smaller since we won’t be persisting any data):
mix phx.new counter --no-ecto --live
enter project and create init commit:
cd counter
git init && git add -A && git commit -m "init"
Step 2 - simple counter page using LiveView
Make a counter_live folder & an index.ex file:
mkdir lib/counter_web/live/counter_live
touch lib/counter_web/live/counter_live/index.ex
cat <<EOF > lib/counter_web/live/counter_live/index.ex
# lib/counter_web/live/counter_live/index.ex
defmodule CounterWeb.CounterLive.Index do
use CounterWeb, :live_view
# since we don't have a db to pull from we initialize on mount
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :val, 0)}
end
def render(assigns) do
~L"""
<h1>Live Counter</h1>
<p>
<b>Here is a great complex page</b>
</p>
<div>
<h2>The count is: <%= @val %></h2>
<button phx-click="dec">-</button>
<button phx-click="inc">+</button>
</div>
<div>
<button phx-click="clear">clear</button>
</div>
<p>
<i>even more awesome content</i>
</p>
"""
end
# event handler for <button phx-click="inc">
def handle_event("inc", _, socket) do
{:noreply, update(socket, :val, &(&1 + 1))}
end
# event handler for <button phx-click="dec">
def handle_event("dec", _, socket) do
{:noreply, update(socket, :val, &(&1 - 1))}
end
# event handler for <button phx-click="clear">
def handle_event("clear", _, socket) do
{:noreply, update(socket, :val, &(&1 - &1))}
# {:noreply, update(socket, :val, 0)} # very slow - why?
end
end
Now update the routers (so we can get to the new webpage – now our app should work:
scope "/", CounterWeb do
pipe_through :browser
# live "/", PageLive, :index # remove this line
live "/", CounterLive.Index, :index # add this line
end
Start pheonix:
mix phx.server
Go to:
localhost:4000
You should now see the website and the counter should function
Assuming all is good, I’ll take a git snapshot:
git add .
git commit -m "counter with live update"
Step 3 - Running tests
In order to run the tests we type:
mix test
We see that PageLive test fails. This is because we replaced this behavior with CounterLive
To fix this we will create a CounterLive test and delete PageLive test.
rm test/counter_web/live/page_live_text.exs
touch test/counter_web/live/counter_live_text.exs
cat <<EOF > test/counter_web/live/counter_live_text.exs
# test/counter_web/live/counter_live_text.exs
defmodule CounterWeb.CounterLiveTest do
use CounterWeb.ConnCase
import CounterWeb.CounterLive.Index
test "disconnected and connected render", %{conn: conn} do
{:ok, page_live, disconnected_html} = live(conn, "/")
assert disconnected_html =~ "Live Counter"
assert render(page_live) =~ "Live Counter"
end
end
Now we can test again: mix test
Now that works, lets take another git snapshot:
git add .
git commit -m "counter with live update"
Step 4 – LiveView Templates
Create a template file (helpful for complex html pages, but simple to create):
touch lib/counter_web/live/counter_live/index.html.leex
Now just copy the html (from the render method into this file):
# lib/counter_web/live/counter_live/index.html.leex
<h1>Live Counter</h1>
<p>
<b>Here is a great complex page</b>
</p>
<div>
<h2>The count is: <%= @val %></h2>
<button phx-click="dec">-</button>
<button phx-click="inc">+</button>
</div>
<div>
<button phx-click="clear">clear</button>
</div>
<p>
<i>even more awesome content</i>
</p>
Now point lib/counter_web/live/counter_live/index.ex
to this file by replacing render with an apply command:
|
|
NOTE: apply_action
understands the rest verbs such as :new
, :show
etc.
Now try the app again and it should still work!
Assuming it still works, I’ll take another git snapshot:
git add .
git commit -m "counter using a template"
Step 5 - Reusable Components (& isolation)
This allows complex components to be reused within multiple templates and isolation to keep one’s mental scope minimal.
Create a file for the component:
touch lib/counter_web/live/counter_live/counter_component.ex
Move the dynamic html and it’s associated functions into this file, it’s important to import the live_components into this file using:
use CounterWeb, :live_component
In order to encapsulate the events into the component we will also move the event handlers into the component file.
So this file will now look like:
|
|
Notice the button tags are slightly more complex
<button phx-click="dec" phx-target="<%= @myself %>">
the @myself basically informs the event that the handler is within the component.
Now update the live template to point at the component using:
<%= live_component @socket, CounterWeb.CounterLive.CounterComponent, id: 0, val: @val %>
Also note we need to pass the @val value into the component using:
id: 0, val: @val
its a little wierd, but we need to pass an id even if there is no ecto backed record.
Now the template file looks like a normal template file again (focused on formating):
|
|
Lets check that this still works.
Assuming it still works, I’ll make one last git snapshot:
git add .
git commit -m "live pages using isolated components - like JS does"