Repository: mhanberg/temple (elixir)

Issue:
Phoenix error when passing `c &live_component/1` into a default slot
First off, want to say I've loved working with Temple – what an incredibly beautiful DSL for expressing template structure and logic! I'm not sure if the situation described below is intended to be supported or not; I realize Temple isn't tightly coupled with Phoenix.

**Describe the bug**
Passing `c &live_component/1` into a default slot causes this error:
```
ArgumentError: cannot convert component TestAppWeb.TestLive.Components.TestLiveComponent with id "test-live-component" to HTML.

A component must always be returned directly as part of a LiveView template.
```
Passing a function component in the same way works fine.

Rendering with HEEx instead and passing the live component through a default slot also works fine.

**To Reproduce**
I have this live component:
```elixir
defmodule TestAppWeb.Components.TestLiveComponent do
  use TestAppWeb, :live_component

  def render(assigns) do
    temple do
      div do: "TEST LIVE COMPONENT CONTENT"
    end
  end
end
```
and this "control" function component:
```elixir
defmodule TestAppWeb.Components.TestFuncComponent do
  use TestAppWeb, :component

  def render(assigns) do
    temple do
      div do: "TEST FUNCTION COMPONENT CONTENT"
    end
  end
end
```
In a normal live view, I want to pass the live component into a standard Phoenix modal (from default generated `core_components.ex`):
```elixir
defmodule TestAppWeb.TestLive do
  use TestAppWeb, :live_view

  def render(assigns) do
    temple do
      button "phx-click": show_modal("test-modal"), do: "Show test modal"
      c &modal/1, id: "test-modal" do
        c &live_component/1,
          id: "test-live-component",
          module: TestAppWeb.TestLive.Components.TestLiveComponent
      end
    end
  end
end
```
Trying to render this, Phoenix throws the error above:
```
cannot convert component TestAppWeb.TestLive.Components.TestLiveComponent with id "test-live-component" to HTML.

A component must always be returned directly as part of a LiveView template.
```
If I replace the live component with the functional component, things work as expected:
```elixir
defmodule TestAppWeb.RealmsLive.Edit do
  use TestAppWeb, :live_view

  def render(assigns) do
    temple do
      button "phx-click": show_modal("test-modal"), do: "Show test modal"
      c &modal/1, id: "test-modal" do
        c &TestAppWeb.TestLive.Components.TestFuncComponent.render/1
        # c &live_component/1,
        #   id: "test-live-component",
        #   module: TestAppWeb.TestLive.Components.TestLiveComponent
      end
    end
  end
end
```

https://github.com/user-attachments/assets/1933285b-ea91-41b8-bf0e-e4f30d5b1d96

If I replace the temple code in the parent live view with HEEx, things also work as expected, with the live component rendering successfully into the modal's default slot:

```elixir
defmodule TestAppWeb.TestLive do
  use TestAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <div>
      <button phx-click={show_modal("test-modal")}>Show test modal</button>
      <.modal id="test-modal">
        <.live_component
          id="test-live-component"
          module={CcWeb.RealmsLive.Components.TestLiveComponent}
        />
      </.modal>
    </div>
    """
  end
end
```


https://github.com/user-attachments/assets/2fb37b4a-e48a-4bc0-9a9b-5d162cec59e4


**Expected behavior**

Ideally, this Temple code in a live view should successfully render a modal with content in the default slot from a live component:

```elixir
defmodule TestAppWeb.TestLive do
  use TestAppWeb, :live_view

  def render(assigns) do
    temple do
      button "phx-click": show_modal("test-modal"), do: "Show test modal"
      c &modal/1, id: "test-modal" do
        c &live_component/1,
          id: "test-live-component",
          module: TestAppWeb.TestLive.Components.TestLiveComponent
      end
    end
  end
end
```

<img width="1440" alt="Screen Shot 2024-12-14 at 12 24 01 PM" src="https://github.com/user-attachments/assets/26d6a90c-f796-4c96-82f1-93fa83b70f9f">


