A few weeks ago, I released a pretty basic Phoenix LiveView tutorial where we made a simple counter that you could increment and decrement. Phoenix LiveView is a great solution for anyone who wants to get a real-time app out to the world quickly without spending a ton of time building out both frontend and backend. Today I want to show you just how simple it can be to build something that is a little more useful than a counter: a todo list. Because we’re building it to be database backed, you could use this same pattern for business applications to add real-time functionality without much overhead.

Since this is a tutorial for a bigger project, I’ve created the entire thing on GitHub as well. If you prefer you can see the code there. If you click the Octocat (Github logo) next to any of the section titles, it will take you to the corresponding commit so you can check what you have against the full codebase there.

New LiveView Project

To start out, let’s set up a new project with Phoenix LiveView. You can follow the directions from my last tutorial if you want, but I’d recommend following the official Phoenix LiveView installation instructions. Since the API still isn’t fully figured out, following along there will mean this tutorial will still work a ways into the future. Make sure that when you create your Phoenix project this time, you include Ecto because we’ll be using the database this time. I’m starting with mix phx.new live_view_todos and then I’ll let you go from there. If you’d rather not have to worry about the boilerplate, you can also clone my new project setup from Github.

Generate the Schema

Once you’ve got your basic project up and running with LiveView, we’ll create our table for the todos. Since there are already plenty of great Phoenix tutorials out there, I’m going to skip over how to build out our data schema manually. Let’s just use the Phoenix generator to do that quickly:

mix phx.gen.context Todos Todo todos title:string done:boolean

Then let’s migrate the database, but create it first if you haven’t already:

mix ecto.create && mix ecto.migrate

Great that should be everything we need from the database standpoint, as well as all the functions we’ll need to work with our database. So now let’s create an interface for interacting with our todo list.

Set up the page

I always like to start from the top level when creating a page and work down to the lowest level. So let’s start at the router. You should have a route that looks like this:

get "/", PageController, :index

In the last tutorial, we created a new live route here that pointed directly to a live page. This time though, let’s stick with the page that’s already there and render our live component into it. Our PageController is rendering through our PageView which renders the layout template at lib/live_view_todos_web/templates/layout/app.html.eex. If you open up that layout template, you’ll see that it has a line that looks like this:

<%= render @view_module, @view_template, assigns %>

Phoenix assigned a @view_module to our connection, that is LiveViewTodos.PageView and then in the PageContoller when we call render(conn, "index.html") we’ve assigned live/live_view_todos_web/templates/page/index.html.eex as the @view_template. That means the line above will use the specified view and template to render our page view template inside of the layout template. Now we’re going to take that a step further. Let’s go into our page view template at lib/live_view_todos_web/templates/page/index.html.eex and modify it to render our live view. Replace everything in that file with the following:

<h1>Todo List</h1>
<%= live_render(@conn, LiveViewTodosWeb.TodoLive) %>

So now we’ll have a layout template loading a view template loading a live view. If that seems complicated, you should see some of the React apps we work on at Podium 😃

So now that we’re telling our page to render a live view, let’s go ahead and create it at lib/live_view_todos_web/live/todo_live.ex. We’ll give it the standard mount and render functions:

defmodule LiveViewTodosWeb.TodoLive do use Phoenix.LiveView def mount(_session, socket) do {:ok, socket} end def render(assigns) do ~L"Rendering LiveView" end
end

Now if you visit start your server with mix phx.server and open your browser to http://localhost:4000, you should see “Rendering LiveView” on the page. Awesome! Except now let’s do something with it.

Display the Todo List

On mount, we should load in our todo list so that we can render them. Let’s update your mount function to look like this (make sure you don’t forget the alias):

alias LiveViewTodos.Todos def mount(_session, socket) do {:ok, assign(socket, todos: Todos.list_todos())}
end

Now we’ll have the list of todos, but we aren’t actually displaying them. Let’s create a LiveEEx file to do that. We’ll first want a view in lib/live_view_todos_web/views/todo_view.ex that looks like this:

defmodule LiveViewTodosWeb.TodoView do use LiveViewTodosWeb, :view
end

Then let’s create our template at lib/live_view_todos_web/templates/todo/todos.html.leex. We’ll just display the list of todos in the database:

<%= for todo <- @todos do %>
<div><%= todo.title %></div>
<% end %>

