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

Passing inner block content using LiveSvelte.Components macro does not work #101

Open
ahacking opened this issue Dec 13, 2023 · 5 comments

Comments

@ahacking
Copy link

Expected Outcome

Use component helpers with inner blocks, just as we can with <.svelte .../>.

Issue

I added use LiveSvelte.Components to html_helpers in lib/myapp_web.ex:

defp html_helpers do
    quote do
      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components and translation
      import MyAppWeb.CoreComponents
      import MyAppWeb.Gettext
      import LiveSvelte
      use LiveSvelte.Components       # <--- added this

      # Shortcut for generating JS commands
      alias Phoenix.LiveView.JS

      # Routes generation with the ~p sigil
      unquote(verified_routes())
    end
  end

The generated component function fails to render the inner block when passing slot content to the LiveSvelte.svelte component, as it is not picked up in LiveSvelte.Slots.filter_slots_from_assigns/1.

I did some investigation and inspected the assigns:

defmodule LiveSvelte do

# ...

def svelte(assigns) do
    init = assigns.__changed__ == nil
    dead = assigns.socket == nil or not LiveView.connected?(assigns.socket)
    IO.inspect(assigns)
   # ...

With a test component HelloWorld.svelte:

Hello World
<slot />

Rendering with <.HelloWorld /> in our markup we get the following assigns:

%{
  name: "HelloWorld",
  socket: nil,
  props: %{},
  __changed__: nil,
  __given__: %{
    name: "HelloWorld",
    socket: nil,
    props: %{},
    __changed__: nil,
    ssr: true,
    class: nil
  },
  ssr: true,
  class: nil,
  inner_block: [],
  live_json_props: %{}
}

Rendering with <.svelte name="HelloWorld">Foo</.svelte> with a block we get working slot/assigns:

%{
  name: "HelloWorld",
  socket: nil,
  props: %{},
  __changed__: nil,
  __given__: %{
    name: "HelloWorld",
    __changed__: nil,
    inner_block: [
      %{
        inner_block: #Function<6.109048978/2 in MyAppWeb.PageHTML.home/1>,
        __slot__: :inner_block
      }
    ]
  },
  ssr: true,
  class: nil,
  inner_block: [
    %{
      inner_block: #Function<6.109048978/2 in MyAppWeb.PageHTML.home/1>,
      __slot__: :inner_block
    }
  ],
  live_json_props: %{}
}

Rendering with <.HelloWorld>Foo</.HelloWorld> the generated function component we get broken assigns with the slot content nested on props:

%{
  name: "HelloWorld",
  socket: nil,
  props: %{
    inner_block: [
      %{
        inner_block: #Function<8.96579134/2 in MyAppWeb.PageHTML.home/1>,
        __slot__: :inner_block
      }
    ]
  },
  __changed__: nil,
  __given__: %{
    name: "HelloWorld",
    socket: nil,
    props: %{
      inner_block: [
        %{
          inner_block: #Function<8.96579134/2 in MyAppWeb.PageHTML.home/1>,
          __slot__: :inner_block
        }
      ]
    },
    __changed__: nil,
    ssr: true,
    class: nil
  },
  ssr: true,
  class: nil,
  inner_block: [],
  live_json_props: %{}
}

I also discovered that Phoenix actually doesn't let one dynamically create components with attrs or slots, raising an error when attempting to add an inner_block slot on the component generator:

      slot(:inner_block, required: false)

could not define slots for function CounterExample/1. Components cannot be dynamically defined or have default arguments

What can we do?

It seems the obvious way forward is for LiveSvelte.Slots to also detect slots on the props assign in its filtering.

Did you have any further insights? Is there a better way to dynamically create the component functions that is more aligned with how phoenix components work?

@woutdp
Copy link
Owner

woutdp commented Dec 13, 2023

We have to be careful with slots as they're not working great and are an experimental feature. Svelte 5 might fix it though as slots will be passed as props, and as such it would present us with a clear API for slots defined from the main component. Currently slots are a bit hacky.

That being said, probably a good idea to make slots work across the board with the current version of Svelte. Besides this, no further insight. Feel free to create a PR if you have a fix!

@ahacking
Copy link
Author

Yeah I followed so many issues that have been raised against svelte over the years in respect of passing slots and events to dynamic components (many from eco-systems wanting to support svelte) but can't due to the limitations of component initialization. It is definitely a sore point, and I am amazed it hasn't been taken seriously.

I did find a supported svelte wrapper svelte-retag which provides a web component wrapper for svelte 3 and 4 and deals with some of the slot issues you have in #52. It might just be the answer to better slot handling in the interim. I note they add support for dynamically creating slots and address many corner cases with compos-ability and rendering of custom components.

I do have a couple of modifications to components.ex and I will work up a PR for addressing this issue (once I get a few other things sorted). I will also raise a PR for handling component generation across nested directories.

