LiveView on form change sends not only changed input

Hi!

Sorry, I am very new to LiveView and Phoenix, and can’t figure out what is going on.

And I see in Network tab/WS that diff result that is coming from the server is sending diff not only for email input which was the only one to be changed, but for password field as well and even for dropdown I have in header which is present in layouts/app.html.heex.

Could you please help to to figure this put?

I have a liveview like:

defmodule AppWeb.TestFormLive do
  use AppWeb, :live_view
  alias App.Accounts

  import Ecto.Changeset

  def render(assigns) do
    ~H"""
    <.simple_form
      id="asd"
      for={@form}
      phx-change={JS.push("validate", loading: "#asd")}
      phx-submit="save"
    >
      <.text_field field={@form[:email]} type="email" label="Email" required />
      <.text_field field={@form[:password]} type="password" label="Password" required />

      <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
    </.simple_form>
    """
  end

  def mount(_params, _session, socket) do
    changeset = Ecto.Changeset.cast(%Accounts.User{}, %{}, [])
    form = to_form(changeset, as: "user")
    socket = assign(socket, form: form)

    {:ok, socket}
  end

  def handle_event("inc_temperature", _params, socket) do
    {:noreply, update(socket, :temperature, &(&1 + 1))}
  end

  def handle_event("validate", %{"user" => attrs}, socket) do
    changeset =
      %Accounts.User{}
      |> cast(attrs, [:email, :password])
      |> validate_length(:email, max: 1)

    form = to_form(Map.put(changeset, :action, :validate), as: "user")

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

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

Here is app.html.heex which is used for live and dead views:

<.app_header {assigns} />

<div class="grid items-start grid-cols-6 gap-12 mx-auto mt-10 max-w-screen-2xl">
  <.app_sidebar />

  <main class="col-span-4 xl:col-span-5">
    <%= @inner_content %>
  </main>
</div>

App header looks like:

<header id="app-header" class="bg-white border-b border-gray-100">
  <div class="px-2 mx-auto max-w-screen-2xl sm:px-6 lg:px-8">
    <div class="relative flex justify-between h-16">
      <div class="flex items-stretch justify-start flex-1">
        <div class="flex items-center flex-shrink-0">
          <.link_to href={~p"/"}>
            <img
              class="w-auto h-12"
              src="https://cdn-icons-png.flaticon.com/512/4119/4119945.png"
              alt="Your Company"
            />
          </.link_to>
        </div>
      </div>

      <div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
        <%= if @current_user do %>
          <.dropdown id="profile-dropdown">
            <:button>
              <img
                class="w-8 h-8 rounded-full mr-2"
                src="https://cdn-icons-png.flaticon.com/512/4119/4119945.png"
              />

              <%= @current_user.email %>
            </:button>

            <.dropdown_item href={~p"/"}>Your profile</.dropdown_item>
            <.dropdown_item href={~p"/users/settings"}>
              <%= dgettext("accounts", "Account settings") %>
            </.dropdown_item>
            <.dropdown_divider />
            <.dropdown_item href={~p"/users/sign_out"} method="delete">
              <%= dgettext("accounts", "Sign out") %>
            </.dropdown_item>
          </.dropdown>
        <% else %>
          <.button variant="flat" class="mr-2" href={~p"/users/sign_up"}>
            <%= dgettext("accounts", "Sign up") %>
          </.button>

          <.button variant="success" href={~p"/users/sign_in"}>
            <%= dgettext("accounts", "Sign in") %>
          </.button>
        <% end %>
      </div>
    </div>
  </div>
</header>

And dropdown component:

defmodule AppWeb.Components.UI.Dropdown do
  import AppWeb.Components.UI.Button

  use Phoenix.Component

  alias Phoenix.LiveView.JS

  attr :id, :string, required: true
  slot :button, required: true
  slot :inner_block, required: true

  def dropdown(assigns) do
    ~H"""
    <div id={@id}>
      <%= for btn <- @button do %>
        <.toggle_button id={"#{@id}-toggle-button"} {btn}><%= render_slot(btn) %></.toggle_button>
      <% end %>

      <.dropdown_content id={"#{@id}-toggle-button"}>
        <%= render_slot(@inner_block) %>
      </.dropdown_content>
    </div>
    """
  end

  attr :class, :string, default: nil
  attr :rest, :global

  slot :inner_block, required: true

  def dropdown_item(assigns) do
    ~H"""
    <.button variant="flat" class={["ui--dropdown--menu-item--tag", @class]} {@rest}>
      <%= render_slot(@inner_block) %>
    </.button>
    """
  end

  def dropdown_divider(assigns) do
    ~H"""
    <div class="ui--dropdown--menu-item--divider"></div>
    """
  end

  attr :id, :string, required: true
  attr :class, :string, default: nil
  attr :rest, :global
  slot :inner_block, required: true

  defp toggle_button(assigns) do
    ~H"""
    <.button
      id={@id}
      variant="flat"
      class={["rounded-xl peer", @class]}
      data-state="closed"
      phx-click={toggle()}
      phx-click-away={hide()}
      {@rest}
    >
      <%= render_slot(@inner_block) %>
    </.button>
    """
  end

  attr :id, :string, required: true
  attr :class, :string, default: nil
  slot :inner_block, required: true

  defp dropdown_content(assigns) do
    ~H"""
    <div
      id={@id}
      class={[
        "absolute peer-data-[state=closed]:hidden z-20 right-0 p-1 mt-2 origin-top-right bg-white border border-gray-200 rounded-xl focus:outline-none",
        @class
      ]}
    >
      <%= render_slot(@inner_block) %>
    </div>
    """
  end

  defp toggle(js \\ %JS{}) do
    JS.toggle_attribute(js, {"data-state", "open", "closed"})
  end

  defp hide(js \\ %JS{}) do
    JS.set_attribute(js, {"data-state", "closed"})
  end
end

Hi! This is expected behaviour. You can read about it here.

1 Like

Got it, thank you so much!

This is most likely caused by passing {assigns} as is to the component. Doing it this way disabled change tracking, therefore you see the header parts in the diff.

You should be able to improve this by only passing the assigns needed by your header, e.g. <.app_header current_user={@current_user} />, or if necessary write <%= app_header(assigns) %> to pass all assigns instead.

2 Likes

@steffend Thank you! Will try.

Just to add for those coming from google:

Here’s a great explanation of the assigns variable: The Assigns Variable

1 Like