Understanding This Parameter Assignment

I am working my way through Programming Phoenix Live Views and I have this weird defect while working on the Promo codes in chapter 5. My solution works, but it has an error that triggers when you submit the form, then start typing again it immediately fails on attempts to validate because the form structure has changed in a way that makes the parameter matching no longer match.

Here’s the PromoLive module (the two handle event functions are important here):

defmodule PentoWeb.PromoLive do
  alias Swoosh.Email.Recipient
  use PentoWeb, :live_view
  alias Pento.Promo
  alias Pento.Promo.Recipient

  def mount(_params, _session, socket) do
    {:ok,
      socket
      |> assign_recipient()
      |> clear_form()}
  end

  def handle_event(
    "save",
    %{"recipient" => recipient_params},
    %{assigns: %{recipient: recipient}} = socket) do
    changeset = recipient
      |> Promo.change_recipient(recipient_params)

    case Promo.send_promo(recipient) do
      {:ok, _} ->
        {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
      {:error, _} ->
        {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
    end


    {:noreply, assign_form(socket, %{})}
  end

  def handle_event(
    "validate",
    %{"recipient" => recipient_params},
    %{assigns: %{recipient: recipient}} = socket) do
    changeset =
      recipient
      |> Promo.change_recipient(recipient_params)
      |> Map.put(:action, :validate)

    {:noreply, assign_form(socket, changeset)}
  end

  def assign_recipient(socket) do
    socket
    |> assign(:recipient, %Recipient{})
  end

  def clear_form(socket) do
    form =
      socket.assigns.recipient
      |> Promo.change_recipient()
      |> to_form

    assign(socket, :form, form)
  end

  def assign_form(socket, changeset) do
    assign(socket, :form, to_form(changeset))
  end
end

And the simple html that supports it.

<.header>
  Send your promo code to a friend
  <:subtitle>promo code for 10% off their first game purchase!</:subtitle>
</.header>

<div>

  <.simple_form
    for={@form}
    id="promo_form"
    phx-change="validate"
    phx-submit="save">

    <.input field={@form[:first_name]} type="text" label="First Name"/>
    <.input field={@form[:email]} type="email" label="Email" phx-debounce="blur"/>

    <:actions>
      <.button phx-disable-with="Sending...">Send Promo</.button>
    </:actions>
  </.simple_form>
</div>

Like I said, this all works fine, but the second I type first name in after submission it crashes and restarts with the following error in the console.

[error] GenServer #PID<0.42461.0> terminating
** (FunctionClauseError) no function clause matching in PentoWeb.PromoLive.handle_event/3
    lib/pento_web/live/promo_live.ex:14: PentoWeb.PromoLive.handle_event("validate", %{"_target" => ["first_name"], "_unused_email" => "", "email" => "", "first_name" => "J"}, #Phoenix.LiveView.Socket<id: "phx-F-iJ5MVWcMOHEBai", endpoint: PentoWeb.Endpoint, view: PentoWeb.PromoLive, parent_pid: nil, root_pid: #PID<0.42461.0>, router: PentoWeb.Router, assigns: %{form: %Phoenix.HTML.Form{source: %{}, impl: Phoenix.HTML.FormData.Map, id: nil, name: nil, data: %{}, action: nil, hidden: [], params: %{}, errors: [], options: [], index: nil}, __changed__: %{}, current_user: #Pento.Accounts.User<__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1, email: "james.r.carr@gmail.com", confirmed_at: nil, inserted_at: ~U[2024-08-01 17:28:18Z], updated_at: ~U[2024-08-01 17:28:18Z], ...>, flash: %{}, live_action: nil, recipient: %Pento.Promo.Recipient{first_name: nil, email: nil}}, transport_pid: #PID<0.42454.0>, ...>)

This is due to the event recieving

%{"_target" => ["first_name"], "_unused_email" => "", "email" => "", "first_name" => "J"}

Instead of the normal %{"recipient" => %{}} map. What causes this behavior?

It’s determined by the :as field of the to_form/1 call, which is automatically populated using the changeset if one is provided.

Is Promo.change_recipient actually returning a changeset?

You’re not assigning the result of case Promo.send_promo(recipient) to anything. Your save handle_event is unconditionally returning {:noreply, assign_form(socket, %{})} in all cases.

3 Likes

When the :form assign is set up initially in mount, it’s populated by clear_form.

After the save event, it’s populated with %{} instead - that causes the form to generate fields without the recipient wrapper.

IMO, calling clear_form in the handle_event("save", ...) head would be the quickest fix.

So, I had tried many different solutions proposed here (all of them correct) and still for the life of me could not figure out why validate failed after saving an entry. Then I saw it… {:noreply, assign_form(socket, %{})} is returned outside of the case statement, an artifact from when I was adding the case statement in the first place. Removing that and adding clear_form() fixed the problem.

Thanks everyone for the help!

1 Like