Creating a Todo Delete button, what am I doing wrong?

Hello,
I am following this tutorial todo list, one of the last challenges is to create a delete button by yourself. I attempted to replicate it using the update_todo and doing the opposite. However, the button does not delete the to-do created.

my func below to delete

def handle_event("toggle_delete", %{"id" => id}, socket)do
       todo = Todos.get_todo!(id) 
       Todos.delete_todo(todo, %{delete: todo.id})
       {:noreply, socket}
    end

&
The button I created below

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

<%= for todo <- @todos do %>
    <div>
    <%= checkbox(:todo, :done, phx_click: "toggle_done", phx_value_id: todo.id, value: todo.done) %>
**<button phx_click="toggle_delete", :delete, phx_value_id: todo.id, value: todo.delete> Delete</button>**
    <%= todo.title %></div>
<% end %>

What am I doing wrong?

1 Like

When formatting code on the forum/in Markdown, you need to put the triple back ticks on a new line, with a blank line after.

You are not assigning the todos after deleting one. The template renders what is in the socket's assigns map.

You have two options.

  1. Read the entire list of todos from the database after deleting and assign the new list, or

  2. Use update(socket, :todos, fn todos -> Enum.reject(todos, fn t -> t.id == todo.id end) end) to remove the one you deleted from the assigns. You should do this within a case statement only if the delete was successful, just in case your delete failed.

1 Like

Thank you for responding,

I will correct my forum in Markdown so it more readable.

Also I don’t quite follow how to create it. In reference to the second option update(socket, :todos, fn todos -> Enum.reject(todos, fn t -> t.id == todo.id end) end) to Where does this even go in my event_handler func, and I create a case statement. I am just trying to make sense of it so I can understand.

1 Like
def handle_event("delete", %{"id" => id}, socket) do
  todo = Todos.get_todo!(id) 

  case Todos.delete_todo(todo, %{delete: todo.id}) do
  {:ok, _todo} -> 
    socket = update(socket, :todos, fn todos -> Enum.reject(todos, fn t -> t.id == todo.id end) end)
    {:noreply, socket}

  {:error, _} ->
    {:noreply, socket}
end
1 Like

Thank you for this

I had a hard time following this tutorial, the programmer did not really explain anything kinda expected us to know it. I only did it because it was the only up-to-date tutorial. This was my first time doing anything with Phoenix and Liveview. I am a bit lost with the many terminology that I need to learn, the syntax and of-course the different pages.
Do you know of any introductory course for getting started with Phoenix and Liveview? I found this one Phoenix LiveView course! Do you recommend it?

Thank you again!

1 Like

Yes, that course is good. They have an elixir one too, but start with the guide at elixir-lang.org.

There are some recent threads here on good learning resources too.

1 Like

Hello,

I was not able to get the code Todo app to delete. No Errors came either, also I got nothing from Google Inspect Elements. If you can assist me to debug this.

I went ahead and pushed it to GitHub Repo

   def handle_event("delete", %{"id" => id}, socket)do
        todo = Todos.get_todo!(id)
    
        case Todos.delete_todo(todo, %{delete: todo.id}) do
         {:ok, _todo} -> 
            socket = update(socket, :todos, fn todos -> Enum.reject(todos, fn t -> t.id == todo.id end) end)
              {:noreply, socket}
          
            {:error, _} ->
              {:noreply, socket}
        end
    end

It seems you are mixing up EEx/Heex.

Try to do this for your button in todo_live.html.leex:

<button phx-click="delete" phx-value-id={todo.id}>Delete</button>

You can read about these bindings: Bindings — Phoenix LiveView v0.17.7
Another thing to check out is Assigns and HEEx templates — Phoenix LiveView v0.17.7

1 Like

I changed the button to what you have I got returned an error. the upside is the page refreshes but nothing removes

[error] GenServer #PID<0.564.0> terminating
** (Ecto.Query.CastError) deps/ecto/lib/ecto/repo/queryable.ex:441: value `"{todo.id}"` in `where` cannot be cast to type :id in query:

from t0 in LiveViewTodos.Todos.Todo,
  where: t0.id == ^"{todo.id}",
  select: t0

    (elixir 1.13.3) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir 1.13.3) lib/enum.ex:1715: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
    (elixir 1.13.3) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto 3.7.1) lib/ecto/repo/queryable.ex:203: Ecto.Repo.Queryable.execute/4
    (ecto 3.7.1) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
    (ecto 3.7.1) lib/ecto/repo/queryable.ex:154: Ecto.Repo.Queryable.one!/3
    (live_view_todos 0.1.0) lib/live_view_todos_web/live/todo_live.ex:22: LiveViewTodosWeb.TodoLive.handle_event/3
    (phoenix_live_view 0.17.7) lib/phoenix_live_view/channel.ex:349: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.0.0) /Users/shansiddiqui/Desktop/live_view_todos/deps/telemetry/src/telemetry.erl:293: :telemetry.span/3
    (phoenix_live_view 0.17.7) lib/phoenix_live_view/channel.ex:206: Phoenix.LiveView.Channel.handle_info/2