Go ahead and update our render function in the Live View to use our new template:

alias LiveViewTodosWeb.TodoView
...
def render(assigns) do TodoView.render("todos.html", assigns)
end

Now if you refresh your page, you’ll see that that it’s blank below the header. That’s because there aren’t currently any todos to display. So let’s make it so they can be created.

Create a Todo

Let’s start by adding a new form to the top of our template:

<form action="#" phx-submit="add"> <%= text_input :todo, :title, placeholder: "What do you want to get done?" %> <%= submit "Add", phx_disable_with: "Adding..." %>
</form>

There’s a few things to break down there. The first is the phx-submit in the <form> element. This means that when the form is submitted, it will send an add action to the server that contains the contents of the form. Another cool thing about LiveView is the phx_disable_with attribute we’re putting on the submit button. This means that while the form is submitting, it will disable the button and change its text to Adding... so that the user won’t submit it more than once. If you try clicking that button now, you’ll see we get an error:

** (UndefinedFunctionError) function LiveViewTodosWeb.TodoLive.handle_event/3 is undefined or private (live_view_todos) LiveViewTodosWeb.TodoLive.handle_event("add", %{"todo" => %{"title" => "Some task"}}, %Phoenix.LiveView.Socket{...}) ...

That’s because we haven’t set up a handler for the form. We can use the second line of the stacktrace to set up the new function in our Live View. Let’s create a new handle_event function that’s watching for an add event, and then takes the todo information that’s passed in and creates a todo. Then we’ll tell it to load the list of todos again so that the list will be re-rendered. Since we’re already doing that in mount, let’s refactor that into a private function called fetch:

def mount(_session, socket) do {:ok, fetch(socket)}
end def handle_event("add", %{"todo" => todo}, socket) do Todos.create_todo(todo) {:noreply, fetch(socket)}
end defp fetch(socket) do assign(socket, todos: Todos.list_todos())
end

Now if you type a new title into the input and hit enter or click the Add button, you’ll see that it shows up in the list below! What’s cooler than a list that updates itself without us writing any Javascript? Well let’s make it work across multiple sessions so that if you and I are both looking at the same todo list, you can add a new item and it will show up for us both.

While Phoenix LiveView is the star of the show in this tutorial, its older brother Phoenix PubSub is doing a lot of the heavy lifting behind the scenes. In case you haven’t used Phoenix PubSub before, it allows you to set up subscriptions in your app to listen to topics and handle events when they occur. Since the process that manages the LiveView components won’t necessarily be the process updating the todo list in the database, we’ll want to have the LiveView process subscribe to updates to the todos table.

Phoenix PubSub comes bundled in Phoenix by default, so all we have to do is use it. To start off, let’s add a function in lib/live_view_todos/todos.ex to enable subscribing. We’ll use the module name as our PubSub topic:

@topic inspect(__MODULE__) def subscribe do Phoenix.PubSub.subscribe(LiveViewTodos.PubSub, @topic)
end

Let’s go ahead and add a call to that function into the mount function of our Live View so that we’re subscribing on mount:

def mount(_session, socket) do Todos.subscribe() {:ok, fetch(socket)}
end

Now that we’re subscribed, we’ll need to handle the events that come in. This can be done by implementing a handle_info function that looks like this:

def handle_info({Todos, [:todo | _], _}, socket) do {:noreply, fetch(socket)}
end

Notice we’re receiving an event from the Todos topic since we used the module name for the topic name when we created it. Then we’re expecting an event key that has :todo as the first element in a list. If you want to get fancy, you could try diffing the todos yourself. With the intent of keep this tutorial as simple as possible, I’m going to stick to just loading them from the database each time.

Now that we’re subscribing and handling events, we need to actually broadcast them. Let’s go back to our Todos context and create a new function that will broadcast whenever we make a change:

defp broadcast_change({:ok, result}, event) do Phoenix.PubSub.broadcast(LiveViewTodos.PubSub, @topic, {__MODULE__, event, result}) {:ok, result}
end

And to make this all work, let’s modify all of the functions in our context that will mutate data in our database so that they will call our new broadcast_change function:

