# phoenix-liveview-todo-list-tutorial
**Repository Path**: mirrors_dwyl/phoenix-liveview-todo-list-tutorial
## Basic Information
- **Project Name**: phoenix-liveview-todo-list-tutorial
- **Description**: โ Beginners tutorial building a Realtime Todo List in Phoenix 1.6.10 + LiveView 0.17.10 โก๏ธ Feedback very welcome!
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-08-08
- **Last Updated**: 2026-01-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Phoenix LiveView Todo List Tutorial

[](https://codecov.io/github/dwyl/phoenix-liveview-todo-list-tutorial?branch=master)
[](https://hex.pm/packages/phoenix_live_view)
[](https://github.com/dwyl/phoenix-liveview-todo-list-tutorial/issues)
[](https://hits.dwyl.io/dwyl/phoenix-liveview-todo-list-tutorial)
**Build your _second_ App** using **Phoenix LiveView**
and _understand_ how to build real-world apps in **20 minutes** or _less_!
- [Phoenix LiveView Todo List Tutorial](#phoenix-liveview-todo-list-tutorial)
- [Why? ๐คท](#why-)
- [What? ๐ญ](#what-)
- [Who? ๐ค](#who-)
- [Prerequisites: What you Need _Before_ You Start ๐](#prerequisites-what-you-need-before-you-start-)
- [How? ๐ป](#how-)
- [Step 0: Run the _Finished_ Todo App on your `localhost` ๐โ](#step-0-run-the-finished-todo-app-on-your-localhost-)
- [Clone the Repository](#clone-the-repository)
- [_Download_ the Dependencies](#download-the-dependencies)
- [_Run_ the App](#run-the-app)
- [Step 1: Create the App ๐](#step-1-create-the-app-)
- [Checkpoint 1a: _Run_ the _Tests_!](#checkpoint-1a-run-the-tests)
- [Checkpoint 1b: _Run_ the New Phoenix App!](#checkpoint-1b-run-the-new-phoenix-app)
- [2. Create the TodoMVC UI/UX](#2-create-the-todomvc-uiux)
- [2.1 Create live folder](#21-create-live-folder)
- [2.2 Update the Root Layout](#22-update-the-root-layout)
- [2.3 Create the page_live layout](#23-create-the-page_live-layout)
- [2.4 Update Router and controller](#24-update-router-and-controller)
- [2.5 Save the TodoMVC CSS to `/assets/css`](#25-save-the-todomvc-css-to-assetscss)
- [2.6 Import the `todomvc-app.css` in `app.scss`](#26-import-the-todomvc-appcss-in-appscss)
- [2.7 Update the test](#27-update-the-test)
- [3. Create the Todo List `items` Schema](#3-create-the-todo-list-items-schema)
- [3.1 Add Aliases to `item.ex`](#31-add-aliases-to-itemex)
- [3.2 Create Todo Item CRUD Tests](#32-create-todo-item-crud-tests)
- [3.3 Make the CRUD Tests _Pass_](#33-make-the-crud-tests-pass)
- [4. Handle Todo List `Item` Creation](#4-handle-todo-list-item-creation)
- [5. _Show_ the Created Todo `Items`](#5-show-the-created-todo-items)
- [6. Toggle the State of Todo Items](#6-toggle-the-state-of-todo-items)
- [7. "Delete" a Todo `item`](#7-delete-a-todo-item)
- [8. Editing Todo `item.text`](#8-editing-todo-itemtext)
- [UI enhancement](#ui-enhancement)
- [9. Footer Navigation](#9-footer-navigation)
- [10. Clear Completed](#10-clear-completed)
- [11. Live Components](#10-liveview-components)
- [12. Deploy to Heroku](#11-deploy-to-heroku)
- [`tl;dr`](#tldr)
# Why? ๐คท
`Phoenix` is already an awesome web framework
that helps teams build reliable Apps & APIs fast.
`LiveView` takes the simplicity of building realtime features
to the next level of elegance and simplicity.
`LiveView` lets us create a slick single-page app
with a **native** (_no lag or refresh_) experience
without writing `JavaScript`.
# What? ๐ญ
This tutorial builds a Todo List from scratch
using Phoenix LiveView in _less_ than 20 minutes.
# Who? ๐ค
This tutorial is aimed at LiveView beginners
who want to _understand_ how everything works
using a familiar UI.
If you are completely new to Phoenix and LiveView,
we recommend you follow the **LiveView _Counter_ Tutorial**:
https://github.com/dwyl/phoenix-liveview-counter-tutorial
## Prerequisites: What you Need _Before_ You Start ๐
This tutorial expects you have a `Elixir`, `Phoenix` and `Node.js` installed.
If you don't already have them on your computer,
please see:
https://github.com/dwyl/learn-elixir#installation
and
https://hexdocs.pm/phoenix/installation.html#phoenix
# How? ๐ป
> ๐ก You can also try the version deployed to Heroku:
> https://live-view-todo.herokuapp.com/
## Step 0: Run the _Finished_ Todo App on your `localhost` ๐โ
Before you attempt to _build_ the todo list app,
we suggest that you clone and _run_
the complete app on your `localhost`.
That way you _know_ it's working
without much effort/time expended.
### Clone the Repository
On your `localhost`,
run the following command to clone the repo
and change into the directory:
```sh
git clone https://github.com/dwyl/phoenix-liveview-todo-list-tutorial.git
cd phoenix-liveview-todo-list-tutorial
```
### _Download_ the Dependencies
Install the dependencies by running the command:
```sh
mix setup
```
It will take a few seconds to download the dependencies
depending on the speed of your internet connection;
be
[patient](https://user-images.githubusercontent.com/194400/76169853-58139380-6174-11ea-8e03-4011815758d0.png).
๐
### _Run_ the App
Start the Phoenix server by running the command:
```sh
mix phx.server
```
Now you can visit
[`localhost:4000`](http://localhost:4000)
in your web browser.
> ๐ก Open a _second_ browser window (_e.g. incognito mode_),
> you will see the the counter updating in both places like magic!
You should expect to see:
With the _finished_ version of the App running on your machine
and a clear picture of where we are headed, it's time to _build_ it!
## Step 1: Create the App ๐
In your terminal run the following `mix` command
to generate the new Phoenix app:
```sh
mix phx.new live_view_todo
```
This command will setup the dependencies (including the liveView dependencies)
and boilerplate for us to get going as fast as possible.
When you see the following prompt in your terminal:
```sh
Fetch and install dependencies? [Yn]
```
Type Y followed by the Enter key.
That will download all the necessary dependencies.
### Checkpoint 1a: _Run_ the _Tests_!
In your terminal, go into the newly created app folder using:
```sh
cd live_view_todo
```
And then run the following `mix` command:
```sh
mix test
```
After the application is compiled you should see:
```
...
Finished in 0.1 seconds (0.08s async, 0.05s sync)
3 tests, 0 failures
```
Tests all pass.
This is _expected_ with a new app.
It's a good way to confirm everything is working.
### Checkpoint 1b: _Run_ the New Phoenix App!
Run the server by executing this command:
```sh
mix phx.server
```
Visit
[`localhost:4000`](http://localhost:4000)
in your web browser.

> ๐ Snapshot of code at the end of Step 1:
> [`#25ba4e7`](https://github.com/dwyl/phoenix-liveview-todo-list-tutorial/commit/25ba4e75cee1dd038fff71aa1ba4b17330d692c9)
## 2. Create the TodoMVC UI/UX
As we saw in the previous step, our App looks like a fresh Phoenix App.
Let's make it look like a todo list.
### 2.1 Create live folder
By convention Phoenix uses a `live` folder to manage the LiveView files.
Create this folder at `lib/live_view_todo_web/live`.
Next we can create the `PageLive` controller module. Create the
`lib/live_view_todo_web/live/page_live.ex` and add the following content:
```elixir
defmodule LiveViewTodoWeb.PageLive do
use LiveViewTodoWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
end
```
When using LiveView, the controller is required to implement
the [`mount`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:mount/3) function,
the entry point of the live page.
### 2.2 Update the Root Layout
Open the `lib/live_view_todo_web/templates/layout/root.html.heex` file
and remove the `` section
such that the contents file is the following:
```html
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "LiveViewTodo", suffix: " ยท Phoenix Framework" %>
<%= @inner_content %>
```
### 2.3 Create the page_live layout
Create the `lib/live_view_todo_web/live/page_live.html.heex` layout file and
add the following content:
```html
todos
```
> **Note**: we borrowed this code from:
> https://github.com/dwyl/phoenix-todo-list-tutorial#3-create-the-todomvc-uiux
> our `Phoenix` (_without `LiveView`_) Todo List Tutorial.
### 2.4 Update Router and controller
in `lib/live_view_todo_web/router.ex` file
change `get` to `live` and rename the controller
`PageController` to `PageLive`
from:
```elixir
scope "/", LiveViewTodoWeb do
pipe_through :browser
get "/", PageController, :index
end
```
to:
```elixir
scope "/", LiveViewTodoWeb do
pipe_through :browser
live "/", PageLive
end
```
If you attempt to run the app now
`mix phx.server` and visit
[http://localhost:4000](http://localhost:4000)
You will see this (_without the TodoMVC `CSS`_):

That's obviously not what we want,
so let's get the TodoMVC `CSS`
and save it in our project!
## 2.5 Save the TodoMVC CSS to `/assets/css`
Visit
https://todomvc.com/examples/vanillajs/node_modules/todomvc-app-css/index.css
and save the file to `/assets/css/todomvc-app.css`
e.g:
[`/assets/css/todomvc-app.css`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/65bec23b92307527a414f77b667b29ea10619e5a/assets/css/todomvc-app.css)
## 2.6 Import the `todomvc-app.css` in `app.scss`
Open the `assets/css/app.scss` file and replace it with the following:
```css
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css";
```
We also commented out the line
`@import "./phoenix.css";`
because we don't want the Phoenix (Milligram) styles
_conflicting_ with the TodoMVC ones.
At the end of this step,
if you run your Phoenix App with
`mix phx.server`
and visit:
[http://localhost:4000](http://localhost:4000)
you should see the following:

Now that we have the layout looking like we want it,
we can move onto the fun part of making it _work_.
## 2.7 Update the test
Now that we have a functioning LiveView page, let's create the tests under
`test/live_view_todo_web/live` folder. Create the file
`test/live_view_todo_web/live/page_live_test.exs` and add the following:
```elixir
defmodule LiveViewTodoWeb.PageLiveTest do
use LiveViewTodoWeb.ConnCase
import Phoenix.LiveViewTest
test "disconnected and connected mount", %{conn: conn} do
{:ok, page_live, disconnected_html} = live(conn, "/")
assert disconnected_html =~ "Todo"
assert render(page_live) =~ "What needs to be done"
end
end
```
and delete the `test/live_view_todo_web/controllers/page_controller_test.exs` file.
Now when you re-run the tests:
```sh
mix test
```
You should see:
```sh
Compiling 1 file (.ex)
...
Finished in 0.2 seconds
3 tests, 0 failures
```
Everything passing, lets get back to building!
## 3. Create the Todo List `items` Schema
In order to _store_ the todo list `items` we need a schema.
In your terminal run the following generator command:
```sh
mix phx.gen.schema Item items text:string person_id:integer status:integer
```
That will create two new files:
- `lib/live_view_todo/item.ex` - the schema
- `priv/repo/migrations/20201227070700_create_items.exs` - migration file (creates database table)
Open the migration file to add a default value to `status`:
```elixir
add :status, :integer, default: 0 # add default value 0
```
Reference:
https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Schema.html
Execute the migration file by running the following command:
```sh
mix ecto.migrate
```
You will see output similar to the following:
```sh
13:44:03.406 [info] == Migrated 20170606070700 in 0.0s
```
Now that the schema has been created
we can write some code
to make the todo list functionality work.
### 3.1 Add Aliases to `item.ex`
Before we create any new functions, let's open the
`lib/live_view_todo/item.ex`
file and make a couple of changes:
```elixir
defmodule LiveViewTodo.Item do
use Ecto.Schema
import Ecto.Changeset
schema "items" do
field :person_id, :integer
field :status, :integer
field :text, :string
timestamps()
end
@doc false
def changeset(item, attrs) do
item
|> cast(attrs, [:text, :person_id, :status])
|> validate_required([:text, :person_id, :status])
end
end
```
First add the line `alias LiveViewTodo.Repo`
below the `import Ecto.Changeset` statement;
we need this alias so that we can make database queries.
Next add the line `alias __MODULE__` below the `alias` we just added;
this just means "alias the Struct contained in this file so we can reference it".
see: https://stackoverflow.com/questions/39854281/access-struct-inside-module/47501059
Then add the default value for `status` to `0`:
```elixir
field :status, :integer, default: 0
```
Finally remove the `:person_id, :status`
from the List of fields in `validate_required`.
We don't want `person_id` to be required for now
as we don't yet have authentication setup for the App.
Your file should now look like this:
```elixir
defmodule LiveViewTodo.Item do
use Ecto.Schema
import Ecto.Changeset
alias LiveViewTodo.Repo
alias __MODULE__
schema "items" do
field :person_id, :integer
field :status, :integer
field :text, :string
timestamps()
end
@doc false
def changeset(item, attrs) do
item
|> cast(attrs, [:text, :person_id, :status])
|> validate_required([:text])
end
end
```
With those changes made, we can proceed to creating our functions.
### 3.2 Create Todo Item CRUD Tests
The `phx.gen.schema` does not automatically create any
["CRUD"](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)
functions
to `Create` an `item` or `Read` `items` in/from the database
or tests for those functions,
so we need to create them ourselves now.
Create a new directory with the path:
`test/live_view_todo`
and in that new directory,
create a file:
`test/live_view_todo/item_test.exs`
Next _open_ the newly created file
`test/live_view_todo/item_test.exs`
and add the following test code to it:
```elixir
defmodule LiveViewTodo.ItemTest do
use LiveViewTodo.DataCase
alias LiveViewTodo.Item
describe "items" do
@valid_attrs %{text: "some text", person_id: 1, status: 0}
@update_attrs %{text: "some updated text", status: 1}
@invalid_attrs %{text: nil}
def item_fixture(attrs \\ %{}) do
{:ok, item} =
attrs
|> Enum.into(@valid_attrs)
|> Item.create_item()
item
end
test "get_item!/1 returns the item with given id" do
item = item_fixture(@valid_attrs)
assert Item.get_item!(item.id) == item
end
test "create_item/1 with valid data creates a item" do
assert {:ok, %Item{} = item} = Item.create_item(@valid_attrs)
assert item.text == "some text"
inserted_item = List.first(Item.list_items())
assert inserted_item.text == @valid_attrs.text
end
test "create_item/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Item.create_item(@invalid_attrs)
end
test "list_items/0 returns a list of todo items stored in the DB" do
item1 = item_fixture()
item2 = item_fixture()
items = Item.list_items()
assert Enum.member?(items, item1)
assert Enum.member?(items, item2)
end
test "update_item/2 with valid data updates the item" do
item = item_fixture()
assert {:ok, %Item{} = item} = Item.update_item(item, @update_attrs)
assert item.text == "some updated text"
end
end
end
```
Take a moment to _understand_ what is being tested.
Once you have written out (_or let's face it, copy-pasted_) the test code,
save the file and run the tests:
```
mix test test/live_view_todo/item_test.exs
```
Since the functions don't yet exist,
you will see all the test _fail_:
```
1) test items get_item!/1 returns the item with given id (LiveViewTodo.ItemTest)
test/live_view_todo/item_test.exs:19
** (UndefinedFunctionError) function LiveViewTodo.Item.create_item/1 is undefined or private
code: item = item_fixture(@valid_attrs)
stacktrace:
(live_view_todo 0.1.0) LiveViewTodo.Item.create_item(%{person_id: 1, text: "some text"})
test/live_view_todo/item_test.exs:14: LiveViewTodo.ItemTest.item_fixture/1
test/live_view_todo/item_test.exs:20: (test)
etc ...
Finished in 0.2 seconds
5 tests, 5 failures
```
Hopefully these CRUD tests are familiar to you.
If they aren't, please read:
https://hexdocs.pm/phoenix/testing.html
If you still have any doubts, please
[ask a specific question](https://github.com/dwyl/phoenix-liveview-todo-list-tutorial/issues/new).
The focus of this tutorial is `LiveView` not CRUD testing,
the sooner we get to the `LievView` part the better,
this is just the "setup" we need to do for inserting todo item data.
Let's write the functions to make the tests pass!
### 3.3 Make the CRUD Tests _Pass_
Open the `lib/live_view_todo/item.ex` file
and add the following lines of code:
```elixir
@doc """
Creates a item.
## Examples
iex> create_item(%{text: "Learn LiveView"})
{:ok, %Item{}}
iex> create_item(%{text: nil})
{:error, %Ecto.Changeset{}}
"""
def create_item(attrs \\ %{}) do
%Item{}
|> changeset(attrs)
|> Repo.insert()
end
@doc """
Gets a single item.
Raises `Ecto.NoResultsError` if the Item does not exist.
## Examples
iex> get_item!(123)
%Item{}
iex> get_item!(456)
** (Ecto.NoResultsError)
"""
def get_item!(id), do: Repo.get!(Item, id)
@doc """
Returns the list of items.
## Examples
iex> list_items()
[%Item{}, ...]
"""
def list_items do
Repo.all(Item)
end
@doc """
Updates a item.
## Examples
iex> update_item(item, %{field: new_value})
{:ok, %Item{}}
iex> update_item(item, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_item(%Item{} = item, attrs) do
item
|> Item.changeset(attrs)
|> Repo.update()
end
```
After saving the `item.ex` file,
re-run the tests with:
```sh
mix test test/live_view_todo/item_test.exs
```
You should see them pass:
```sh
.....
Finished in 0.2 seconds
5 tests, 0 failures
Randomized with seed 208543
```
Now that we have our CRUD functions written (_and documented+tested_),
we can move on to the _fun_ part, building the Todo App in `LiveView`!
## 4. Handle Todo List `Item` Creation
The first event we want to handle in our `LiveView` App is "create";
the act of creating a new Todo List `item`.
Let's start by adding a _test_ for creating an item.
Open the
`test/live_view_todo_web/live/page_live_test.exs`
file and add the following test:
```elixir
test "connect and create a todo item", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
assert render_submit(view, :create, %{"text" => "Learn Elixir"}) =~ "Learn Elixir"
end
```
Docs for this LiveView testing using `render_submit/1`:
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#render_submit/1
If you attempt to run this test:
```sh
mix test test/live_view_todo_web/live/page_live_test.exs
```
you will see it _fail_:
```sh
1) test connect and create a todo item (LiveViewTodoWeb.PageLiveTest)
test/live_view_todo_web/live/page_live_test.exs:12
** (EXIT from #PID<0.441.0>) an exception was raised:
** (FunctionClauseError) no function clause matching in LiveViewTodoWeb.PageLive.handle_event/3
```
In order to make the test _pass_ we will need to add two blocks of code.
Open the `lib/live_view_todo_web/live/page_live.html.heex` file
and locate the line in the `` section:
```html
```
Replace it with the following:
```html
```
The important part is the `phx-submit="create"`
which tells `LiveView` which event to emit when the form is submitted.
Once you've saved the `page_live.html.leex` file,
open the `lib/live_view_todo_web/live/page_live.ex` file
and under `use LiveViewTodoWeb, :live_view` add
```elixir
alias LiveViewTodo.Item
@topic "live"
```
and the add the following handler code after the `mount` function:
```elixir
@impl true
def handle_event("create", %{"text" => text}, socket) do
Item.create_item(%{text: text})
socket = assign(socket, items: Item.list_items(), active: %Item{})
LiveViewTodoWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
{:noreply, socket}
end
```
The `@topic "live"` is the WebSocket (_Phoenix Channel_) topic
defined as a
[module attribute](https://elixir-lang.org/getting-started/module-attributes.html)
(_like a Global Constant_),
which we will use to both subscribe to and broadcast on.
So the following line:
```elixir
LiveViewTodoWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
```
Will send the "update" event with the `socket.assigns` data
to all the other clients on listening to the @topic.
Now to listen to this message we can define the [handle_info](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_info/2) callback.
Add the following code:
```elixir
@impl true
def handle_info(%{event: "update", payload: %{items: items}}, socket) do
{:noreply, assign(socket, items: items)}
end
```
We are using pattern matching on the first parameter to make sure
the handle_info matches the "update" event. We then assign to the socket
the new list of items.
With that in place you can now create items in the browser!
Run the app: `mix phx.sever` and you should be able to add items.
_However_ they will not _appear_ in the UI.
Let's fix that next.
## 5. _Show_ the Created Todo `Items`
In order to _show_ the Todo `items` we are creating,
we need to:
1. Lookup and assign the `items` in the `mount/3` function
2. Loop through and render the `item` in the `page_live.html.leex` template
Let's start by updating the `mount/3` function in
`/lib/live_view_todo_web/live/page_live.ex`:
```elixir
def mount(_params, _session, socket) do
# subscribe to the channel
if connected?(socket), do: LiveViewTodoWeb.Endpoint.subscribe(@topic)
{:ok, assign(socket, items: Item.list_items())} # add items to assigns
end
```
Then in the
`lib/live_view_todo_web/live/page_live.html.leex` file
replace the code:
```html
```
With the following:
```elixir
<%= for item <- @items do %>
<%= if checked?(item) do %>
<% else %>
<% end %>
<% end %>
```
You will notice that there are two functions
`completed?/1` and `checked?/1`
invoked in that block of template code.
We need to define the functions in
`/lib/live_view_todo_web/live/page_live.ex`:
```elixir
def checked?(item) do
not is_nil(item.status) and item.status > 0
end
def completed?(item) do
if not is_nil(item.status) and item.status > 0, do: "completed", else: ""
end
```
These are convenience functions.
We _could_ have embedded this code directly in the template,
however we prefer to _minimize_ logic in the templates
so that they are easier to read/maintain.
With that template update and helper functions saved,
we can now create and _see_ our created Todo `item`:

## 6. Toggle the State of Todo Items
The next piece of functionality we want in a Todo List
is the ability to **`toggle`** the completion from "todo" to "done".
In our `item` `schema` (created in step 3),
we defined `status` as an `integer`.
The `default` value for `item.status`
when a **new `item`** is inserted is `0`.
Let's create a (_failing_) test for **toggling** items.
Open the
`test/live_view_todo_web/live/page_live_test.exs`
file and add the following test to it:
```elixir
test "toggle an item", %{conn: conn} do
{:ok, item} = Item.create_item(%{"text" => "Learn Elixir"})
assert item.status == 0
{:ok, view, _html} = live(conn, "/")
assert render_click(view, :toggle, %{"id" => item.id, "value" => 1}) =~ "completed"
updated_item = Item.get_item!(item.id)
assert updated_item.status == 1
end
```
Make sure to alias the `Item` structure in your test file:
```elixir
defmodule LiveViewTodoWeb.PageLiveTest do
use LiveViewTodoWeb.ConnCase
import Phoenix.LiveViewTest
alias LiveViewTodo.Item # alias Item here
```
You may have noticed that in the template,
we included an `` with the `type="checkbox"`
```elixir
<%= if checked?(item) do %>
<% else %>
<% end %>
```
These lines of code already has everything we need to enable the **`toggle`** feature
on the front-end, we just need to create a handler in `page_live.ex`
to handle the event.
Open the
`/lib/live_view_todo_web/live/page_live.ex`
file and add the following code to it:
```elixir
@impl true
def handle_event("toggle", data, socket) do
status = if Map.has_key?(data, "value"), do: 1, else: 0
item = Item.get_item!(Map.get(data, "id"))
Item.update_item(item, %{id: item.id, status: status})
socket = assign(socket, items: Item.list_items(), active: %Item{})
LiveViewTodoWeb.Endpoint.broadcast(@topic, "update", socket.assigns)
{:noreply, socket}
end
```
Note that we are using `broadcast/3` instead of `broadcast_from/4` to make
sure the count of items left is updated for the client itself.
Once you've saved the file,
the test will pass.
## 7. "Delete" a Todo `item`
Rather than _permanently_ deleting items which destroys history/accountability,
we prefer to
["_soft deletion_"](https://en.wiktionary.org/wiki/soft_deletion)
which allows people to "undo" the operation.
Open
`test/live_view_todo/item_test.exs`
and add the following test to it:
```elixir
test "delete_item/1 soft-deletes an item" do
item = item_fixture()
assert {:ok, %Item{} = deleted_item} = Item.delete_item(item.id)
assert deleted_item.status == 2
end
```
If you attempt to run the test,
you will see it _fail_:
```sh
1) test items delete_item/1 soft-deltes an item (LiveViewTodo.ItemTest)
test/live_view_todo/item_test.exs:50
** (UndefinedFunctionError) function LiveViewTodo.Item.delete_item/1 is undefined or private
code: assert {:ok, %Item{} = deleted_item} = Item.delete_item(item.id)
stacktrace:
(live_view_todo 0.1.0) LiveViewTodo.Item.delete_item(157)
test/live_view_todo/item_test.exs:52: (test)
```
To make the test _pass_,
open your `lib/live_view_todo/item.ex` file
and add the following function definition:
```elixir
def delete_item(id) do
get_item!(id)
|> Item.changeset(%{status: 2})
|> Repo.update()
end
```
Having defined the `delete/1` function
as updating the `item.status` to **`2`**,
we can now create a test for a `LiveView` handler
that invokes this function.
Open the
`test/live_view_todo_web/live/page_live_test.exs`
file and add the following test to it:
```elixir
test "delete an item", %{conn: conn} do
{:ok, item} = Item.create_item(%{"text" => "Learn Elixir"})
assert item.status == 0
{:ok, view, _html} = live(conn, "/")
assert render_click(view, :delete, %{"id" => item.id})
updated_item = Item.get_item!(item.id)
assert updated_item.status == 2
end
```
To make this test pass,
we need to add the following `handle_event/3` handler to `page_live.ex`:
```elixir
@impl true
def handle_event("delete", data, socket) do
Item.delete_item(Map.get(data, "id"))
socket = assign(socket, items: Item.list_items(), active: %Item{})
LiveViewTodoWeb.Endpoint.broadcast(@topic, "update", socket.assigns)
{:noreply, socket}
end
```
This point we've written a bunch of code,
let's see it in _action_ in the front-end.
Run the Phoenix Sever: `mix phx.server`
and visit
[http://localhost:4000](http://localhost:4000)
in your web browser.
You should see:

## 8. Editing Todo `item.text`
For _editing_ an `item` we'll continue to use `LiveView` and:
- 1. Display the "**edit**" form when an `item` is clicked on
- 2. On submit, `LiveView` will handle the **`update-item`** event to **update** the `item`
First we want to **update** the `html` to display the `form` when an `item` is edited:
update `lib/live_view_todo_web/live/page_live.html.heex` to display the form:
```html
<%= for item <- @items do %>
<%= if item.id == @editing do %>
<% else %>
<%= if checked?(item) do %>
<% else %>
<% end %>
<% end %>
<% end %>
```
For each `item` we check
if the `item.id`
matches the `@editing` value
and we display
either the `form` or the `label` value.
We have added the `phx-click="edit-item"` event on the `label` which is used
to define the `@editing` value:
in `lib/live_view_todo_web/live/page_live.ex` create the logic for `edit-item` event:
```elixir
@impl true
def handle_event("edit-item", data, socket) do
{:noreply, assign(socket, editing: String.to_integer(data["id"]))}
end
```
We assign the `editing` value
to the socket with the `item.id`
defined by
`phx-value-id`.
Finally we can handle the `phx-submit="update-item"` event:
```elixir
@impl true
def handle_event("update-item", %{"id" => item_id, "text" => text}, socket) do
current_item = Item.get_item!(item_id)
Item.update_item(current_item, %{text: text})
items = Item.list_items()
socket = assign(socket, items: items, editing: nil)
LiveViewTodoWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
{:noreply, socket}
end
```
We update the item matching the id with the new text value and broadcast the change
to the other connected clients.
Let's update the tests to make sure the editing feature is covered:
```elixir
test "edit item", %{conn: conn} do
{:ok, item} = Item.create_item(%{"text" => "Learn Elixir"})
{:ok, view, _html} = live(conn, "/")
assert render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) =~
"
<% else %>
<% end %>
<% end %>
"""
end
end
```
We have defined the `render` function which display the list of items.
Note that we have also defined the `attr` function. This tells us that we need
to pass the `:items` attribute when calling our component.
In `lib/live_view_todo_web/live/page_live.html.heex` we can already call our component:
```heex
<.live_component
module={LiveViewTodoWeb.ItemComponent}
id="cpn"
items={@items}
editing={@editing}
/>
```
Now that we have moved the `ul` and `li` tags to the render function we can
directly use `<.live_component/>`. Make sure to define the `module` and `id`.
We can also see that we have the `items` and `editing` attribute too.
Finally we can move the `handle_event` linked to the items in `live_page.ex`
to the `item_component.ex` file:
```elixir
def render(assigns) do
...
end
@impl true
def handle_event("toggle", data, socket) do
status = if Map.has_key?(data, "value"), do: 1, else: 0
item = Item.get_item!(Map.get(data, "id"))
Item.update_item(item, %{id: item.id, status: status})
socket = assign(socket, items: Item.list_items(), active: %Item{})
LiveViewTodoWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
{:noreply, socket}
end
@impl true
def handle_event("edit-item", data, socket) do
{:noreply, assign(socket, editing: String.to_integer(data["id"]))}
end
@impl true
def handle_event("update-item", %{"id" => item_id, "text" => text}, socket) do
current_item = Item.get_item!(item_id)
Item.update_item(current_item, %{text: text})
items = Item.list_items()
socket = assign(socket, items: items, editing: nil)
LiveViewTodoWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
{:noreply, socket}
end
@impl true
def handle_event("delete", data, socket) do
Item.delete_item(Map.get(data, "id"))
socket = assign(socket, items: Item.list_items(), active: %Item{})
LiveViewTodoWeb.Endpoint.broadcast(@topic, "update", socket.assigns)
{:noreply, socket}
end
def checked?(item) do
not is_nil(item.status) and item.status > 0
end
def completed?(item) do
if not is_nil(item.status) and item.status > 0, do: "completed", else: ""
end
```
More documentation:
- https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html
- https://elixirschool.com/blog/live-view-live-component
## 12. Deploy to Heroku
Deployment is beyond the scope of this tutorial.
But we created a _separate_
guide for it:
[elixir-phoenix-app-deployment.md](https://github.com/dwyl/learn-heroku/blob/master/elixir-phoenix-app-deployment.md)
Once you have _deployed_ you will will be able
to view/use your app in any Web/Mobile Browser.
e.g:
https://liveview-todo.herokuapp.com
### `tl;dr`
- [x] Add the build packs
Run the commands:
```
heroku git:remote -a liveview-todo
heroku run "POOL_SIZE=2 mix ecto.migrate"
```