(Partly generated by LLM)
Phoenix LiveView tutorial
Purpose: Phoenix LiveView has great documentation in Hexdocs, but it is in a top-down format. You can read it from start to end and understand many things, but if you want help on how to do a certain thing, it is extremely difficult to find concise information. Additionally, it provides various generator utilities that generate all CRUD pages and code. However, these are so complicated—especially in the LiveView framework—that making even trivial changes is quite complex unless you understand everything in it.
This tutorial is my attempt to build a simple app in Phoenix LiveView in a bottom-up fashion. I’ll start with very basic but complete examples and build complexity gradually until we have a complete CRUD app. Finally, I’ll try to compare my code with the generator code.
1: LiveView basics:
1.1: Setup
To get started with Phoenix LiveView, first create a new Phoenix project with LiveView enabled. Once your project is set up, you need to add a route for your LiveView in the router:
-
Add to your router:
live "/scratchpad", Scratchpad
-
Create the file
live/scratchpad/index.ex
and define your LiveView module. Here’s a minimal example with a simple render function:
defmodule YourWeb.Scratchpad do
use YourWeb, :live_view
def render(assigns) do
~H"""
<h1>Scratchpad</h1>
"""
end
end
This sets up a basic LiveView page that simply displays a heading.
1.2: Assigns
LiveView uses assigns to manage state between the server and the client. You can set initial assigns in the mount/3
callback. Here’s how you can assign some data and use it in your render function:
- Add a
mount
function:
def mount(_params, _session, socket) do
IO.puts("mount called")
{:ok, assign(socket, :data, "test data")}
end
- Use the assigned data in your render function:
def render(assigns) do
IO.inspect(assigns, label: "Scratchpad assigns")
~H"""
<h1>Scratchpad</h1>
data: {@data}
"""
end
Now, when you visit the page, you’ll see the value of @data
rendered.
1.3: Interactivity using Events
LiveView allows you to handle client-side events on the server. For example, you can handle button clicks by defining a handle_event/3
function.
- Add an event handler:
def handle_event(event, _params, socket) do
IO.puts("handle_event called with event: #{inspect(event)}")
{:noreply, socket}
end
- Add a button to generate an event:
def render(assigns) do
IO.inspect(assigns, label: "Scratchpad assigns")
~H"""
<h1>Scratchpad</h1>
data: {@data}
<button class="bg-blue-300 rounded-md p-2 m-2" phx-click="btn-click">Click me</button>
"""
end
When the button is clicked, the btn-click
event is sent to the server and handled by your handle_event/3
function.
1.4: Submit Data
You can use forms in LiveView to submit data to the server. The form submission triggers an event, and you can update assigns based on the submitted data.
- Add a form, an input, and a submit button:
def render(assigns) do
IO.inspect(assigns, label: "Scratchpad assigns")
~H"""
<h1>Scratchpad</h1>
Your input: {@data}
<form phx-submit="form-submit">
<input type="text" name="input_data" />
<button type="submit" class="bg-blue-200 p-2 m-2">Submit</button>
</form>
"""
end
- Handle the form submission event and update the data:
def handle_event(event, params, socket) do
IO.puts("handle_event called with event: #{inspect(event)}")
{:noreply, assign(socket, :data,
params["input_data"])}
end
Now, when you submit the form, the input value is assigned to @data
and displayed on the page.
1.5: JavaScript Interop
LiveView supports interop with client-side JavaScript using hooks. You can push events from JavaScript to the server and update the UI accordingly.
Example: Timer with JS and LiveView
Suppose you want a JavaScript timer that ticks every second, and every 10 ticks, it sends a message to the server. Both the JS ticks and server messages are shown in the browser.
- Client-side changes:
Add a hook in your assets/js/app.js
:
// assets/js/app.js
let Hooks = {}
Hooks.TimerHook = {
mounted() {
let count = 0
this.timer = setInterval(() => {
count++
document.getElementById("actual_tick").innerText = "Actual tick " + count
if (count % 10 === 0) {
this.pushEvent("tick", {count: count})
}
}, 1000)
},
destroyed() {
clearInterval(this.timer)
}
}
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: Hooks,
})
-
Server-side changes:
- Add placeholders in your component’s assigns:
def mount(_params, _session, socket) do
IO.puts("mount called")
{:ok, socket
|> assign(:data, "")
|> assign(:event, "")}
end
- Render using the placeholders:
def render(assigns) do
IO.inspect(assigns, label: "Scratchpad assigns")
~H"""
<div id="timer" phx-hook="TimerHook">
<h1>Scratchpad</h1>
<p id="actual_tick"></p>
<p>Event: {@event}</p>
<p>Your input: {@data}</p>
<form phx-submit="form-submit">
<input type="text" name="input_data" />
<button type="submit" class="bg-blue-200 p-2 m-2">Submit</button>
</form>
</div>
"""
end
- Handle the events from both the form and the timer:
def handle_event(event, params, socket) do
case event do
"form-submit" ->
IO.puts("Form submitted with params: #{inspect(params)}")
{:noreply,
socket
|> assign(:data, params["input_data"] || "")
|> assign(:event, event)}
"tick" ->
IO.puts("Tick event received")
{:noreply,
socket
|> assign(:event, event <> " " <> to_string(params["count"]))}
_ ->
IO.puts("Unknown event: #{event}")
{:noreply,
socket
|> assign(:event, event)}
end
end
This setup allows you to see real-time updates from both JavaScript and server events in your LiveView.
1.6: Conditional Attributes and Rendering
LiveView makes it easy to conditionally render elements and change their attributes based on state. For example, you can show or hide a div and change its class based on a checkbox value.
- Setup (mount):
def mount(_params, _session, socket) do
IO.puts("mount called")
{:ok, socket
|> assign(:data, :off)
|> assign(:event, "")}
end
- Render:
Note the conditional checked
attribute and the use of phx-change
to handle checkbox changes. The class and visibility of elements are updated based on the value of @data
.
def render(assigns) do
IO.inspect(assigns, label: "Scratchpad assigns")
~H"""
<div id="timer">
<h1>Scratchpad</h1>
<p id="actual_tick"></p>
<p>Event: {@event}</p>
<p>Your input: {@data}</p>
<form phx-change="checkbox">
<input type="checkbox" name="show?" checked={@data==:on}/> Show Div
</form>
<div class={if @data==:on, do: "bg-green-100"}> Random string</div>
<div id="toggle_div" :if={@data == :on}>
<p>This is a toggleable div.</p>
</div>
</div>
"""
end
- Handle event:
def handle_event(event, params, socket) do
case event do
"checkbox" ->
case params["show?"] do
"on" ->
IO.puts("Checkbox checked, showing div")
{:noreply,
socket
# }
|> assign(:data, :on)
}
_ ->
IO.puts("Checkbox unchecked, hiding div")
{:noreply,
socket
# }
|> assign(:data, :off)
}
end
_ ->
IO.puts("Unknown event: #{event}")
{:noreply,
socket
|> assign(:event, event)}
end
# IO.puts("handle_event called with event: #{inspect(event)}")
end
This example demonstrates how to use LiveView’s assigns and event handling to create dynamic, interactive UIs with conditional rendering and attribute changes.
This completes part 1 of the tutorial, which covers fundamentals of liveview. In next tutorial we’ll build basic components for a typical CRUD app.