LiveView TodoMVC - Part 1: templates and events
Tags: phoenix, elixir, liveview, tutorial
Published: 2019-10-20 13:59:27

---
title: LiveView TodoMVC - Part 1: templates and events
published: true
description: How to build the canonical TodoMVC with (almost) no JavaScript
tags: phoenix,elixir,liveview,tutorial
---

Update: [part two](https://dev.to/amencarini/liveview-todomvc-part-2-params-and-hooks-2l5) is also out 😉

The canonical way to judge a JavaScript framework is its implementation of [TodoMVC](http://todomvc.com/), a simple app to manage a todo list.
Phoenix LiveView promise is to create interactive webapps without having to writing (almost) any JavaScript. I decided to give it a try and check if that promise holds up.

This guide assumes a certain familiarity with Elixir and Phoenix; if you're new to them and want to follow along, feel free to do so and to ask for clarifications in the comments or by DMing me!

You can follow along the commits of [this repo](https://github.com/amencarini/live_view_todo_mvc).

# Create the app

You can check out a guide on [how to install Phoenix](https://hexdocs.pm/phoenix/installation.html#content) if needed.

Once you have Phoenix installed, create the app:

```
mix phx.new todo_mvc --module TodoMVC --no-ecto
```

We're not going to persist the todos, so we'll not use Ecto.

Follow [the instructions to make the app work with LiveView](https://github.com/phoenixframework/phoenix_live_view/tree/v0.3.1#installation) (skip the CSS bit).

Now you can prepare the layout.
Take [the base HTML of a TodoMVC app](https://github.com/tastejs/todomvc/blob/master/examples/vanillajs/index.html) and place it in `lib/todo_mvc_web/templates/layout/app.html.eex`

Add the base CSS by adding its dependency to `assets/package.json`

```json
"dependencies": {
  "todomvc-app-css": "^2.0.1"
}
```

Run:

```
npm install --prefix assets
```

And finally add the CSS to the pipeline:

```
/* assets/css/app.css */
@import "../node_modules/todomvc-app-css/index.css";
```

Let's prepare the `main` template you'll be working on:

```elixir
# lib/todo_mvc_web/views/main_view.ex
defmodule TodoMVCWeb.MainView do
  use TodoMVCWeb, :view
end
```

From the layout, move `<section>` to `lib/todo_mvc_web/templates/main/index.html.leex` and replace it with

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

N.B.: the `.leex` extension will make sure that the contents can be updated dynamically when the state associated with a live view changes

Finally, set up the protagonist of this tutorial, the live view:

```
# lib/todo_mvc_web/live/main_live.ex
defmodule TodoMVCWeb.MainLive do
  use Phoenix.LiveView

  def render(assigns) do
    Phoenix.View.render(TodoMVCWeb.MainView, "index.html", assigns)
  end
end
```

Amend the main `get "/"` route so that the live view is used instead

```elixir
# lib/todo_mvc_web/router.ex
live "/", MainLive
```

Now you're ready to tackle the more interesting bits!

# Add a todo

Throughout this tutorial, we'll need a way to track what todo we'll be actioning. We'll use uuids for this.
Install the `UUID` module as a dependency in `mix.exs`

```elixir
{:uuid, "~> 1.1"}
```

Let's start by creating a struct for todos. We'll use this struct extensively!

```elixir
# lib/todo_mvc/todo.ex
defmodule TodoMVC.Todo do
  @enforce_keys [:id, :text, :state]
  defstruct [:id, :text, :state, :editing]

  def new(text) do
    %__MODULE__{id: UUID.uuid4(), text: text, state: "active"}
  end
end
```

Add the `alias` call, and `mount` and `handle_event` functions to the live view

```elixir
defmodule TodoMVCWeb.MainLive do
  use Phoenix.LiveView

  alias TodoMVC.Todo

  def render(assigns) do
    Phoenix.View.render(TodoMVCWeb.MainView, "index.html", assigns)
  end

  def mount(_params, socket) do
    {:ok, assign(socket, todos: [Todo.new("test!")])}
  end

  # We don't want to add a todo if the text is empty
  def handle_event("add-todo", %{"text" => ""}, socket) do
    {:noreply, socket}
  end

  def handle_event("add-todo", %{"text" => text}, socket) do
    todos = socket.assigns[:todos] ++ [Todo.new(text)]

    {:noreply, assign(socket, todos: todos)}
  end
end
```

Expand the `ul` in `lib/todo_mvc_web/templates/main/index.html.leex` to accomodate the todos:

```eex
<ul class="todo-list">
  <%= for todo <- @todos do %>
    <%= content_tag :li do %>
      <div class="view">
        <%= content_tag :input,
          nil,
          type: "checkbox",
          class: "toggle"
        %>
        <label>
          <%= todo.text %>
        </label>
        <button class="destroy"></button>
      </div>
    <% end %>
  <% end %>
</ul>
```

The page should now look like this:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/re73q756t7z9qi4w4pox.png)

Replace the `class="new-todo"` input with this small form

```eex
<form phx-submit="add-todo">
  <input name="text" class="new-todo" placeholder="What needs to be done?" autofocus>
</form>
```

Now, when you type text in the "What needs to be done?" box and press Enter, a new todo will appear at the bottom of the list!

# Delete a todo

Remember that we added an `id` field to the todos? This will come in handy to remove them selectively off the list.

Modify the `button` with `class="destroy"` like this:

```eex
<button class="destroy" phx-click="destroy" phx-value-todo-id="<%= todo.id %>"></button>
```

We now need a matching event in the live view

```elixir
# lib/todo_mvc_web/live/main_live.ex
def handle_event("destroy", %{"todo-id" => id}, socket) do
  todos = socket.assigns[:todos] |> Enum.reject(fn t -> t.id == id end)

  {:noreply, assign(socket, todos: todos)}
end
```

As we're simply persisting the todos in the live view state, we'll just recreate the todos list by omitting the deleted todo.

# Complete a todo

Now, for something a bit more complex!

First of all, define the behaviour to change state of a todo in the struct module

```elixir
# lib/todo_mvc/todo.ex
def toggle(%__MODULE__{state: "active"} = todo), do: complete(todo)
def toggle(%__MODULE__{state: "completed"} = todo), do: activate(todo)

def complete(todo), do: %{todo | state: "completed"}

def activate(todo), do: %{todo | state: "active"}
```

Got to love some pattern matching!

We'll need to add some functionality to the checkbox of the todo view items:

```eex
<%= content_tag :input,
  nil,
  type: "checkbox",
  class: "toggle",
  phx_click: "toggle",
  phx_value_todo_id: todo.id,
  checked: if todo.state == "completed", do: "checked"
%>
```

Tie up the relevant event in the live view:

```elixir
# lib/todo_mvc_web/live/main_live.ex
def handle_event("toggle", %{"todo-id" => id}, socket) do
  toggle = fn
    %Todo{id: ^id} = todo -> Todo.toggle(todo)
    todo -> todo
  end

  todos = socket.assigns[:todos] |> Enum.map(toggle)

  {:noreply, assign(socket, todos: todos)}
end
```

This won't strikethrough the todo when completed. To achieve that, we need to add a class to the `<li>` that contains the todo:

```eex
<%= content_tag :li, class: todo_classes(todo) do %>
```

You can define the helper function in the main view for convenience:

```elixir
# lib/todo_mvc_web/views/main_view.ex
alias TodoMVC.Todo

def todo_classes(%Todo{state: "completed"}), do: "completed"
def todo_classes(_), do: ""
```

Et voilà! 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/fbqzhjjb0cd98c3tk24x.png)

# Toggle all todos

First, we'll change the little chevron at the top to carry more information about our current state, and only show it if we actually have todos in the list:

```eex
<%= if Enum.any?(@todos) do %>
  <%= content_tag :input,
    nil,
    type: "checkbox",
    id: "toggle-all",
    class: "toggle-all",
    name: "toggle-all",
    phx_click: "toggle-all",
    phx_value_checked: all_todos_completed?(@todos) |> to_string(),
    checked: if all_todos_completed?(@todos), do: "checked"
  %>
  <label for="toggle-all">Mark all as complete</label>
<% end %>
```

Again, let's define a helper function in the view file to check whether all todos are completed:

```elixir
# lib/todo_mvc_web/views/main_view.ex
def all_todos_completed?(todos) do
  !Enum.any?(todos, fn t -> t.state == "active" end)
end
```

Now, we'll be passing a value `checked` (see `phx_value_checked` above) to the live view to decide what to do with our items. We need to cast the boolean into a string, otherwise on a value of `false` the `phx-value-checked` attribute won't be printed in the HTML.

Add the associated live view event:

```elixir
# lib/todo_mvc_web/live/main_live.ex
def handle_event("toggle-all", %{"checked" => "false"}, socket) do
  todos = socket.assigns[:todos] |> Enum.map(&Todo.complete/1)

  {:noreply, assign(socket, todos: todos)}
end

def handle_event("toggle-all", _params, socket) do
  todos = socket.assigns[:todos] |> Enum.map(&Todo.activate/1)

  {:noreply, assign(socket, todos: todos)}
end
```

Click events will pass a host of other information (e.g.: where in the page the click occurred). The only think we're interested in is the `checked` value, so we can ignore the rest.

# Clear all completed

Change the "Clear completed" button to show only if at least a todo is completed, and wire it up with a live view event:

```eex
<%= if Enum.any?(@todos, fn t -> t.state == "completed" end) do %>
  <button class="clear-completed" phx-click="clear-completed">
    Clear completed
  </button>
<% end %>
``` 

And this is the corresponding event handler:

```elixir
# lib/todo_mvc_web/live/main_live.ex
def handle_event("clear-completed", _params, socket) do
  todos = socket.assigns[:todos] |> Enum.reject(fn t -> t.state == "completed" end)

  {:noreply, assign(socket, todos: todos)}
end
```

# Hide the footer

Last thing for part one: hide the footer if there are no todos. All you need to do is to wrap the `footer` element in:

```eex
<%= if Enum.any?(@todos) do %>
  ... footer here ...
<% end %>
```

Let's also remove the sample todo in the live view:

```elixir
def mount(_params, socket) do
  {:ok, assign(socket, todos: [])}
end
```

That's it for part one!
In [part two](https://dev.to/amencarini/liveview-todomvc-part-2-params-and-hooks-2l5) we'll explore the `handle_params` function, which allows us to get information from the URL we're visiting, and hooks, which we'll use for some JavaScript interoperability. Stay tuned!