Live Component assigning clean changeset on handle_event doesn't seem to reflect

First entry, expect the form to clear as I am setting brand new changeset.

 @impl true
  def mount(socket) do
    changeset = Stock.change_inventory(%Inventory{})

    {:ok,
     socket
     |> assign(:units, Stock.units())
     |> assign(:changeset, changeset)}
  end

  @impl true
  def handle_event("add", %{"inventory" => inventory_params}, socket) do
    case Stock.create_inventory(inventory_params) do
      {:ok, inventory} ->
        send(self(), {:new, inventory})

        # why the fuck are you not setting?
        {:noreply, assign(socket, changeset: Stock.change_inventory(%Inventory{}))}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

Nothing clears, let me set the name forcefully

 # why the fuck are you not setting?
        {:noreply,
         assign(socket, changeset: Stock.change_inventory(%Inventory{name: "WTFFFF!!!"}))}

Third entry is this

In video a bit clear to understand

https://imgur.com/a/6mKhYy2

Is this happening in the LiveView or in a LiveComponent? If the latter, can you show the live_component(@socket, ...) call from the template?

My hunch is that there’s a race condition here:

        send(self(), {:new, inventory})

        # why the fuck are you not setting?
        {:noreply, assign(socket, changeset: Stock.change_inventory(%Inventory{}))}

If you comment out the send() call, does the behaviour change?

1 Like

Can you show your template code?

1 Like
  • tried removing the send()
  • tried also adding update method rather mount as per phoenix documentation
  • tried passing the changeset from the LW
  • remove all the HTML and leave the component on the page standalone

all individual and combinations yield to the same result. I must be doing something so simple yet very stupid again. I doubt I have found a bug.

LV.html

<%= if @live_action in [:edit] do %>
  <%= live_modal @socket, ExWeb.Stock.InventoryLive.FormComponent,
    id: @inventory.id,
    title: @page_title,
    action: @live_action,
    inventory: @inventory,
    return_to: Routes.inventory_index_path(@socket, :index, @uuid) %>
<% end %>
<%= live_component @socket, ExWeb.Stock.InventoryLive.AddComponent, id: "add" %>
<table class="table is-fullwidth is-hoverable is-striped">
  <thead>
    <tr>
      <th>Favourite</th>
      <th>Add</th>
      <th>Name</th>
      <th>Unit</th>
      <th>Description</th>
      <th>Last bought</th>
      <th></th>
    </tr>
  </thead>
  <tbody id="inventories" phx-update="prepend">
    <%= for inventory <- @inventories do %>
      <tr id="inventory-<%= inventory.id %>" <%= if inventory.__meta__.state == :deleted do %>class="is-hidden"<% end %>>
        <td>
          <%= link to: "#", phx_click: "toggle-fav", phx_value_id: inventory.id do %>
            <%= if inventory.is_fav do %>
              <span class="icon has-text-warning has-tooltip-arrow" data-tooltip="Remove from favorites">
                <i class="fas fa-star fa-lg"></i>
              <% else %>
                <span class="icon has-tooltip-arrow" data-tooltip="Add to favorites">
                  <i class="far fa-star fa-lg"></i>
                <% end %>
              <% end %>
            </td>
            <td>
              <%= link to: "#", phx_click: "cart", phx_value_id: inventory.id do %>
                <span class="icon has-tooltip-arrow" data-tooltip="Add to current shopping list">
                  <i class="fas fa-cart-plus fa-lg"></i>
                <% end %>
              </td>
              <td><%= inventory.name %></td>
              <td><%= inventory.unit %></td>
              <td><%= inventory.description %></td>
              <td><%= inventory.last_bought %></td>
              <td>
                <%= live_patch to: Routes.inventory_index_path(@socket, :edit, @uuid, inventory) do %>
                  <span class="icon has-tooltip-arrow" data-tooltip="Edit">
                    <i class="far fa-edit"></i>
                  </span>
                <% end %>
                <%= link to: "#", phx_click: "delete", phx_value_id: inventory.id, data: [confirm: "Are you sure?"] do %>
                  <span class="icon has-tooltip-arrow" data-tooltip="Delete">
                    <i class="far fa-trash-alt"></i>
                  </span>
                <% end %>
              </td>
            </tr>
          <% end %>
        </tbody>
      </table>

