Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent event bubbling on file upload targets #3003

Merged
merged 1 commit into from
Jan 11, 2024

Conversation

Gazler
Copy link
Member

@Gazler Gazler commented Jan 11, 2024

In cases where an event is dispatched to live_file_input the event keeps triggering until a call stack error is raised:

Uncaught RangeError: Maximum call stack size exceeded.

This can be triggered with something like:

<div phx-click={Phoenix.LiveView.JS.dispatch("click", to: "##{@uploads.image.ref}")}>
Upload
</div>
<form style="display: none;" id="upload-form" phx-submit="save" phx-change="validate">
  <.live_file_input upload={@uploads.image} />
  <button type="submit">Upload</button>
</form>

To fix this, we set bubble to false if the event target is a file input.

In cases where an event is dispatched to  `live_file_input` the event
keeps triggering until a call stack error is raised:

  Uncaught RangeError: Maximum call stack size exceeded.

This can be triggered with something like:

    <div phx-click={Phoenix.LiveView.JS.dispatch("click", to: "##{@uploads.image.ref}")}>
    Upload
    </div>
    <form style="display: none;" id="upload-form" phx-submit="save" phx-change="validate">
      <.live_file_input upload={@uploads.image} />
      <button type="submit">Upload</button>
    </form>

To fix this, we set bubble to false if the event target is a file input.
@Gazler
Copy link
Member Author

Gazler commented Jan 11, 2024

Here's the code to reproduce the bug:

# Based on https://hexdocs.pm/phoenix_live_view/0.18.18/uploads.html

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

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7.2"},
  {:phoenix_live_view, "~> 0.18.18"}
])

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}

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:uploaded_urls, [])
     |> allow_upload(
       :image,
       accept: ~w(.jpg .jpeg .png),
       max_entries: 1
     )}
  end

  @impl true
  def handle_event("validate", _params, socket) do
    {:noreply, socket}
  end

  @impl true
  def handle_event("save", _params, socket) do
    tmp_dir = Path.join(System.tmp_dir!(), "phoenix_live_view_upload_example")
    File.mkdir_p!(tmp_dir)

    uploaded_urls =
      consume_uploaded_entries(socket, :image, fn %{path: path}, entry ->
        dest = Path.join(tmp_dir, entry.client_name)
        File.cp!(path, dest)
        {:ok, "/uploads/#{entry.client_name}"}
      end)

    {:noreply, update(socket, :uploaded_urls, &(&1 ++ uploaded_urls))}
  end

  @impl true
  def handle_event("cancel-upload", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :image, ref)}
  end

  @impl true
  def render("live.html", assigns) do
    ~H"""
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/priv/static/phoenix.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/priv/static/phoenix_live_view.min.js"></script>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <%= @inner_content %>
    """
  end

  @impl true
  def render(assigns) do
    ~H"""
    <h1>Phoenix LiveView Upload Image Example</h1>
    <div phx-drop-target={@uploads.image.ref} phx-click={Phoenix.LiveView.JS.dispatch("click", to: "##{@uploads.image.ref}")}>
    Click to upload

      <form style="display: none;" id="upload-form" phx-submit="save" phx-change="validate">
        <.live_file_input upload={@uploads.image} />
        <button type="submit">Upload</button>
      </form>

      <%= for entry <- @uploads.image.entries do %>
        <article class="upload-entry">
          <h2 id="lol">Preview</h2>

          <figure>
            <.live_img_preview entry={entry} width="400" />
            <figcaption><%= entry.client_name %></figcaption>
          </figure>

          <progress value={entry.progress} max="100"><%= entry.progress %>%</progress>

          <button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref} aria-label="cancel">&times;</button>

          <%= for err <- upload_errors(@uploads.image, entry) do %>
            <p class="alert alert-danger">1: <%= err %></p>
          <% end %>

        </article>
      <% end %>

      <%= for err <- upload_errors(@uploads.image) do %>
        <p class="alert alert-danger">2: <%= err %></p>
      <% end %>
    </div>

    <h2>Uploaded Files</h2>

    <p :if={@uploaded_urls == []}>No files yet.</p>
    <div :for={url <- @uploaded_urls}>
      <img src={url} style="max-width: 400px">
    </div>
    """
  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)
  end
end

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

  plug(Plug.Static,
    at: "/uploads",
    from: Path.join(System.tmp_dir!(), "phoenix_live_view_upload_example")
  )

  plug(Example.Router)
end

children = [
  {Phoenix.PubSub, name: Example.PubSub},
  Example.Endpoint
]

{:ok, _} = Supervisor.start_link(children, strategy: :one_for_one)

# unless running from IEx, sleep idenfinitely so we can serve requests
unless IEx.started?() do
  Process.sleep(:infinity)
end

@Gazler
Copy link
Member Author

Gazler commented Jan 11, 2024

I'm not sure if we should also restrict the event type to only prevent bubbling on click events in this instance?

@chrismccord chrismccord merged commit 7781a38 into main Jan 11, 2024
8 checks passed
@chrismccord
Copy link
Member

❤️❤️❤️🐥🔥

@chrismccord chrismccord deleted the gr-prevent-bubbling-on-upload branch January 11, 2024 13:47
@SteffenDE
Copy link
Collaborator

I'm currently looking into #2965 and this PR prevents the "track-uploads" event

else { DOM.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, {detail: {files: filesOrBlobs}}) }
from reaching the handler
this.on(PHX_TRACK_UPLOADS, e => {

@josevalim
Copy link
Member

Looks good to me, please do send a PR!

chrismccord pushed a commit that referenced this pull request Jan 17, 2024
In cases where an event is dispatched to  `live_file_input` the event
keeps triggering until a call stack error is raised:

  Uncaught RangeError: Maximum call stack size exceeded.

This can be triggered with something like:

    <div phx-click={Phoenix.LiveView.JS.dispatch("click", to: "##{@uploads.image.ref}")}>
    Upload
    </div>
    <form style="display: none;" id="upload-form" phx-submit="save" phx-change="validate">
      <.live_file_input upload={@uploads.image} />
      <button type="submit">Upload</button>
    </form>

To fix this, we set bubble to false if the event target is a file input.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants