Phoenix LiveView with Elixir

Updated: 10 May 2024

Notes based on How to start writing LiveView

Prerequisites

Probably take a look at the Elixir Notes and Phoenix Notes

In order to get started the following are required:

It’s also handy to have the following installed if using VSCode:

Initialize a Project

Firstly, initialize a new Phoenix app called phoenixlive with:

Terminal window
1
mix phx.new phoenixlive --database sqlite3

A few important files we want to be aware of specifically related to the LiveView app are:

  1. lib/elixirlive_web/components/layouts/root.html.heex which is loaded once
  2. lib/elixirlive_web/components/layouts/app.html.heex which is updated by LiveView when changes happen

Authentication

Phoenix comes with a generator for building user authentication. In order to do this we can use:

Terminal window
1
mix phx.gen.auth Users User users

Then, re-fetch dependencies:

Terminal window
1
mix deps.get

And run migrations

Terminal window
1
mix ecto.migrate

Then, start the server with:

Terminal window
1
mix phx.server

You can then visit the application and you will now see a Register and Log In button. You can register to create a new user

Resource

We can generate a live resource called Link:

Terminal window
1
mix phx.gen.live Links Link links url:text

And then run mix ecto migrate

All of the live views will be in the lib/web/live directory

This will have generated a lot of content but we’ll delete most of this as we go on

For now though, we can add the live routes to our router.ex file as directed by the command output:

lib/phoenixlive_web/router.ex
1
scope "/", PhoenixliveWeb do
2
pipe_through [:browser, :require_authenticated_user]
3
4
live_session :require_authenticated_user,
5
on_mount: [{PhoenixliveWeb.UserAuth, :ensure_authenticated}] do
6
live "/users/settings", UserSettingsLive, :edit
7
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
8
9
live "/links", LinkLive.Index
10
end
11
end

Make sure that your IDE doesn’t auto import anything when adding routes - I got stuck with an annoying _live_/0 is undefined issue because it added an import that had a naming collision with my routes

At this point to avoid potential collisions and issues please also delete the following files:

  1. lib/phoenixlive_web/live/link_live/show.ex
  2. lib/phoenixlive_web/live/link_live/show.html.heex
  3. lib/phoenixlive_web/live/link_live/form_component.ex

Live Views

LiveView implicitly has some callbacks that we should implement

  1. mount
  2. handle_params
  3. render - implicit when we have a template file

We can clear our resource’s index.ex file and add the following:

lib/phoenixlive_web/live/link_live/index.ex
1
defmodule PhoenixliveWeb.LinkLive.Index do
2
use PhoenixliveWeb, :live_view
3
4
def mount(_params, _session, socket) do
5
{:ok, socket}
6
end
7
end

The socket is like our conn in a normal route. Every function basically does something to the socket and passes it on and this is effectively how a request is executed from a functional standpoint

And we can update the index.html.heex to just have some placeholder content for now:

lib/phoenixlive_web/live/link_live/index.html.heex
1
<div>Hello World</div>

If we start our server again we can visit the /links page when logged in to see the content of the page we just added

We can update our screen to list thelinks for the current user. In order to do this we need to do a few things first:

  1. Add a reference to the user from our Link schema
  2. Define a migration for adding the user reference to the database table
  3. Add a way to list links by user
  4. Get the user from the current route and use that to list the links
  5. Display a list of links in the HEEX template

Add Reference to User

To add the reference to the user, we can add the following lines to our schema:

lib/phoenixlive/links/link.ex
1
defmodule Phoenixlive.Links.Link do
2
use Ecto.Schema
3
import Ecto.Changeset
4
5
schema "links" do
6
field :url, :string
7
8
belongs_to :user, Phoenixlive.Users.User
9
10
timestamps()
11
end
12
13
@doc false
14
def changeset(link, attrs) do
15
link
16
|> cast(attrs, [:url, :user_id])
17
|> validate_required([:url, :user_id])
18
end
19
end

Add a Migration

We can define a migration by generating a new migration with:

Terminal window
1
mix ecto.gen.migration add_user_to_link

Which should generate a migration file into which we add the following:

priv/repo/migrations/20240510083258_add_user_to_link.exs
1
defmodule Phoenixlive.Repo.Migrations.AddUserToLink do
2
use Ecto.Migration
3
4
def change do
5
alter table(:links) do
6
add :user_id, references(:users, on_delete: :nothing)
7
end
8
end
9
end

Then we can apply the migration with:

Terminal window
1
mix ecto.migrate

We can update our list_links function to take a user_id. We can implement it using the Ecto Query Syntax:

lib/phoenixlive/links.ex
1
def list_links(user_id) do
2
Repo.all(from l in Link, where: l.user_id == ^user_id)
3
end

Get Current User from Route

We can get the current user from the route using the socket.assigns, this comes from the router :ensure_authenticated reference that we have in the router.ex file. Using this data we can list the links for the user and assign it to the socket which will make it available during render:

lib/phoenixlive_web/live/link_live/index.ex
1
defmodule PhoenixliveWeb.LinkLive.Index do
2
alias Phoenixlive.Links
3
use PhoenixliveWeb, :live_view
4
5
def mount(_params, _session, socket) do
6
user_id = socket.assigns.current_user.id
7
8
socket = socket
9
|> assign(:links, Links.list_links(user_id))
10
11
{:ok, socket}
12
end
13
end

