How to Create a Todo List with Phoenix LiveView
Tags: elixir, webdev, tutorial
Published: 2020-11-02 17:03:20

_Update Nov 2, 2020: It's been over a year and a half since I first published this Phoenix LiveView tutorial. Since then a lot has changed with the library. I've now updated the entire tutorial to work with Phoenix 1.5 and Phoenix LiveView 0.14. The new generators will let you get up and running a lot faster. As always [let me know on Twitter](https://twitter.com/dnsbty) if there's anything you don't understand or want me to cover more in depth!_

I previously released a pretty basic [Phoenix LiveView tutorial](https://dennisbeatty.com/how-to-create-a-counter-with-phoenix-live-view.html) 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 computer emoji 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 [💻](https://github.com/dnsbty/live_view_todos/commit/bbfa7f709c8798470c354201cdc67d0a6ab95e55)

To start out, set up a new project with Phoenix LiveView. Thanks to the changes to the generator in Phoenix 1.5, all of the LiveView boilerplate will be added for you when you generate a new project. First, make sure you have the latest version of the generator:

```bash
mix archive.install hex phx_new
```

As of the time of writing, version 1.5.4 is the latest. With it installed, you can create a new project. Including the `live` flag will include all the boilerplate necessary to get LiveView up and running:

```bash
mix phx.new live_view_todos --live
```

Use `Y` to download and install dependencies, and then `cd` into the new project directory and setup its database:

```bash
mix ecto.setup
```

And now you can start your server, and go to [http://localhost:4000](http://localhost:4000) in your browser to see your application running

```bash
mix phx.server
```

## Generate the Schema [💻](https://github.com/dnsbty/live_view_todos/commit/187d6df73c36a6c5928f2ac39dad5dce9ec5f4d3)

With your basic project up and running, you'll want to create the 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. Just use the Phoenix generator to do that quickly:

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

Then migrate your database:

```bash
mix ecto.migrate
```

That should be everything you'll need from the database standpoint, as well as all the functions you'll need to work with your database. Now you'll create an interface for interacting with the todo list.

## Set up the page [💻](https://github.com/dnsbty/live_view_todos/commit/02988abb750f8a3889d446c50ae483420534b157)

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:

```elixir
live "/", PageLive, :index
```

Replace that route with a new one for the todo list:

```elixir
live "/", TodoLive, :index
```

If you try to run your app, you'll see a compilation error because TodoLive doesn't exist yet. Go ahead and create a new file at `lib/live_view_todos_web/live/todo_live.ex`. Implement the standard `mount` callback, and use the `render` callback to render an example string inline:

```elixir
defmodule LiveViewTodosWeb.TodoLive do
  use LiveViewTodosWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def render(assigns) do
    ~L"Rendering LiveView"
  end
end
```

Now if you start your server again with `mix phx.server` and open your browser to [http://localhost:4000](http://localhost:4000), you should see "Rendering LiveView" on the page. Awesome! Except now let's do something with it.

## Display the Todo List [💻](https://github.com/dnsbty/live_view_todos/commit/1c36431c103aa00acc288115e5cbdaf752971ac2)

On mount, you should load in your todo list so that the live view can render them. Update your mount function to look like this (make sure you don't forget the alias):

```elixir
alias LiveViewTodos.Todos

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

Now the live view process will have the list of todos, but it isn't actually displaying them.  Create a LiveEEx file to do that. Add a new template file at `lib/live_view_todos_web/live/todo_live.html.leex`. In it you'll just add code to display the list of todos from the database:

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

Previously you would have needed to also create a view module and change your `render` callback to call the view and tell it to render the template. Thanks to some new Phoenix magic though, a template in the same folder as the live view code that has the same name will be used automatically. For a large project with many live views and templates, this might not be the easiest way to organize things, but because this project is smaller, go ahead and take advantage of it by removing the `render` implementation from your live view module so that only the `mount` callback remains.

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 enable adding new todos to the list.

## Create a Todo [💻](https://github.com/dnsbty/live_view_todos/commit/e97c0007fa544afeaeeb65a60ee5c8ce635a54ce)

To start add a new form to the top of your template:

```elixir
<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` attribute in the `<form>` element. This instructs LiveView to send an `add` action to the server that contains the contents of the form when it is submitted. The `phx_disable_with` attribute on the `submit` button is a nice LiveView feature that will disable the button when the form is submitted and change its text to `Adding...` so that the user won't submit it more than once. If you try clicking that button now, nothing will happen in the browser, and you'll see an error in the console:

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

That's because you haven't set up a handler for the form. You can use the second line of the stacktrace to set up the new function in the live view. 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 you'll tell it to load the list of todos again so that the list will be re-rendered. Since you're already doing that in mount, go ahead and refactor that into a private function called `fetch`:

```elixir
def mount(_params, _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? You can 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.

## Updating in Real-Time with PubSub [💻](https://github.com/dnsbty/live_view_todos/commit/fcee4eb0a5cab66e6f41337a969dc0df5b6202c0)

While Phoenix LiveView is the star of the show in this tutorial, its older sister [Phoenix PubSub](https://hexdocs.pm/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 you have to do is use it. To start off, add a function in `lib/live_view_todos/todos.ex` to enable subscribing. Use the module name as our PubSub topic:

```elixir
@topic inspect(__MODULE__)

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

Now go ahead and add a call to that function into the `mount` callback of your live view so that processes will get subscribed on mount:

```elixir
def mount(_params, _session, socket) do
  Todos.subscribe()

  {:ok, fetch(socket)}
end
```

Now that the process is subscribed, it will need to handle the events that come in. This can be implemented with a `handle_info` callback that looks like this:

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

Notice the callback is matching on an event from the `Todos` topic since you used the module name for the topic name when you created it. Then it's 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 keeping this tutorial as simple as possible, we'll stick to just loading them from the database each time though.

Now that the live views are subscribed to the topic and handling events, you need to actually broadcast events to them. Go back to the `Todos` context and create a new function that will broadcast whenever a change occurs:

```elixir
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, modify all of the functions in the context that will mutate data in the database so that they will call your new `broadcast_change` function:

```elixir
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. The live view processes listen to those changes, and then send 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 [💻](https://github.com/dnsbty/live_view_todos/commit/12812b35a8e47dd8fd2deb7563c49f7a85bbaaae)

Now that you're properly creating and reading the list of todos, let's give updating a shot. Since it's a todo list, you really ought to be able to mark items as done right? To start with, modify the LiveEEx template to show a checkbox next to each todo item. Swap out this line:

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

for this:

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

You'll notice that the checkbox value will be specifically 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; you'll see that its box will get checked. But you want to be able to update those values using the checkboxes. Modify that checkbox line in your template to add some more attributes:

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

Now your checkbox will have 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. Now when the checkbox is clicked, LiveView will send an event to the server. I didn't previously cover `phx_value_*` though. It will allow you to specify values that get sent to the server with the event. To see this in action, go ahead and save your 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:

```elixir
** (FunctionClauseError) no function clause matching in LiveViewTodosWeb.TodoLive.handle_event/3
    (live_view_todos) lib/live_view_todos_web/live/todo_live.ex:11: LiveViewTodosWeb.TodoLive.handle_event("toggle_done", %{...}, #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"`, a map of values including one called `"id"` set to the ID of the task you clicked on, and the socket. Go ahead and implement a handler for this in your live view:

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

A few things are happening there. First the live view uses the ID it receives from the `phx_value_*` attribute and fetches the todo from the database. Then it updates that todo and sets 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 the process reloads the list of todos from the database and sends them back down to the application. One more thing though: the PubSub subscription should already be telling our socket to fetch all the new todos, so you don't need to do that here. Go ahead and remove the call to `fetch` from the new event handler. You can do the same with the `add` event too:

```elixir
{:noreply, socket}
```

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

# What's Next?

![Todo list updating in real-time in side by side browser windows](https://dennisbeatty.com/content/2019-04-24-how-to-create-a-todo-list-with-phoenix-liveview/todos.gif)

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, and Phoenix PubSub is pretty amazing too. It was super simple to subscribe to events and
publish those when you update the database, and that allowed you 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 you just did with the checkbox as a template. If you do anything further, please [reach out to me on Twitter](https://twitter.com/dnsbty) 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](https://twitter.com/intent/retweet?tweet_id=1323283654685020160) and
[subscribe to my YouTube
channel](https://www.youtube.com/channel/UC9l_1J8y_au9Nlgrfz0rduw?sub_confirmation=1)
so you get notified of my next post. I plan to continue releasing new YouTube videos with accompanying blog posts whenever I can make time. Let me know on Twitter or in the comments for [the video above](https://youtu.be/xgz-HTot3R8) if there is anything further you'd like to see me cover. Thanks for reading!