@ahacking
Copy link
Author

@woutdp To follow up on this I have successfully coerced the props and slots for the component helper.

However whilst I was working through this I did also notice with any kind of nesting (using component helper or not), that the data-slots in the rendered html payload grows to include what is rendered within the element plus the rendering of data-slots on that element for its children and childrens-children with inner content being reflected in both the html and data-slots on the inner content, so as things nest you get exponential growth in the html due to recursively carrying the nested content in data-slots.

For example, using the following markup with 4 levels using the previous HelloWorld component:

<.HelloWorld>
  <.HelloWorld>
    <.HelloWorld>
      <.HelloWorld />
    </.HelloWorld>
  </.HelloWorld>
</.HelloWorld>

or similarly the non-component way:

<.svelte name="HelloWorld">
  <.svelte name="HelloWorld">
    <.svelte name="HelloWorld">
      <.svelte name="HelloWorld" />
    </.svelte>
  </.svelte>
</.svelte>

The assigns within LiveSvelte.svelte/1 that are provided to the Heex templatte <.live_json .... grow as follows:

Innermost (level 4) "HelloWorld":

%{
  init: true,
  name: "HelloWorld",
  socket: nil,
  slots: %{},
  props: %{},
  inner_block: [],
  __changed__: nil,
  __given__: %{name: "HelloWorld", __changed__: nil},
  class: nil,
  ssr: true,
  live_json_props: %{},
  ssr_render: %{
    "css" => %{"code" => "", "map" => nil},
    "head" => "",
    "html" => "Hello World\n"
  }
}

Level 3 "HelloWorld":

%{
  init: true,
  name: "HelloWorld",
  socket: nil,
  slots: %{
    default: "<script></script>\n  <div id=\"HelloWorld-6787\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n\n  </div>"
  },
  props: %{},
  __changed__: nil,
  __given__: %{
    name: "HelloWorld",
    inner_block: [
      %{
        inner_block: #Function<10.22662313/2 in MyAppWeb.PageHTML.home/1>,
        __slot__: :inner_block
      }
    ],
    __changed__: nil
  },
  class: nil,
  ssr: true,
  live_json_props: %{},
  ssr_render: %{
    "css" => %{"code" => "", "map" => nil},
    "head" => "",
    "html" => "Hello World\n<script></script>\n  <div id=\"HelloWorld-6787\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n\n  </div>"
  }
}

The level 2 "HelloWorld":

%{
  init: true,
  name: "HelloWorld",
  socket: nil,
  slots: %{
    default: "<script></script>\n  <div id=\"HelloWorld-6915\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{&quot;default&quot;:&quot;PHNjcmlwdD48L3NjcmlwdD4KICA8ZGl2IGlkPSJIZWxsb1dvcmxkLTY3ODciIGRhdGEtbmFtZT0iSGVsbG9Xb3JsZCIgZGF0YS1wcm9wcz0ie30iIGRhdGEtc3NyIGRhdGEtbGl2ZS1qc29uPSJ7fSIgZGF0YS1zbG90cz0ie30iIHBoeC11cGRhdGU9Imlnbm9yZSIgcGh4LWhvb2s9IlN2ZWx0ZUhvb2siIGNsYXNzPSIiPgogICAgPHN0eWxlPjwvc3R5bGU+CiAgICBIZWxsbyBXb3JsZAoKICA8L2Rpdj4=&quot;}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n<script></script>\n  <div id=\"HelloWorld-6787\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n\n  </div>\n  </div>"
  },
  props: %{},
  __changed__: nil,
  __given__: %{
    name: "HelloWorld",
    inner_block: [
      %{
        inner_block: #Function<8.22662313/2 in MyAppWeb.PageHTML.home/1>,
        __slot__: :inner_block
      }
    ],
    __changed__: nil
  },
  class: nil,
  ssr: true,
  live_json_props: %{},
  ssr_render: %{
    "css" => %{"code" => "", "map" => nil},
    "head" => "",
    "html" => "Hello World\n<script></script>\n  <div id=\"HelloWorld-6915\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{&quot;default&quot;:&quot;PHNjcmlwdD48L3NjcmlwdD4KICA8ZGl2IGlkPSJIZWxsb1dvcmxkLTY3ODciIGRhdGEtbmFtZT0iSGVsbG9Xb3JsZCIgZGF0YS1wcm9wcz0ie30iIGRhdGEtc3NyIGRhdGEtbGl2ZS1qc29uPSJ7fSIgZGF0YS1zbG90cz0ie30iIHBoeC11cGRhdGU9Imlnbm9yZSIgcGh4LWhvb2s9IlN2ZWx0ZUhvb2siIGNsYXNzPSIiPgogICAgPHN0eWxlPjwvc3R5bGU+CiAgICBIZWxsbyBXb3JsZAoKICA8L2Rpdj4=&quot;}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n<script></script>\n  <div id=\"HelloWorld-6787\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n\n  </div>\n  </div>"
  }
}