Component

defmodule ExWeb.Stock.InventoryLive.AddComponent do
  use ExWeb, :live_component

  alias Ex.Stock
  alias Ex.Stock.Inventory

  @impl true
  def mount(socket) do
    changeset = Stock.change_inventory(%Inventory{})

    {:ok,
     socket
     |> assign(:units, Stock.units())
     |> assign(:changeset, changeset)}
  end

  @impl true
  def handle_event("add", %{"inventory" => inventory_params}, socket) do
    case Stock.create_inventory(inventory_params) do
      {:ok, inventory} ->
        send(self(), {:new, inventory})

        # why the fuck are you not setting?
        {:noreply, assign(socket, changeset: Stock.change_inventory(%Inventory{}))}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

  @impl true
  def render(assigns) do
    ~L"""
    <%= f = form_for @changeset, "#",
    id: "add-inventory-form",
    phx_target: @myself,
    phx_submit: "add" %>
    <%= label f, :unit %>
    <%= select f, :unit, @units %>
    <%= error_tag f, :unit %>
    <%= label f, :name %>
    <%= text_input f, :name, required: true %>
    <%= error_tag f, :name %>
    <%= label f, :description %>
    <%= text_input f, :description %>
    <%= error_tag f, :description %>
    <%= submit "WTF phoenix!!!", phx_disable_with: "Saving..." %>
    </form>
    """
  end
end

What happens when you wrap the form in a div? (Inside the form component render function)

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-components-require-at-least-one-html-tag

1 Like

Ah very hopefully added that fine print! thank you for highlighting it again.

but no dice. I even wrapped the component itself in a unique id div on LV.

hurray, I have a “workaround” “fix” “hack” not sure why I need to do this when I explicitly set it.

I did not have temporary assigns in my LV.

temporary_assigns: [changeset: %{}]

Should I raise a bug for this if other people can replicate it?

@cenotaph we are definitely able to reset forms using blank changesets in our apps, can you create a reproduceable we can run?

1 Like

I have managed to re-create with another project. I might be doing something fundamentally wrong.

I’ve found the issue.

In the mount callback, you set the changeset with changeset = Stock.change_inventory(%Inventory{}). When the form saves the record, it sets the changeset to a new changeset using Stock.change_inventory(%Inventory{}) as well. Since the changeset has not changed in the assigns, the HTML is not updated.

If you add the same validate you have in the other form and set a phx-change in the form, it will work because when validating it will update the changeset in the assigns.

You can also verify this by just clicking the submit which will update the changeset in the assigns and render errors, and then filling and submitting again, which will reset the changeset as expected.

4 Likes

What I mentioned above explains why the temporary assigns fixes the issue. Mount will set a “new” Ecto.Changeset and then will reset to %{}, when saving you replace it with an Ecto.Changeset again, which triggers the update because it changed.

1 Like

Ha, I just arrived at the same conclusion. It’s definitely a tricky situation. @cenotaph I’ve found as a general rule that a phx-change setting is basically mandatory on live view forms. It’s not only important for situations like this, but it’s also critical if you want automatic form recovery on disconnect.

2 Likes

Thank you for the detailed explanation! It makes sense since the values coming back is not a changeset but http params! I always forget that.

I always removed validate as I rather validate on the submission to keep the wire clean but there is the one reason it is in the generated component code then :slight_smile:

I knew I was doing something dumb. Thank you all for spending the time on this and helping me get to the bottom of it!

Yes I definitely see the value of it on the forms and recoveryI I got rid of them to prevent noise. Silly me!