**Screenshots**

Please see screenshots/recordings inlined above for clarity

**Desktop (please complete the following information):**
 - OS: MacOS Monterey (12.3) 
 - Temple Version `{:temple, "~> 0.14.0"}`
 - Elixir Version 
 - Erlang Version 
 ```
$ elixir --version
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]

Elixir 1.17.2 (compiled with Erlang/OTP 26)
```

**Additional context**

I imagine live components represent a complex set of factors to integrate well with – my impression overall is that even the Phoenix docs recommend avoiding them if at all possible in favor of simpler function components. I'm just wondering if this is a use case that Temple intends to support? It feels like a rough edge at the moment (I can work around it with an intermediate modal component that uses HEEx instead of Temple, but that's not ideal).

Pull request description:
fix!: pass rest! assign as @rest to components
Previously, when you passed data as a `rest!` assign to a component, it
compiled it into a call that ran it throught a Map.merge with the rest
of the assigns at runtime.

This allowed you to access to data passed as `@foo` if you passed it as
`rest!: [foo: "bar"]`.

This implementation conflicted with the `Phoenix.LiveView.Engine` and
made it so LiveComponents didn't work.

This implementation also just differed from HEEx, which is probably
confusing.

Fixes #254

Thank you @SteffenDE for the help!


Patch:
diff --git a/lib/temple/renderer.ex b/lib/temple/renderer.ex
index 3f44967..3beb19c 100644
--- a/lib/temple/renderer.ex
+++ b/lib/temple/renderer.ex
@@ -117,16 +117,11 @@ defmodule Temple.Renderer do
 
     component_arguments =
       {:%{}, [],
-       arguments
+       (arguments ++ [rest: rest])
        |> Map.new()
        |> Map.merge(slot_quotes)
        |> Enum.to_list()}
 
-    component_arguments =
-      quote do
-        Map.merge(unquote(component_arguments), Map.new(unquote(rest)))
-      end
-
     expr =
       quote do
         component(
@@ -135,6 +130,7 @@ defmodule Temple.Renderer do
           {__MODULE__, __ENV__.function, __ENV__.file, __ENV__.line}
         )
       end
+      |> tag_slots(Enum.map(slots, & &1.name))
 
     state.engine.handle_expr(buffer, "=", expr)
   end
@@ -375,4 +371,10 @@ defmodule Temple.Renderer do
 
   def new_line(%{terminal_node: false}), do: "\n"
   def new_line(%{terminal_node: true}), do: ""
+
+  # the liveview engine expects the ast meta
+  # to have this metadata
+  defp tag_slots({call, meta, args}, slots) do
+    {call, [slots: slots] ++ meta, args}
+  end
 end


Test patch:
diff --git a/test/support/components.ex b/test/support/components.ex
index 9956d36..1fd97e7 100644
--- a/test/support/components.ex
+++ b/test/support/components.ex
@@ -44,8 +44,8 @@ defmodule Temple.Support.Components do
 
   def rest_component(assigns) do
     temple do
-      div do
-        "I am a basic #{@id} with #{@class}"
+      div id: @id, rest!: @rest do
+        inspect(@rest)
       end
     end
   end
diff --git a/test/temple/renderer_test.exs b/test/temple/renderer_test.exs
index 5e9b7a3..dd194d0 100644
--- a/test/temple/renderer_test.exs
+++ b/test/temple/renderer_test.exs
@@ -636,7 +636,7 @@ defmodule Temple.RendererTest do
       assert_html expected, result
     end
 
-    test "rest! attribute can mix in dynamic assigns to components" do
+    test "rest! attribute can is passed as @rest to component assigns" do
       assigns = %{
         rest: [
           class: "font-bold"
@@ -650,8 +650,8 @@ defmodule Temple.RendererTest do
 
       # heex
       expected = """
-      <div>
-        I am a basic foo with font-bold
+      <div id="foo" class="font-bold">
+        [class: &quot;font-bold&quot;]
       </div>
 
       """