We can access the links using @links in our template. Phoenix uses this for change tracking in live view which means this data will be re-rendered when changed. We can use the for attribute to iterate through these values. The resulting template can be seen below:

lib/phoenixlive_web/live/link_live/index.html.heex
1
<h1>Links</h1>
2
3
<ol>
4
<li :for={link <- @links}><%= link.url %>></li>
5
</ol>

We have a way to list links but we don’t have anything in our database at this point, in order to do this we need to add a way to add links. First, we’re going to add a link using the LiveView Link Component. We’re going to use the navigate property which will give us SPA like naviation along with the ~p syntax for the route which will type check the URL we use relative to our app router and warn us if it does not exist:

lib/phoenixlive_web/live/link_live/index.html.heex
1
<h1>Links</h1>
2
3
<.link navigate={~p"/new"}>
4
Add Link
5
</.link>
6
7
<ol>
8
<li :for={link <- @links}><%= link.url %>></li>
9
</ol>

Next, we can define our Live View and make it pass in a form attribute that can be used by our template to render a form. This makes use of a changeset and the to_form method to get the data into the form data structure. Additionally, we need to handle the submit event so that the user can submit the form and we will create the entry in our database

1
defmodule PhoenixliveWeb.LinkLive.New do
2
alias Phoenixlive.Links
3
use PhoenixliveWeb, :live_view
4
5
def mount(_params, _session, socket) do
6
changeset = Links.Link.changeset(%Links.Link{})
7
8
socket = socket
9
|> assign(:form, to_form(changeset))
10
11
{:ok, socket}
12
end
13
14
def handle_event("submit", %{"link" => link_params}, socket) do
15
user_id = socket.assigns.current_user.id
16
17
params = link_params |> Map.put("user_id", user_id)
18
19
case Links.create_link(params) do
20
{:ok, _link} ->
21
socket = socket
22
|> put_flash(:info, "Link created successfully")
23
|> push_navigate(to: ~p"/links")
24
25
{:noreply, socket}
26
27
{:error, changeset} ->
28
socket = socket
29
|> assign(:form, to_form(changeset))
30
31
{:noreply, socket}
32
end
33
end
34
end

In the handle_event when the form is submitted we use Links.create_link to create a new link in the database using the user_id from the socket.assigns. We also use put_flash which will show a message to the user in the UI as well as push_navigate which will navigate the user to another URL

When we have an error, we currently assign the changeset back to the form, an example of a changeset which deontes an error can be seen below:

1
#Ecto.Changeset<
2
action: :insert,
3
changes: %{},
4
errors: [
5
user_id: {"can't be blank", [validation: :required]},
6
url: {"is invalid", [type: :string, validation: :cast]}
7
],
8
data: #Phoenixlive.Links.Link<>,
9
valid?: false
10
>

Next, we can add a template for the /links/new route which references the LiveView Form Component that will use the @form data we pass into the template. We will also use the .input component from our lib/phoenixlive_web/components/core_components.ex file to render the fields for our form:

lib/phoenixlive_web/live/link_live/new.html.heex
1
<h1>Add Link</h1>
2
3
<.form for={@form} phx-submit="submit">
4
<.input field={@form[:url]} type="text" label="url" />
5
6
<button type="submit">Submit</button>
7
</.form>

In the form, we also use the phx-submit attribute which defines the event that our form submission will fire, this relates directly to the event we defined in our handle_event method above

And add a reference to this page in the router:

lib/phoenixlive_web/router.ex
1
scope "/", PhoenixliveWeb do
2
pipe_through [:browser, :require_authenticated_user]
3
4
live_session :require_authenticated_user,
5
on_mount: [{PhoenixliveWeb.UserAuth, :ensure_authenticated}] do
6
live "/users/settings", UserSettingsLive, :edit
7
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
8
9
live "/links", LinkLive.Index
10
live "/links/new", LinkLive.New
11
end
12
end

We should be able to visit the /links/new screen now to view our new page

Making things Live

The thing that makes LiveView really “Live” is the ability to easily work with data and forms and have the result easily visible to a user. To do this, we’ll add a delete button in our list view:

lib/phoenixlive_web/live/link_live/index.html.heex
1
<h1>Links</h1>
2
3
<.link navigate={~p"/links/new"}>
4
Add Link
5
</.link>
6
7
<ol>
8
<li :for={link <- @links}>
9
<%= link.url %>
10
<button phx-click={JS.push("delete", value: %{id: link.id})}>Delete</button>
11
</li>
12
</ol>

And then we’ll implement the deletion logic similar to how we did for our previous event hander. In this case note that we reassign the :links property which will update this where it is used in the UI:

lib/phoenixlive_web/live/link_live/index.ex
1
def handle_event("delete", %{"id" => id}, socket) do
2
user_id = socket.assigns.current_user.id
3
4
case Links.delete_link(%Links.Link{id: id}) do
5
{:ok, _link} ->
6
socket = socket
7
|> assign(:links, Links.list_links(user_id))
8
|> put_flash(:info, "Link deleted successfully")
9
10
{:noreply, socket}
11
12
{:error, _changeset} ->
13
socket = socket
14
|> put_flash(:error, "Error deleting link")
15
16
{:noreply, socket}
17
end
18
end

Conclusion

The power of live view comes from us being able to write a fairly small amount of code and handle in a fairly straightforward mannner that let’s us quite quickly go from simple forms to automatic live-updating UI without us having to think about it at all