The top Level 1 "HelloWorld":

%{
  init: true,
  name: "HelloWorld",
  socket: nil,
  slots: %{
    default: "<script></script>\n  <div id=\"HelloWorld-7043\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{&quot;default&quot;:&quot;PHNjcmlwdD48L3NjcmlwdD4KICA8ZGl2IGlkPSJIZWxsb1dvcmxkLTY5MTUiIGRhdGEtbmFtZT0iSGVsbG9Xb3JsZCIgZGF0YS1wcm9wcz0ie30iIGRhdGEtc3NyIGRhdGEtbGl2ZS1qc29uPSJ7fSIgZGF0YS1zbG90cz0ieyZxdW90O2RlZmF1bHQmcXVvdDs6JnF1b3Q7UEhOamNtbHdkRDQ4TDNOamNtbHdkRDRLSUNBOFpHbDJJR2xrUFNKSVpXeHNiMWR2Y214a0xUWTNPRGNpSUdSaGRHRXRibUZ0WlQwaVNHVnNiRzlYYjNKc1pDSWdaR0YwWVMxd2NtOXdjejBpZTMwaUlHUmhkR0V0YzNOeUlHUmhkR0V0YkdsMlpTMXFjMjl1UFNKN2ZTSWdaR0YwWVMxemJHOTBjejBpZTMwaUlIQm9lQzExY0dSaGRHVTlJbWxuYm05eVpTSWdjR2g0TFdodmIyczlJbE4yWld4MFpVaHZiMnNpSUdOc1lYTnpQU0lpUGdvZ0lDQWdQSE4wZVd4bFBqd3ZjM1I1YkdVK0NpQWdJQ0JJWld4c2J5QlhiM0pzWkFvS0lDQThMMlJwZGo0PSZxdW90O30iIHBoeC11cGRhdGU9Imlnbm9yZSIgcGh4LWhvb2s9IlN2ZWx0ZUhvb2siIGNsYXNzPSIiPgogICAgPHN0eWxlPjwvc3R5bGU+CiAgICBIZWxsbyBXb3JsZAo8c2NyaXB0Pjwvc2NyaXB0PgogIDxkaXYgaWQ9IkhlbGxvV29ybGQtNjc4NyIgZGF0YS1uYW1lPSJIZWxsb1dvcmxkIiBkYXRhLXByb3BzPSJ7fSIgZGF0YS1zc3IgZGF0YS1saXZlLWpzb249Int9IiBkYXRhLXNsb3RzPSJ7fSIgcGh4LXVwZGF0ZT0iaWdub3JlIiBwaHgtaG9vaz0iU3ZlbHRlSG9vayIgY2xhc3M9IiI+CiAgICA8c3R5bGU+PC9zdHlsZT4KICAgIEhlbGxvIFdvcmxkCgogIDwvZGl2PgogIDwvZGl2Pg==&quot;}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n<script></script>\n  <div id=\"HelloWorld-6915\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{&quot;default&quot;:&quot;PHNjcmlwdD48L3NjcmlwdD4KICA8ZGl2IGlkPSJIZWxsb1dvcmxkLTY3ODciIGRhdGEtbmFtZT0iSGVsbG9Xb3JsZCIgZGF0YS1wcm9wcz0ie30iIGRhdGEtc3NyIGRhdGEtbGl2ZS1qc29uPSJ7fSIgZGF0YS1zbG90cz0ie30iIHBoeC11cGRhdGU9Imlnbm9yZSIgcGh4LWhvb2s9IlN2ZWx0ZUhvb2siIGNsYXNzPSIiPgogICAgPHN0eWxlPjwvc3R5bGU+CiAgICBIZWxsbyBXb3JsZAoKICA8L2Rpdj4=&quot;}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n<script></script>\n  <div id=\"HelloWorld-6787\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n\n  </div>\n  </div>\n  </div>"
  },
  props: %{},
  __changed__: nil,
  __given__: %{
    name: "HelloWorld",
    inner_block: [
      %{
        inner_block: #Function<6.22662313/2 in MyAppWeb.PageHTML.home/1>,
        __slot__: :inner_block
      }
    ],
    __changed__: nil
  },
  class: nil,
  ssr: true,
  live_json_props: %{},
  ssr_render: %{
    "css" => %{"code" => "", "map" => nil},
    "head" => "",
    "html" => "Hello World\n<script></script>\n  <div id=\"HelloWorld-7043\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{&quot;default&quot;:&quot;PHNjcmlwdD48L3NjcmlwdD4KICA8ZGl2IGlkPSJIZWxsb1dvcmxkLTY5MTUiIGRhdGEtbmFtZT0iSGVsbG9Xb3JsZCIgZGF0YS1wcm9wcz0ie30iIGRhdGEtc3NyIGRhdGEtbGl2ZS1qc29uPSJ7fSIgZGF0YS1zbG90cz0ieyZxdW90O2RlZmF1bHQmcXVvdDs6JnF1b3Q7UEhOamNtbHdkRDQ4TDNOamNtbHdkRDRLSUNBOFpHbDJJR2xrUFNKSVpXeHNiMWR2Y214a0xUWTNPRGNpSUdSaGRHRXRibUZ0WlQwaVNHVnNiRzlYYjNKc1pDSWdaR0YwWVMxd2NtOXdjejBpZTMwaUlHUmhkR0V0YzNOeUlHUmhkR0V0YkdsMlpTMXFjMjl1UFNKN2ZTSWdaR0YwWVMxemJHOTBjejBpZTMwaUlIQm9lQzExY0dSaGRHVTlJbWxuYm05eVpTSWdjR2g0TFdodmIyczlJbE4yWld4MFpVaHZiMnNpSUdOc1lYTnpQU0lpUGdvZ0lDQWdQSE4wZVd4bFBqd3ZjM1I1YkdVK0NpQWdJQ0JJWld4c2J5QlhiM0pzWkFvS0lDQThMMlJwZGo0PSZxdW90O30iIHBoeC11cGRhdGU9Imlnbm9yZSIgcGh4LWhvb2s9IlN2ZWx0ZUhvb2siIGNsYXNzPSIiPgogICAgPHN0eWxlPjwvc3R5bGU+CiAgICBIZWxsbyBXb3JsZAo8c2NyaXB0Pjwvc2NyaXB0PgogIDxkaXYgaWQ9IkhlbGxvV29ybGQtNjc4NyIgZGF0YS1uYW1lPSJIZWxsb1dvcmxkIiBkYXRhLXByb3BzPSJ7fSIgZGF0YS1zc3IgZGF0YS1saXZlLWpzb249Int9IiBkYXRhLXNsb3RzPSJ7fSIgcGh4LXVwZGF0ZT0iaWdub3JlIiBwaHgtaG9vaz0iU3ZlbHRlSG9vayIgY2xhc3M9IiI+CiAgICA8c3R5bGU+PC9zdHlsZT4KICAgIEhlbGxvIFdvcmxkCgogIDwvZGl2PgogIDwvZGl2Pg==&quot;}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n<script></script>\n  <div id=\"HelloWorld-6915\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{&quot;default&quot;:&quot;PHNjcmlwdD48L3NjcmlwdD4KICA8ZGl2IGlkPSJIZWxsb1dvcmxkLTY3ODciIGRhdGEtbmFtZT0iSGVsbG9Xb3JsZCIgZGF0YS1wcm9wcz0ie30iIGRhdGEtc3NyIGRhdGEtbGl2ZS1qc29uPSJ7fSIgZGF0YS1zbG90cz0ie30iIHBoeC11cGRhdGU9Imlnbm9yZSIgcGh4LWhvb2s9IlN2ZWx0ZUhvb2siIGNsYXNzPSIiPgogICAgPHN0eWxlPjwvc3R5bGU+CiAgICBIZWxsbyBXb3JsZAoKICA8L2Rpdj4=&quot;}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n<script></script>\n  <div id=\"HelloWorld-6787\" data-name=\"HelloWorld\" data-props=\"{}\" data-ssr data-live-json=\"{}\" data-slots=\"{}\" phx-update=\"ignore\" phx-hook=\"SvelteHook\" class=\"\">\n    <style></style>\n    Hello World\n\n  </div>\n  </div>\n  </div>"
  }
}

Is there something we can do in the rendering strategy to avoid this exponential growth?

@woutdp
Copy link
Owner

woutdp commented Dec 15, 2023

Oh that is interesting, I didn't think of this issue. Not seeing a way around it though with how data is being passed to Svelte at the moment. Maybe in a better scenario we wouldn't pass the data through an html attribute, maybe it's possible to do it with straight JS, that way at least the HTML wouldn't grow exponentially. Just thinking out loud here

@ahacking
Copy link
Author

ahacking commented Dec 15, 2023

I was looking at a few options including live-state and live-data for transporting state.

Then I began investigating use of web components which I actually think might be the answer for wrapping Svelte components using Lit and liveview support for working with web components using live_elements. Lit also supports SSR and client side hydration and slot binding is possible with Declarative Shadow DOM.

It is likely I will move forward with using either Shoelace web components but more probably the new Adobe Spectrum 2 web components as they have built all their apps on web components, so it should be a fairly solid design language, fully functional, well documented and tested

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

No branches or pull requests

2 participants