Last message: %Phoenix.Socket.Message{event: "event", join_ref: "4", payload: %{"event" => "delete", "type" => "click", "value" => %{"id" => "{todo.id}", "value" => ""}}, ref: "6", topic: "lv:phx-Ftrdmz0T3VisSwAj"}
State: %{components: {%{}, %{}, 1}, join_ref: "4", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{__changed__: %{}, flash: %{}, live_action: :index, todos: [%LiveViewTodos.Todos.Todo{__meta__: #Ecto.Schema.Metadata<:loaded, "todos">, done: false, id: 2, inserted_at: ~N[2022-03-09 07:30:29], title: "linky", updated_at: ~N[2022-03-09 17:07:48]}, %LiveViewTodos.Todos.Todo{__meta__: #Ecto.Schema.Metadata<:loaded, "todos">, done: false, id: 1, inserted_at: ~N[2022-03-09 07:11:04], title: "notty", updated_at: ~N[2022-03-10 00:07:53]}, %LiveViewTodos.Todos.Todo{__meta__: #Ecto.Schema.Metadata<:loaded, "todos">, done: false, id: 3, inserted_at: ~N[2022-03-09 07:30:44], title: "stink", updated_at: ~N[2022-03-10 00:07:56]}]}, endpoint: LiveViewTodosWeb.Endpoint, id: "phx-Ftrdmz0T3VisSwAj", parent_pid: nil, root_pid: #PID<0.564.0>, router: LiveViewTodosWeb.Router, transport_pid: #PID<0.556.0>, view: LiveViewTodosWeb.TodoLive, ...>, topic: "lv:phx-Ftrdmz0T3VisSwAj", upload_names: %{}, upload_pids: %{}}

Try this for your handle event:

  def handle_event("delete", %{"id" => id}, socket) do
    todo = Todos.get_todo!(id)

    case Todos.delete_todo(todo) do
      {:ok, _} ->
        socket = update(socket, :todos, fn todos -> &Enum.reject(&1.id == todo.id) end)
        {:noreply, socket}

      {:error, _} ->
        {:noreply, socket}
    end
  end

Todos.delete_todo/1 expects only a todo.

1 Like

Also you might want to return the values again in your broadcast functions. That way it whenever you call them they return like epxected, i.e. delete_todo returns {;ok, deleted_todo} | {:error, reason}, now it does not because it returns the result of your broadcast.
So put this in todos.ex:

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

  defp broadcast_change(result, _event) do
    result
  end
1 Like

Does it matter what order the handle_event functions are in?

After updating the new handle_event delete I still get Errors

[error] GenServer #PID<0.544.0> terminating
** (Ecto.Query.CastError) deps/ecto/lib/ecto/repo/queryable.ex:441: value `"{todo.id}"` in `where` cannot be cast to type :id in query:

from t0 in LiveViewTodos.Todos.Todo,
  where: t0.id == ^"{todo.id}",
  select: t0

    (elixir 1.13.3) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir 1.13.3) lib/enum.ex:1715: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
    (elixir 1.13.3) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto 3.7.1) lib/ecto/repo/queryable.ex:203: Ecto.Repo.Queryable.execute/4
    (ecto 3.7.1) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
    (ecto 3.7.1) lib/ecto/repo/queryable.ex:154: Ecto.Repo.Queryable.one!/3
    (live_view_todos 0.1.0) lib/live_view_todos_web/live/todo_live.ex:22: LiveViewTodosWeb.TodoLive.handle_event/3
    (phoenix_live_view 0.17.7) lib/phoenix_live_view/channel.ex:349: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.0.0) /Users/shansiddiqui/Desktop/live_view_todos/deps/telemetry/src/telemetry.erl:293: :telemetry.span/3
    (phoenix_live_view 0.17.7) lib/phoenix_live_view/channel.ex:206: Phoenix.LiveView.Channel.handle_info/2
Last message: %Phoenix.Socket.Message{event: "event", join_ref: "13", payload: %{"event" => "delete", "type" => "click", "value" => %{"id" => "{todo.id}", "value" => ""}}, ref: "20", topic: "lv:phx-Ftrd-zqoxGg9CgSB"}
State: %{components: {%{}, %{}, 1}, join_ref: "13", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{__changed__: %{}, flash: %{}, live_action: :index, todos: [%LiveViewTodos.Todos.Todo{__meta__: #Ecto.Schema.Metadata<:loaded, "todos">, done: false, id: 2, inserted_at: ~N[2022-03-09 07:30:29], title: "linky", updated_at: ~N[2022-03-09 17:07:48]}, %LiveViewTodos.Todos.Todo{__meta__: #Ecto.Schema.Metadata<:loaded, "todos">, done: false, id: 1, inserted_at: ~N[2022-03-09 07:11:04], title: "notty", updated_at: ~N[2022-03-10 00:07:53]}, %LiveViewTodos.Todos.Todo{__meta__: #Ecto.Schema.Metadata<:loaded, "todos">, done: false, id: 3, inserted_at: ~N[2022-03-09 07:30:44], title: "stink", updated_at: ~N[2022-03-10 00:07:56]}]}, endpoint: LiveViewTodosWeb.Endpoint, id: "phx-Ftrd-zqoxGg9CgSB", parent_pid: nil, root_pid: #PID<0.544.0>, router: LiveViewTodosWeb.Router, transport_pid: #PID<0.536.0>, view: LiveViewTodosWeb.TodoLive, ...>, topic: "lv:phx-Ftrd-zqoxGg9CgSB", upload_names: %{}, upload_pids: %{}}
[debug] QUERY OK source="todos" db=13.6ms idle=90.6ms
SELECT t0."id", t0."done", t0."title", t0."inserted_at", t0."updated_at" FROM "todos" AS t0 []

I added this to my todos.ex still errors, I restarted the server I find that the page refreshes and when I click delete. But that’s as far as it goes.

Quickly looking, theres some things I noticed. Y

  1. You are using Heex syntac in a Leex file. so rename todo_live.html.leex to todo_live.html.heex
    This is what it should look like inside:
<div>
  <form action="#" phx-submit="add">
    <%= text_input :todo, :title, placeholder: "What do you want to get done" %>
    <%= submit "Add", phx_disable_with: "Adding..." %>
  </form>

  <%= for todo <- @todos do %>
    <div>
      <%= checkbox(:todo, :done, phx_click: "toggle_done", phx_value_id: todo.id, value: todo.done) %>
      <button phx-click="delete" phx-value-id={todo.id}>Delete</button>
      <%= todo.title %>
    </div>
  <% end %>
</div>
  1. Fixed the Enum.reject function in handle event and displayed a flash when its success or failue, this way we can see if it ever even reaches that part.
  def handle_event("delete", %{"id" => id}, socket) do
    todo = Todos.get_todo!(id)

    case Todos.delete_todo(todo) do
      {:ok, deleted_todo} ->
        socket =
          update(socket, :todos, fn todos ->
            Enum.reject(todos, &(&1.id == deleted_todo.id))
          end)

        {:noreply, put_flash(socket, :info, "Todo '#{deleted_todo.id}' deleted")}

      {:error, _} ->
        {:noreply, put_flash(socket, :error, "An error occured while deleting a todo")}
    end
  end

What happens if you update these things? I think changing the leex to heex might resolve the refresh crash.

Also you have to remove the following:

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

And only have these 2 for broadcast_change

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

  defp broadcast_change(result, _event) do
    result
  end

Because you are subscribing to all changes to Todos in the mount of that liveview, you don’t need to bother assigning them in the handle_event calls. You are capturing all the changes to Todos and reassigning the entire list here:

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


defp fetch(socket) do
    assign(socket, todos: Todos.list_todos())
end

So, you can ignore the Enum.reject suggestion I made and apply @tomkonidas’s suggestion so it looks like:

def handle_event("delete", %{"id" => id}, socket) do
    todo = Todos.get_todo!(id)

    case Todos.delete_todo(todo) do
      {:ok, _} ->
        {:noreply, put_flash(socket, :info, "Todo '#{deleted_todo.id}' deleted")}

      {:error, _} ->
        {:noreply, put_flash(socket, :error, "An error occured while deleting a todo")}
    end
  end

You need to make the change @tomkonidas suggested to broadcast_change to return the result when the operation is successful.


The order of handle_events matters when you are pattern matching on aspects of the params or socket for the same event. However, you are not doing any of that, so it doesn’t matter in this instance.

For example, you could ignore the delete command if something was not set to true (e.g. a checkbox wasn’t checked). Bear in mind that this is a nonsense example, but hopefully it conveys the idea.

def handle_event("delete", %{"id" => id, "something" => "true"}, socket) do
  todo = Todos.get_todo!(id)
    
  case Todos.delete_todo(todo) do
    {:ok, _} ->
      socket = update(socket, :todos, fn todos -> &Enum.reject(&1.id == todo.id) end)
      {:noreply, socket}
    
    {:error, _} ->
      {:noreply, socket}
  end
end

def handle_event("delete", _params, socket) do
  {:noreply, socket}
end

It worked!!

Few Questions:

why use a heex file vs leex file whats the difference?

Why was it important to use a case file vs just doing it like Todos.delete?

Leex is the old way (will be deprecated in the future). Heex is what we will use for LiveViews and normal views going forward.

In your situation, you did not need the case because you handled the result via handle_info. If you did not subscribe to the topic and handle_info, then you would need to case on the result of Todos.delete_todo/1 to know when it was successful to filter out the deleted todo in our assigns :todos. Or else if you just called the function, the view would not update because you would have sent back the same socket you got at the beginning. Then you would only see the updated changes if you refresh your page manually