phoenixframework / phoenix_live_view

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

phx-hook not working on an element mounted by a LiveComponent #3423

Closed borisgoro closed 1 month ago

borisgoro commented 2 months ago

Environment

Code: https://github.com/borisgoro/hooks_test

I created the app with mix phx.new hooks_test and tried upgrading the live view version to 0.20.12, ..., 1.0.0-rc.6 with the same result.

Define two hooks:

const liveSocket = new LiveSocket('/live', Socket, {
    params: { _csrf_token: csrfToken },
    hooks: {
      Hook1: {
        mounted () {
          console.log('Hook1 mounted')
        }
      },
      Hook2: {
        mounted () {
          console.log('Hook2 mounted')
        }
      }
    }
  })

and a LiveComponent with a "panel" for each hook:

defmodule HooksTestWeb.TestLiveComponent do
  use HooksTestWeb, :live_component

  @impl true
  def render(%{panel: 1} = assigns) do
    ~H"""
    <div id="panel1" phx-hook="Hook1" class="mt-2 p-2 border-2">
      <.panel panel="1" target={@myself} />
    </div>
    """
  end

  def render(%{panel: 2} = assigns) do
    ~H"""
    <div id="panel2" phx-hook="Hook2" class="mt-2 p-2 border-2">
      <.panel panel="2" target={@myself} />
    </div>
    """
  end

  def panel(assigns) do
    ~H"""
    <div><%= "Panel #{@panel}" %></div>
    <button class="mt-2 p-1 border-2" phx-click="panel1" phx-target={@target}>Panel 1</button>
    <button class="mt-2 p-1 border-2" phx-click="panel2" phx-target={@target}>Panel 2</button>
    """
  end

  @impl true
  def handle_event("panel1", _, socket) do
    {:noreply, assign(socket, panel: 1)}
  end

  def handle_event("panel2", _, socket) do
    {:noreply, assign(socket, panel: 2)}
  end
end

Hook1.mounted() is called on the initial render, but Hook2.mounted() is not called when the Panel 2 button is clicked.

Identical code (without the phx-target stuff) works fine in a LiveView:

defmodule HooksTestWeb.HomeLive do
  use Phoenix.LiveView, layout: {HooksTestWeb.Layouts, :app}

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, panel: 1)}
  end

  # @impl true
  # def render(assigns) do
  #   ~H"""
  #   <.live_component id="test-lc" panel={@panel} module={HooksTestWeb.TestLiveComponent} />
  #   """
  # end

  @impl true
  def render(%{panel: 1} = assigns) do
    ~H"""
    <div id="panel1" phx-hook="Hook1" class="mt-2 p-2 border-2">
      <.panel panel="1" />
    </div>
    """
  end

  def render(%{panel: 2} = assigns) do
    ~H"""
    <div id="panel2" phx-hook="Hook2" class="mt-2 p-2 border-2">
      <.panel panel="2" />
    </div>
    """
  end

  def panel(assigns) do
    ~H"""
    <div><%= "Panel #{@panel}" %></div>
    <button class="mt-2 p-1 border-2" phx-click="panel1">Panel 1</button>
    <button class="mt-2 p-1 border-2" phx-click="panel2">Panel 2</button>
    """
  end

  @impl true
  def handle_event("panel1", _, socket) do
    {:noreply, assign(socket, panel: 1)}
  end

  def handle_event("panel2", _, socket) do
    {:noreply, assign(socket, panel: 2)}
  end
end

Actual behavior

Browser console displays Hook1 mounted on initial render, nothing when buttons are clicked.

Expected behavior

Browser console displays Hook1 mounted and Hook2 mounted when the corresponding button is clicked.

josevalim commented 1 month ago

We should try address this but, given that the root element of live components are treated especially, we should forbid phx-hook at the root of LiveComponents.