LiveView TodoMVC - Part 2: params and hooks
Tags: phoenix, elixir, liveview, tutorial
Published: 2019-10-21 07:31:51

Welcome back!

In [Part 1](https://dev.to/amencarini/liveview-todomvc-4jin) we built an almost fully functional version of the classic [TodoMCV](http://todomvc.com/) tutorial by simply using Phoenix LiveView.
There are a couple of bits of functionality that we haven't covered: the "Active, Completed, All" filter in the footer, and the ability to double-click an item to edit it.

Let's tackle them!

# Passing parameters to a live view

To make the links in the footer actually do something, you'll need to use the `live_path` function as their destination:

```eex
<ul class="filters">
  <li>
    <%= live_link "All",
      to: Routes.live_path(@socket, TodoMVCWeb.MainLive, %{filter: "all"}),
      class: selected_class(@filter, "all")
    %>
  </li>
  <li>
    <%= live_link "Active",
      to: Routes.live_path(@socket, TodoMVCWeb.MainLive, %{filter: "active"}),
      class: selected_class(@filter, "active")
    %>
  </li>
  <li>
    <%= live_link "Completed",
      to: Routes.live_path(@socket, TodoMVCWeb.MainLive, %{filter: "completed"}),
      class: selected_class(@filter, "completed")
    %>
  </li>
</ul>
```

Again, we're using a helper function to decide whether the link should have the `selected` CSS class: we'll define it in the view, by using a pattern matching trick: 

```elixir
# lib/todo_mvc_web/views/main_view.ex
def selected_class(filter, filter), do: "selected"
def selected_class(_current_filter, _filter), do: ""
```

To break it down: if the first and second argument we're passing to `selected_class/2` are the same string, it'll return the `"selected"` string; an empty string will be returned otherwise.

We're now referring to a `@filter` assign in the template, and we'll need to initialise it in the live view for the app to compile. Change the `mount` function:

```elixir
# lib/todo_mvc_web/live/main_live.ex
def mount(_params, socket) do
  {:ok, assign(socket, todos: [], filter: "all")}
end
```

Now: how do we capture the value that's passed by query string? We need a `handle_params` callback in the live view!

```elixir
# lib/todo_mvc_web/live/main_live.ex
def handle_params(%{"filter" => filter}, _uri, socket) do
  {:noreply, assign(socket, filter: filter)}
end

def handle_params(_params, _uri, socket) do
  {:noreply, socket}
end
```

This will be enough to get the selected filter to be highlighted. But what about _actually_ filtering the todos? We'll use the template and its view for this.

# Filter the todos

Start by defining a helper function that given a todo and our current filter will say whether a todo should be visible:

```elixir
def todo_visible?(_todo, "all"), do: true
def todo_visible?(%{state: state}, state), do: true
def todo_visible?(_, _), do: false
```

Again, we use some pattern matching magic: if the filter is set to `"all"`, we're sure the todo needs to be visible; if the filter is set to `"active"` and so is the state of the todo, show it; otherwise hide the todo.

Now, an elegant way to incorporate this change is to use a comprehension filter! In the template, change the start of the `ul` like this:

```eex
<ul class="todo-list">
  <%= for todo <- @todos, todo_visible?(todo, @filter) do %>
    ...
```

And that's it! You can now click around and only see the todos in the desired state.

# Edit a todo

Phoenix LiveView does not (currently?) support binding an event to a double-click. To deal with this, we'll have to write a live view hook and... some JavaScript!

We'll start by defining the hook on the `li` that hosts the todo:

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

Also add this at the bottom of the `li`:

```eex
<form phx-change="change" phx-submit="change">
  <input
    class="edit"
    name="title"
    phx-value-todo-id="<%= todo.id %>"
    phx-blur="stop-editing"
    value="<%= todo.text %>"
  >
</form>
```

We'll use the form to actually capture the changes to the todo text.

The function `todo_classes` needs to be amended to allow for the `editing` CSS class:

```elixir
# lib/todo_mvc_web/views/main_view.ex
def todo_classes(todo) do
  [
    if(todo.editing, do: "editing"),
    if(todo.state == "completed", do: "completed")
  ]
  |> Enum.reject(&is_nil/1)
  |> Enum.join(" ")
end
```

Now, for the big reveal! Go to `assets/js/app.js` and add this:

```js
let Hooks = {}
Hooks.Todo = {
  mounted() {
    this.el.addEventListener("dblclick", e => {
      const toggle = this.el.querySelector(".toggle")

      this.pushEvent("edit", {
        "todo-id": toggle.getAttribute("phx-value-todo-id")
      })
    })
  },
  updated() {
    const edit = this.el.querySelector(".edit")
    edit.focus()
    edit.setSelectionRange(edit.value.length, edit.value.length);
  }
}
```

What's happening here? We're asking LiveView to help us with two things:
- When one of our `li`s with the `phx-hook="Todo"` attribute gets mounted, we want to set up a listener for double clicks that will send an `"edit"` event down the wire, together with the todo id that we'll pick up from the checkbox of the `li` we double-clicked;
- When one of the `li`s gets re-rendered, we want its `edit` text input child to be in focus, and we want the cursor to be at the end of its contests.

(I'm not 100% happy with this, so if you have suggestions on how to improve this, please leave a comment!)

You'll also need to change the `liveSocket` initialisation:
```js
let liveSocket = new LiveSocket("/live", Socket, {
  hooks: Hooks
})
```

We'll have to add a bunch of event handlers to the live view for this to work!

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

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

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

def handle_event("change", %{"title" => text}, socket) do
  update_text = fn
    %Todo{editing: true} = todo -> %{todo | text: text}
    todo -> todo
  end

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

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

def handle_event("stop-editing", %{"todo-id" => id}, socket) do
  toggle_editing = fn
    %Todo{id: ^id} = todo -> %{todo | editing: false}
    todo -> todo
  end

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

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

That's quite a lot of code! Let's break it down a bit.

- `edit` is triggered by the JS hook and sets up the input text field to be visible by changing the `editing` field of a todo
- `change` is triggered by changes in the input text field and when the form gets submitted and sets the text of the todo to whatever is passed
- `stop-editing` is set on blur (i.e.: when the input text field loses focus, because of the form being submitted or clicks in other areas of the page) and sets the todo field `editing` to `false`

Phew! That was a lot of work!

Let's move for the last bit of UI.

# Todo counter with pluralisation

Add this at the start of the footer:

```eex
<span class="todo-count">
  <%= left_count_label(@todos) %>
</span>
```

Again, we'll do the heavy lifting in a view function!

```elixir
# lib/todo_mvc_web/views/main_view.ex
def left_count_label(todos) do
  ngettext(
    "1 item left",
    "%{count} items left",
    Enum.count(todos, fn t -> t.state == "active" end)
  )
end
```

For the pluralisation of the "items left" counter we rely on a `gettext` facility. It's great we can leverage it even when we are not going to translate any locale!

# Typespecs

To wrap things up, I'd suggest to add (Typespecs)[https://hexdocs.pm/elixir/typespecs.html] as an exercise. They're great as documentation and they can assist catching potential errors when running dialyzer (most likely through the brilliant (dialyxir)[https://github.com/jeremyjh/dialyxir] package).

Just to get you started, as an example, here's a specced version of `handle_event` in the live view:

```
@spec handle_event(binary, map, Phoenix.LiveView.Socket.t()) ::
        {:noreply, Phoenix.LiveView.Socket.t()}
```

When you have multiple heads of a function, remember you only need a spec before the first head.

And that's all for the TodoMVC with LiveView! I hope you found this a useful way of starting out with this amazing library!