Prevent ecto changeset errors to be rendered during liveview's initial HTML response

I am trying to create a form on a page whose contents are synced with the query params of the url in a liveview.

I would like it so that any inputs on form inputs are added as query params on the url, and any url with query params automatically also inserts those params into the respective form inputs (sort of like how sharing a google url automatically inserts the text into the form field).

First I create a schema for an item:

defmodule LiveviewPages.Items.Item do
  use Ecto.Schema
  import Ecto.Changeset

  schema "items" do
    field :name, :string
    field :quantity, :integer
  end

  @doc false
  def changeset(item, attrs) do
    item
    |> cast(attrs, [:name, :quantity])
    |> validate_required(:name)
    |> validate_length(:name,
      min: 3,
      max: 7
    )
    |> validate_format(:name, ~r/^[a-zA-Z0-9 ]*$/,
      message: "must contain only letters, numbers, and spaces"
    )
    |> validate_required(:quantity)
    |> validate_change(:quantity, &validate_quantity/2)
  end

  defp validate_quantity(:quantity, quantity) do
    if quantity < 0 do
      [:quantity, "must be greater than 0"]
    else
      []
    end
  end
end

Then I create a corresponding Liveview that has form inputs for name, and quantity and some error_tags.
In the assigns I have a changeset that starts with an empty item, but its action is nil.
However because I want my form inputs to update when the url updates, I end up having a handle_params that reads the url params and updates the changeset via change_item.

To ensure that any incoming events from the client also update the changeset I push_patch from my handle_event("validate") into my handle_params after updating the url’s query params with the newly added form input data.

Also in my handle_params I end up having to |> Map.put(:action, :insert) to update the changeset action so that its action does not remain nil.

defmodule LiveviewPagesWeb.Changesets do
  use LiveviewPagesWeb, :live_view
  alias LiveviewPages.Items
  alias LiveviewPages.Items.Item

  def mount(_params, _session, socket) do
    {:ok, assign(socket, changeset: Items.change_item(%Item{}))}
  end

  def handle_params(params, _uri, socket) do
      name = params["name"]
      quantity = params["quantity"]

      changeset =
        %Item{}
        |> Items.change_item(%{name: name, quantity: quantity})
        |> Map.put(:action, :insert)

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

  def handle_event("validate", %{"item" => params}, socket) do
    query_params = URI.encode_query(params)

    if query_params != "" do
      {:noreply, push_patch(socket, to: "/changesets" <> "?" <> "#{query_params}")}
    else
      {:noreply, push_patch(socket, to: "/changesets")}
    end
  end

  def render(assigns) do
    ~H"""
    <section class="flex flex-col w-screen h-screen justify-center items-center text-center">
      <.form let={f} for={@changeset} phx-change="validate">
        <div class="grid grid-cols-8 max-w-xs gap-2">
          <%= text_input(f, :name,
            class:
              "focus:ring-gray-500 focus:border-gray-500 block w-full text-sm border-gray-300 rounded-md col-span-6",
            placeholder: "Enter an item name"
          ) %>
          <%= text_input(f, :quantity,
            inputmode: "numeric",
            class:
              "focus:ring-gray-500 focus:border-gray-500 block w-full text-sm border-gray-300 rounded-md col-span-2",
            placeholder: "Qty"
          ) %>
        </div>
        <div class="my-2 text-left">
          <%= error_tag(f, :name) %>
          <%= error_tag(f, :quantity) %>
        </div>
        <div class="grid grid-cols-8 max-w-xs gap-2">
          <%= submit("Save",
            disabled: !assigns.changeset.valid?,
            class:
              "col-span-8 bg-black hover:bg-gray-800 text-white font-medium py-2 px-4 rounded" <>
                if(!assigns.changeset.valid?, do: " opacity-50", else: "")
          ) %>
        </div>
      </.form>
    </section>
    """
  end
end

The issue I am facing is this.

During the initial HTML response from the liveview all changeset actions are rendered temporarily, since handle_params updates the changeset action and the phx-no-feedback class is only added after the websocket connection is established.

To fix this issue, is there any way for me to:

  1. force phx-no-feedback to be applied on the liveview’s initial HTML response OR
  2. distinguish in the handle_params whether the clause is being invoked in the initial HTML render or the subsequent websocket connection

UPDATE: I found that the transport_pid in the socket is nil in the initial HTML connection but not in the subsequent websocket connection. This seems hacky but can anyone confirm if it would work with point 2. above?

1 Like

The standard way to do this is with connected?(socket)

2 Likes