In the previous video we learned what makes Phoenix LiveView unique. Now let’s build a basic LiveView from scratch to see how to react to user events. This video is from our LiveView course, and all the steps are outlined below so you can easily recreate it yourself. 🏗
A Simple Light Controller
Building this simple light controller is a great way to get our feet wet with LiveView. It starts at 10% brightness. From there we can turn it off, turn it all the way on, or adjust the brightness where we want it.

Create the Phoenix App
In the video above we started in a directory that already had a generated Phoenix app. To get to the same starting point, first create a new Phoenix app:
mix phx.new live_view_studio
Then change into the live_view_studio
directory and set up the app:
cd live_view_studio
mix setup
Now you’re ready to fire up the Phoenix server:
mix phx.server
Then if you then browse to http://localhost:4000 you should see the standard Phoenix welcome page. 🔥
Add a Live Route
Next we need a live route which we define using the live
macro in router.ex
like so:
scope "/", LiveViewStudioWeb do
pipe_through :browser
...
live "/light", LightLive
end
The live
macro takes a path and a LiveView module to use. So in this case a request to /light
gets routed to the LightLive
module.
Let’s define that module…
Create a LiveView Module
By convention LiveView modules reside in the live_view_studio_web/live/
directory. Create a light_live.ex
file in that directory and then define the LightLive
module like so:
defmodule LiveViewStudioWeb.LightLive do
use LiveViewStudioWeb, :live_view
end
All the good stuff we need to be able to create a LiveView gets pulled in by use LiveViewStudioWeb, :live_view
.
Define mount
A LiveView module needs to define three callback functions:
-
mount
assigns the initial state of the LiveView process -
handle_event
changes the state of the process -
and
render
renders a new view for the newly-updated state
Let’s start with the mount
callback since it’s the first callback that’s invoked when the request comes in through the router:
defmodule LiveViewStudioWeb.LightLive do
use LiveViewStudioWeb, :live_view
def mount(_params, _session, socket) do
end
end
The mount
callback takes three arguments:
-
params
is a map containing the current query params, as well as any router parameters -
session
is a map containing private session data -
socket
is a struct representing the websocket connection
For this example we only need the socket
argument, so we’ve ignored the others. It’s in the socket
struct that we store the state of a LiveView process. And it’s in mount
that we assign the initial state.
Let’s say we want the light to initially be set at a warm glow of 10% brightness. To do that we use the assign
function, passing it the socket
and a key/value pair to assign to the socket:
def mount(_params, _session, socket) do
socket = assign(socket, :brightness, 10)
{:ok, socket}
end
Calling assign
this way returns a new socket
struct with :brightness
set to 10
. And mount
must return an :ok
tuple with the new socket
.
Rather than using the temporary socket
variable you’ll often see it inlined like this:
def mount(_params, _session, socket) do
{:ok, assign(socket, :brightness, 10)}
end
Notice we passed the key and value as separate arguments to assign
, but if you had multiple key/value pairs you would pass a keyword list.
Define render
Next up we’ll define the render
callback:
def render(assigns) do
~H"""
"""
end
The render
function takes assigns
which is the map of key/value pairs we assigned to the socket in mount
. The one-and-only job of render
is to return rendered content, which we create using a LiveView template.
In this case we used the special ~H
sigil which defines an inlined LiveView template. Alternatively, we could put this template code in a separate file, but we’ll co-locate the template since it’ll be relatively short.
Inside the template we can access the current brightness value using @brightness
and interpolate it in an EEx tag:
def render(assigns) do
~H"""
<h1>Front Porch Light</h1>
<%= @brightness %>%
"""
end
Now if you hop into the browser and go to http://localhost:4000/light, you should see the initial brightness of 10%.
Great, so mount
assigns the initial state and render
renders it. Easy, peasy.
Show a Light Meter
Now let’s dress it up a bit by using the brightness value to display a light meter, kinda like a progress bar. We’ll use a span
with a width
based on the current brightness value:
def render(assigns) do
~H"""
<h1>Front Porch Light</h1>
<div class="meter">
<span style={"width: #{@brightness}%"}>
<%= @brightness %>%
</span>
</div>
"""
end
A splash of CSS is needed to bring this to life. To save you the trouble, go ahead and plunk this in at the bottom of live_view_studio/assets/css/app.css
:
.meter {
display: flex;
overflow: hidden;
background-color: #e2e8f0;
border-radius: 0.75rem;
margin-bottom: 2rem;
height: 4rem;
}
.meter span {
display: flex;
flex-direction: column;
justify-content: center;
background-color: #faf089;
text-align: center;
white-space: nowrap;
font-weight: 700;
font-size: 1.5rem;
transition: width 2s ease;
}
Not too shabby, eh?
Make It Interactive: On and Off Buttons
Now let’s make it interactive by adding buttons to turn the light on and off. They’re part of the view, so they go in the render
function below the light meter:
<button>
Off
</button>
<button>
On
</button>
When a button is clicked we need to send an event to our LightLive
process to change the brightness. To do that, we use the special phx-click
HTML attribute (it’s unique to LiveView) and its value is the name of the event to send. Because we’re wildly creative, we’ll have the “Off” button send an off
event and the “On” button send an on
event:
<button phx-click="off">
Off
</button>
<button phx-click="on">
On
</button>
LiveView refers to the phx-click
attribute (and others like it) as a binding. It binds a click event to the button so when it’s clicked the event is sent via the websocket to the LightLive
process.
Handle On and Off Events
Then to handle those inbound events, we need to define matching handle_event
callbacks. We’ll start by handling the on
event:
def handle_event("on", _, socket) do
socket = assign(socket, :brightness, 100)
{:noreply, socket}
end
The handle_event
callback takes three arguments:
- the name of the event (
on
in this case) - metadata related to the event which we’ll ignore
- the
socket
which, remember, has the current state of our live view assigned to it
To turn the light on we use assign
to set the brightness
value to 100
. And then handle_event
must return a tuple with :noreply
and the new socket
.
Now, whenever a live view’s state changes (in our case brightness
changed from 10
to 100
), the render
function is automatically called to render a new view with the newly-updated state.
Then we also need a handle_event
callback for the off
event:
def handle_event("off", _, socket) do
socket = assign(socket, :brightness, 0)
{:noreply, socket}
end
And that’s all there is to reacting to user events. 👍
Dim the Lights
Now that we’ve got the hang of this, let’s add a couple more buttons to dim the lights down and up, which will introduce us to something new.
No surprise, these buttons emit corresponding down
and up
events:
<button phx-click="down">
Down
</button>
<button phx-click="up">
Up
</button>
Then we need to handle those two inbound events. The down
event will dial back the brightness by 10 and the up
event will bump up the brightness by 10.
To do that, the respective handle_event
callbacks need to access the current brightness
value. And since the socket
struct holds the current state of a LiveView process, we can get the current brightness
value using socket.assigns.brightness
. So the handlers look like this:
def handle_event("down", _, socket) do
brightness = socket.assigns.brightness - 10
socket = assign(socket, :brightness, brightness)
{:noreply, socket}
end
def handle_event("up", _, socket) do
brightness = socket.assigns.brightness + 10
socket = assign(socket, :brightness, brightness)
{:noreply, socket}
end
That works, but there’s a convenient shortcut. When updating a value, we can use the update
function, like so:
def handle_event("down", _, socket) do
socket = update(socket, :brightness, fn b -> b - 10 end)
{:noreply, socket}
end
def handle_event("up", _, socket) do
socket = update(socket, :brightness, fn b -> b + 10 end)
{:noreply, socket}
end
Notice that update
takes a function as its third argument. That function receives the current key’s value (the brightness
value in this case) and returns an updated value which is then assigned to the key in the socket. Thus, the value is updated.
You can make this more concise using the Elixir shorthand capture syntax:
def handle_event("down", _, socket) do
socket = update(socket, :brightness, &(&1 - 10))
{:noreply, socket}
end
def handle_event("up", _, socket) do
socket = update(socket, :brightness, &(&1 + 10))
{:noreply, socket}
end
For bonus points, you could limit how high or low the light can go by using max and min:
def handle_event("down", _, socket) do
socket = update(socket, :brightness, &max(&1 - 10, 0))
{:noreply, socket}
end
def handle_event("up", _, socket) do
socket = update(socket, :brightness, &min(&1 + 10, 100))
{:noreply, socket}
end
Now if you head over to http://localhost:4000/light, you should have a fully-functioning light controller. 💡
Summary
Stepping back, you gotta admit that’s a lot of functionality for such a minimal amount of code:
-
mount
initialize the state of the LiveView process by assigning values to the socket -
render
uses a LiveView template to render a view for the state in the socket -
handle_event
functions handle each inbound event by changing the state in the socket using eitherassign
orupdate
And whenever the state changes render
is automatically called to render a new view for the updated state. 🌀
It’s a fairly simple (and fun!) programming model, all written in Elixir. Not one line of custom JavaScript! But there’s more going on behind the scenes than meets the eye. And we’ll look at the lifecycle of a LiveView in more detail in the next video.
🔥 Learn LiveView In Depth!
We distilled everything you need to know about LiveView, assembled it in the right order, and neatly packaged it as a video course that's paced for experienced, gotta-get-it-done developers like you. 🙌
