phoenixframework / phoenix_live_view

Rich, real-time user experiences with server-rendered HTML
https://hex.pm/packages/phoenix_live_view
MIT License
6.14k stars 921 forks source link

check asyncFilter on blur (closes #3194) #3253

Closed SteffenDE closed 3 months ago

SteffenDE commented 4 months ago

When submitting a form with phx-debounce="blur" without ever leaving the field, the blur event would be fired twice on the input, as the browser would trigger a second blur event when the view is patched by morphdom and the currently focused input is removed, sending the event to the new LiveView.

My fix here is to check for the asyncFilter introduced in https://github.com/phoenixframework/phoenix_live_view/commit/5a62253004abc4617c51e3f7c2cbb68245d2f86d, which already checks if the view is destroyed or the element not in the DOM.

Fixes #3194.

SteffenDE commented 4 months ago

To reproduce the problem:

Application.put_env(:sample, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64)
)

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7"},
  # please test your issue using the latest version of LV from GitHub!
  {:phoenix_live_view, github: "phoenixframework/phoenix_live_view", branch: "main", override: true},
  # uncomment to use the fix
  # {:phoenix_live_view, github: "phoenixframework/phoenix_live_view", branch: "sd-issue-3194", override: true},
])

# build the LiveView JavaScript assets (this needs mix and npm available in your path!)
path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../")
System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream())
System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream())
System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream())

defmodule Example.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def mount(_params, _session, socket) do
    {:ok, assign(socket, :form, to_form(%{}, as: :foo))}
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="/assets/phoenix/phoenix.js"></script>
    <script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
    <%!-- <script src="http://127.0.0.1:8000/priv/static/phoenix_live_view.js"></script> --%>
    <%!-- uncomment to use enable tailwind --%>
    <%!-- <script src="https://cdn.tailwindcss.com"></script> --%>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <style>
      * { font-size: 1.1em; }
    </style>
    <%= @inner_content %>
    """
  end

  def render(assigns) do
    ~H"""
    <.form
      for={@form}
      phx-change="validate"
      phx-submit="submit"
    >
      <input
        id={@form[:store_number].id}
        name={@form[:store_number].name}
        value={@form[:store_number].value}
        type="text"
        phx-debounce="blur"
      />
    </.form>
    """
  end

  def handle_event("submit", params, socket) do
    {:noreply, push_navigate(socket, to: "/other")}
  end

  def handle_event("validate", params, socket) do
    {:noreply, socket}
  end
end

defmodule Example.OtherLive do
  use Phoenix.LiveView, layout: {Example.HomeLive, :live}

  def render(assigns) do
    ~H"""
    <h2>The other one</h2>
    """
  end
end

defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", Example do
    pipe_through(:browser)

    live("/", HomeLive, :index)
    live("/other", OtherLive, :index)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample
  socket("/live", Phoenix.LiveView.Socket)

  plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
  plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"

  plug(Example.Router)
end

{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
SteffenDE commented 4 months ago

The e2e tests showed that this issue only happens in Chromium based browsers. Safari and Firefox seemingly don't trigger blur when a focused input is removed from the DOM.

SteffenDE commented 4 months ago

https://groups.google.com/a/chromium.org/g/blink-dev/c/L1aI9JZTrBs?pli=1

chrismccord commented 3 months ago

❤️❤️❤️🐥🔥