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

When navigating from a sticky child live view, <.link> won't have its phx-click-loading cleared #3656

Closed
tmjoen opened this issue Jan 30, 2025 · 0 comments · Fixed by #3659
Closed

Comments

@tmjoen
Copy link
Contributor

tmjoen commented Jan 30, 2025

Environment

  • Elixir version (elixir -v): Elixir 1.18.1
  • Phoenix version (mix deps): 1.7.18
  • Phoenix LiveView version (mix deps): master
  • Operating system: OSX 14.5
  • Browsers you attempted to reproduce this bug on (the more the merrier): Chrome, FF
  • Does the problem persist after removing "assets/node_modules" and trying again? Yes/no: Yes

Actual behavior

When navigating from a sticky child live view, <.link> won't have its phx-click-loading cleared. This means that every link clicked in the sticky navigation child live view will have phx-click-loading classes, which fails my E2E tests that wait for the LV to sync.

Here is a (rather crappy) repro:

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}
])

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

defmodule Example.Nav do
  use Phoenix.LiveView, layout: {__MODULE__, :live_child}

  def mount(_, %{"current_url" => url}, socket) do
    {:ok, socket |> assign(:current_url, url)}
  end

  def render("live_child.html", assigns) do
    ~H"""
    {@inner_content}
    """
  end

  def render(assigns) do
    ~H"""
    <nav>
      <.link navigate="/p/1" class={active(@current_url, "/p/1")}>Link 1</.link>
      <.link navigate="/p/2" class={active(@current_url, "/p/2")}>Link 2</.link>
      <.link navigate="/p/3" class={active(@current_url, "/p/3")}>Link 3</.link>
    </nav>
    """
  end

  defp active(current_url, url) do
    if current_url == url, do: "active", else: nil
  end
end

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

  def handle_params(params, url, socket) do
    {:noreply, assign(socket, :url, URI.parse(url).path)}
  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>
      window.addEventListener('phx:page-loading-stop', ({ detail }) => {
        if (detail.kind === 'redirect') {
          // remove current active
          const currentActiveItem = document.querySelector('nav .active')
          if (currentActiveItem) {
            currentActiveItem.classList.remove('active')
            // currentActiveItem.classList.remove('phx-click-loading')
          }

          const newActiveItem = document.querySelector(
            `nav [data-phx-link][href="${window.location.pathname + window.location.search}"]`
          )
          if (newActiveItem) {
            newActiveItem.classList.add('active')
            // newActiveItem.classList.remove('phx-click-loading')
          }
        }
      })

      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <style>
      * { font-size: 1.1em }
      nav { margin-top: 1em }
      nav a { padding: 8px 16px; border: 1px solid black; text-decoration: none }
      nav a:visited { color: inherit }
      nav a.active { border: 3px solid green }
      nav a.phx-click-loading { animation: pulsate 2s infinite }
      @keyframes pulsate {
        0% {
          background-color: white;
        }
        50% {
          background-color: red;
        }
        100% {
          background-color: white;
        }
      }
    </style>
    <body>
      <header>
        {live_render(@socket, Example.Nav,
          id: "app-nav",
          session: %{"current_url" => @url},
          sticky: true
        )}
      </header>
      <main>
        {@inner_content}
      </main>
    </body>
    """
  end

  def render(assigns) do
    ~H"""
    <p style="margin-top: 2em; max-width: 55ch">
      When clicking any of the above links in the sticky child LiveView, `phx-click-loading` is not cleared
      after navigation.
    </p>
    """
  end
end

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

  def mount(%{"id" => id}, _session, socket) do
    {:ok, assign(socket, :id, id)}
  end

  def handle_params(params, url, socket) do
    {:noreply, assign(socket, :url, URI.parse(url).path)}
  end

  def render(assigns) do
    ~H"""
    <h1>Page {@id}</h1>
    """
  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_session :default do
      live("/", HomeLive, :index)
      live("/p/:id", PageLive, :show)
    end
  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)

Expected behavior

Not sure what I expect really, I can solve it like the commented out JS line in the example repro above:

// currentActiveItem.classList.remove('phx-click-loading')

but @SteffenDE mentioned that it COULD be a bug so I created a repro, just in case :)

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 a pull request may close this issue.

1 participant