def create_todo(attrs \\ %{}) do %Todo{} |> Todo.changeset(attrs) |> Repo.insert() |> broadcast_change([:todo, :created])
end
...
def update_todo(%Todo{} = todo, attrs) do todo |> Todo.changeset(attrs) |> Repo.update() |> broadcast_change([:todo, :updated])
end
...
def delete_todo(%Todo{} = todo) do todo |> Repo.delete() |> broadcast_change([:todo, :deleted])
end

And that’s all there is to it! Now if you open the app in a second window, you can add new todos to the list and see that they’re being added in real-time in both windows. Any time there is a change in the Todos context, it’s being broadcasted out. Our live view listens to those changes, and then sends the update down to the client in real-time. I don’t know if there has ever been a way to build something like this with so little effort.

Marking Items as Done

Now that we’re properly creating and reading our list of todos, let’s try updating. Since it’s a todo list, we really ought to be able to mark items as done right? To start with let’s modify our LiveEEx template to show a checkbox next to each todo item. Let’s swap out this line:

<div><%= todo.title %></div>

for this:

<div> <%= checkbox(:todo, :done, value: todo.done) %> <%= todo.title %>
</div>

You’ll notice that we specified that the value will be handled by todo.done. This will now display whether or not an item is done. Try toggling one or two via psql (UPDATE todos SET done = TRUE WHERE id = 1;) and refresh the page and you’ll see that its box will get checked. But now we want to be able to modify those values using the checkboxes. Let’s modify that checkbox line in our template to add some more attributes:

<%= checkbox(:todo, :done, phx_click: "toggle_done", phx_value: todo.id, value: todo.done) %>

Now we’re adding a phx_click as well as a phx_value attribute. You may remember this from my last tutorial, but in case you don’t, phx_click adds a click handler to an item so that when it’s clicked, an event will be sent to the server. I didn’t cover phx_value before though. It will allow you to specify the value that gets sent with the event. To see this in action, let’s go ahead and save our new template and then click on one of our checkboxes. You should see an error like this in your terminal where your application is running:

** (FunctionClauseError) no function clause matching in LiveViewTodosWeb.TodoLive.handle_event/3 (live_view_todos) lib/live_view_todos_web/live/todo_live/index.ex:17: LiveViewTodosWeb.TodoLive.handle_event("toggle_done", "2", %Phoenix.LiveView.Socket{...})

If you look at that second line, you’ll see that it’s trying to call LiveViewTodosWeb.TodoLive.handle_event and pass the parameters "toggle_done", "2" (or whatever the ID of the task you clicked on is), and the socket. Let’s go ahead and implement a handler for this in our Live View:

def handle_event("toggle_done", id, socket) do todo = Todos.get_todo!(id) Todos.update_todo(todo, %{done: !todo.done}) {:noreply, fetch(socket)}
end

We’re doing a few things there. First we’re using the ID we get from the phx_value attribute and getting the todo from the database. Then we’re updating that todo and setting its done property to the opposite of whatever it was before (if it was false, it’s now true, and vice versa). Then you’ll notice that we’re reloading the todos from the database and sending them back down to our application. One more thing though: our PubSub subscription should already be telling our socket that we need to fetch all the new todos, so we don’t need to do that here. Let’s go ahead and remove the call to fetch from our new event handler. We can do the same with our add event too:

Now if you try checking (and unchecking) a few boxes, you’ll see that they update in real-time, even across multiple browser windows!

A todo list that uses Phoenix LiveView to manage state in the backend and update in real-time for multiple simultaneous users

Well there you have it! A simple todo list with Phoenix LiveView that didn’t take too long to implement. Like I said before, LiveView is awesome, but Phoenix PubSub is pretty amazing too. It was super simple for us to subscribe to events and publish those when we update our database, and that allowed us to get this all set up really easily. I would highly recommend spending some time getting more familiar with PubSub because it can be a very powerful tool in your arsenal as you build real-time applications.

If you want to keep going with this project, the next thing I would recommend is adding a delete functionality. The Todos context already has a delete_todo function that you can make use of, so you can probably use what we just did with the checkbox as a template. If you do anything further, please reach out to me on Twitter and let me know what you did. I’d love to see the things you’re building as a result of these tutorials.

If you liked this, please retweet it so others can find it and subscribe to my YouTube channel so you get notified of my next post. I plan to continue releasing new YouTube videos with accompanying blog posts at least once a month (usually much more frequently than that). Let me know in the comments for the video above if there is anything further you’d like to see me cover. Thanks for reading!