Pheonix LiveView Rerender after changeset value change

I am new to elixir and Pheonix
My issue is that i have a checkbox that has phx-change which calls a function change_todo which in turn makes a database change to a completed boolean. I am currently getting the result i want which is to toggle the completed value when the checkbox is clicked and i can log it and see the correct value but when i pass this value to my socket and update the for loop for my todos im not seeing the todos update and i dont know why. Its a pretty simple todo list.

This is my event handler

    def handle_event("complete", %{"todo_id" => todo_id}, socket) do
        todo_id = todo_id
        case Todos.change_todo(todo_id) do
        {:ok, completed} ->
            updated_todos = Enum.map(socket.assigns[:todos], fn todo ->
               if todo.id == todo_id do
                    %{todo | completed: completed}
               else
                todo
               end
            end)
            # here i am correctly retrieving the UPDATED completed boolean for the clicked in checkbox
            Logger.info(completed)
            socket = assign(socket, todos: updated_todos)
            # i believe here i can just assign that value to the SOCKET and somehow update the completed Variable
            # in my UI
            {:noreply, socket}
        end
    end

Here is the template portion which handles my checkbox

    <div :for={todo <- @todos} class="py-2">
      <div class="flex justify-between">
        <div>
          <div class={["font-bold", if(todo.completed, do: "line-through")]}><%= todo.description %></div>
        </div>
        <div class="inline-flex items-center">
            <.form for={@form} phx-value-todo_id={todo.id} >
                <.input field={@form[:completed]} phx-change="complete" name="completed" value={todo.completed} type="checkbox" label="Completed"/>
                <p><%= todo.completed %></p>
            </.form>
        </div> 
        <.form phx-value-todo_id={todo.id} phx-submit="delete" data-confirm={"Are you sure you want to delete: #{ todo.description} ?"}>
          <button class="bg-black border border-black hover:bg-gray-700 text-white font-bold py-2 px-3 rounded-md">
            Delete
          </button>
        </.form>
      </div>
    </div>

Id rather not use any JavaScript or anything i was just wondering if Phoenix LiveView could handle a simple re render like this. Thanks

For checkboxes, the contents of the value property do not appear in the user interface. The value property only has meaning when submitting a form. If a checkbox is in checked state when the form is submitted, the name of the checkbox is sent along with the value of the value property (if the checkbox is not checked, no information is sent).

Helpful resources

I don’t think that’s my issue here because when i refresh the page after checking a box my “line-through” class is applied and the box is checked. I am just wondering why LiveView is not re-rendering the component instead of me having to refresh the page

I guess you still need to do that and in worst case also some extra JavaScript stuff for value attribute change …

The JavaScript client is always the source of truth for current input values. For any given input with focus, LiveView will never overwrite the input’s current value, even if it deviates from the server’s rendered updates. This works well for updates where major side effects are not expected, such as form validation errors, or additive UX around the user’s input values as they fill out a form.

Source: https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-specifics

I had the very same problem with textarea, see:

I’m pretty sure it’s the value={todo.completed} that is messing it up. field handled both name and value. Try removing it—LiveView certainly handles this as I have a very checkbox heavy app I’m currently working on and doing nothing special to make them happen :slight_smile:

That’s not it either. I pretty much confirmed that im not updating my socket correctly
when i Logger.info both
updated_todos
and
socket
both of them still hold the todo with the wrong completed value
but if i log completed it is holding the correct todo with the correct completed value.
seems like that updated_todos map isn’t doing anything

Ohhhhhh todo_id is coming from params so it’s a string so == is failing because you’re comparing an int to a string.

1 Like

It could be a binary_id :slight_smile:

Yes, I am making a dangerous assumption based on OP’s latest remarks :grin:

Ok so after looking around online i feel dumb…i didnt need to loop over the todos as i was already updating the completed value in another function but i was on the right track with assigning the todo back to the socket. My issues was i just needed to retrieve all the todos again and assign that to the socket. example:

        {:ok, completed} ->
            socket =
                socket
                |> assign(:todos,Todos.list_todos(user_id))
            {:noreply, socket}

Its updating correctly now but i get them in a new order each time…so now im wondering if there is a order_by function for ecto? Anyway thanks for the help.

And when You’ll want to broadcast this to other connected clients… they will reload too? From the database?

This won’t scale well

You shouldn’t need to do this, there’s gotta be something else wrong.

For some unsolicited feedback, it’s more idiomatic (and flexible) to return the struct, even if just updating one field. So instead of {:ok, <bool>}, return {:ok, %Todo{}}. Then you can splice it in:

{:ok, %{id: updated_id} = updated_todo} ->
  socket =
    update(socket, :todos, fn todos ->
      Enum.map(todos, fn ->
        %{id: ^updated_id} -> updated_todo
        todo -> todo
      end)
    end)

  {:noreply, socket}

(there are various ways to write that, that’s just how I do it)