Phoenix LiveView with Elixir
Updated: 23 December 2025
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:
1mix phx.new phoenixlive --database sqlite3A few important files we want to be aware of specifically related to the LiveView app are:
lib/elixirlive_web/components/layouts/root.html.heexwhich is loaded oncelib/elixirlive_web/components/layouts/app.html.heexwhich 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:
1mix phx.gen.auth Users User usersThen, re-fetch dependencies:
1mix deps.getAnd run migrations
1mix ecto.migrateThen, start the server with:
1mix phx.serverYou 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:
1mix phx.gen.live Links Link links url:textAnd 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:
1scope "/", PhoenixliveWeb do2 pipe_through [:browser, :require_authenticated_user]3
4 live_session :require_authenticated_user,5 on_mount: [{PhoenixliveWeb.UserAuth, :ensure_authenticated}] do6 live "/users/settings", UserSettingsLive, :edit7 live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email8
9 live "/links", LinkLive.Index10 end11endMake sure that your IDE doesn’t auto import anything when adding routes - I got stuck with an annoying
_live_/0 is undefinedissue 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:
lib/phoenixlive_web/live/link_live/show.exlib/phoenixlive_web/live/link_live/show.html.heexlib/phoenixlive_web/live/link_live/form_component.ex
Live Views
LiveView implicitly has some callbacks that we should implement
mounthandle_paramsrender- implicit when we have a template file
We can clear our resource’s index.ex file and add the following:
1defmodule PhoenixliveWeb.LinkLive.Index do2 use PhoenixliveWeb, :live_view3
4 def mount(_params, _session, socket) do5 {:ok, socket}6 end7endThe 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:
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
Listing Links for the Current User
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:
- Add a reference to the user from our Link schema
- Define a migration for adding the user reference to the database table
- Add a way to list links by user
- Get the user from the current route and use that to list the links
- 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:
1defmodule Phoenixlive.Links.Link do2 use Ecto.Schema3 import Ecto.Changeset4
5 schema "links" do6 field :url, :string7
8 belongs_to :user, Phoenixlive.Users.User9
10 timestamps()11 end12
13 @doc false14 def changeset(link, attrs) do15 link16 |> cast(attrs, [:url, :user_id])17 |> validate_required([:url, :user_id])18 end19endAdd a Migration
We can define a migration by generating a new migration with:
1mix ecto.gen.migration add_user_to_linkWhich should generate a migration file into which we add the following:
1defmodule Phoenixlive.Repo.Migrations.AddUserToLink do2 use Ecto.Migration3
4 def change do5 alter table(:links) do6 add :user_id, references(:users, on_delete: :nothing)7 end8 end9endThen we can apply the migration with:
1mix ecto.migrateList Links by User
We can update our list_links function to take a user_id. We can implement it using the Ecto Query Syntax:
1def list_links(user_id) do2 Repo.all(from l in Link, where: l.user_id == ^user_id)3endGet 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:
1defmodule PhoenixliveWeb.LinkLive.Index do2 alias Phoenixlive.Links3 use PhoenixliveWeb, :live_view4
5 def mount(_params, _session, socket) do6 user_id = socket.assigns.current_user.id7
8 socket = socket9 |> assign(:links, Links.list_links(user_id))10
11 {:ok, socket}12 end13endDisplay Links in the HEEX Template
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:
1<h1>Links</h1>2
3<ol>4 <li :for={link <- @links}><%= link.url %>></li>5</ol>Creating a Link
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:
1<h1>Links</h1>2
3<.link navigate={~p"/new"}>4 Add Link5</.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
1defmodule PhoenixliveWeb.LinkLive.New do2 alias Phoenixlive.Links3 use PhoenixliveWeb, :live_view4
5 def mount(_params, _session, socket) do6 changeset = Links.Link.changeset(%Links.Link{})7
8 socket = socket9 |> assign(:form, to_form(changeset))10
11 {:ok, socket}12 end13
14 def handle_event("submit", %{"link" => link_params}, socket) do15 user_id = socket.assigns.current_user.id16
17 params = link_params |> Map.put("user_id", user_id)18
19 case Links.create_link(params) do20 {:ok, _link} ->21 socket = socket22 |> put_flash(:info, "Link created successfully")23 |> push_navigate(to: ~p"/links")24
25 {:noreply, socket}26
27 {:error, changeset} ->28 socket = socket29 |> assign(:form, to_form(changeset))30
31 {:noreply, socket}32 end33 end34endIn 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?: false10>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:
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:
1scope "/", PhoenixliveWeb do2 pipe_through [:browser, :require_authenticated_user]3
4 live_session :require_authenticated_user,5 on_mount: [{PhoenixliveWeb.UserAuth, :ensure_authenticated}] do6 live "/users/settings", UserSettingsLive, :edit7 live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email8
9 live "/links", LinkLive.Index10 live "/links/new", LinkLive.New11 end12endWe 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:
1<h1>Links</h1>2
3<.link navigate={~p"/links/new"}>4 Add Link5</.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:
1def handle_event("delete", %{"id" => id}, socket) do2 user_id = socket.assigns.current_user.id3
4 case Links.delete_link(%Links.Link{id: id}) do5 {:ok, _link} ->6 socket = socket7 |> assign(:links, Links.list_links(user_id))8 |> put_flash(:info, "Link deleted successfully")9
10 {:noreply, socket}11
12 {:error, _changeset} ->13 socket = socket14 |> put_flash(:error, "Error deleting link")15
16 {:noreply, socket}17 end18endConclusion
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