Fixes issue with slot warnings and use of slots when using LiveSvelte use macro (#196)

* Fixes issue with slot warnings and use of slots when using LiveSvelte. Components use macro
This commit is contained in:
John Barker 2026-02-02 16:19:33 -05:00 committed by GitHub
parent fa242ab419
commit 2f5ecbc077
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 230 additions and 5 deletions

View file

@ -23,7 +23,16 @@ defmodule LiveSvelte.Components do
defp name_to_function(name) do
quote do
def unquote(:"#{name}")(assigns) do
props = Map.drop(assigns, [:__changed__, :__given__, :ssr, :class, :socket])
# Extract slot assigns to pass through to LiveSvelte.svelte
slot_assigns = LiveSvelte.Slots.filter_slots_from_assigns(assigns)
# Filter out reserved keys and slots from props
# Slots contain functions (inner_block) that can't be JSON encoded
props =
assigns
|> Map.drop([:__changed__, :__given__, :ssr, :class, :socket])
|> Map.drop(Map.keys(slot_assigns))
|> Enum.into(%{})
var!(assigns) =
assigns
@ -32,6 +41,7 @@ defmodule LiveSvelte.Components do
|> Map.put_new(:class, nil)
|> Map.put_new(:socket, nil)
|> assign(:props, props)
|> assign(:__slot_assigns, slot_assigns)
~H"""
<LiveSvelte.svelte
@ -40,6 +50,7 @@ defmodule LiveSvelte.Components do
socket={@socket}
ssr={@ssr}
props={@props}
{@__slot_assigns}
/>
"""
end

26
lib/dynamic_slots.ex Normal file
View file

@ -0,0 +1,26 @@
defmodule LiveSvelte.DynamicSlots do
@moduledoc false
@doc false
defmacro __before_compile__(_env) do
quote do
# Mark Phoenix's generated __components__/0 as overridable, then override it.
# Phoenix generates __components__/0 but doesn't mark it as overridable.
defoverridable __components__: 0
# Override Phoenix's __components__/0 to skip slot validation for the svelte component.
#
# Phoenix LiveView 1.x uses __components__/0 to get component definitions for validation.
# The verification code does:
# component = submod.__components__()[fun]
#
# If this returns nil, the verification is skipped entirely for that component.
# This allows LiveSvelte to accept arbitrary slot names without warnings.
def __components__ do
# Return empty map so __components__()[:svelte] returns nil
# This skips slot/attr validation entirely for the svelte component
%{}
end
end
end
end

View file

@ -13,6 +13,11 @@ defmodule LiveSvelte do
alias LiveSvelte.Slots
alias LiveSvelte.SSR
# Override Phoenix's slot validation to accept arbitrary slot names.
# This allows users to pass any named slot to Svelte components without
# getting "undefined slot" warnings during compilation.
@before_compile LiveSvelte.DynamicSlots
attr :props, :map,
default: %{},
doc: "Props to pass to the Svelte component",

View file

@ -27,12 +27,21 @@ defmodule LiveSvelte.Slots do
|> Enum.into(%{})
end
@doc false
defp filter_slots_from_assigns(assigns) do
@doc """
Filters assigns to return only slot entries.
Slots are identified by having a list value containing maps with `__slot__` key.
"""
def filter_slots_from_assigns(assigns) do
assigns
|> Enum.filter(fn
{_key, [%{__slot__: _}]} -> true
_ -> false
{_key, value} when is_list(value) ->
Enum.any?(value, fn
%{__slot__: _} -> true
_ -> false
end)
_ ->
false
end)
|> Enum.into(%{})
end

72
test/components_test.exs Normal file
View file

@ -0,0 +1,72 @@
defmodule LiveSvelte.ComponentsTest do
use ExUnit.Case, async: true
alias LiveSvelte.Slots
describe "slot handling in generated components" do
test "slots are properly identified and separated from props" do
# Simulate assigns that would be passed to a generated component
button_slot = %{__slot__: :button, inner_block: fn _, _ -> "button content" end}
items_slot = %{__slot__: :items, inner_block: fn _, _ -> "items content" end}
assigns = %{
__changed__: nil,
__given__: %{},
ssr: true,
class: "my-class",
socket: nil,
button: [button_slot],
items: [items_slot],
custom_prop: "value",
another_prop: 123
}
# Extract slots
slot_assigns = Slots.filter_slots_from_assigns(assigns)
# Build props by filtering out reserved keys and slots
props =
assigns
|> Map.drop([:__changed__, :__given__, :ssr, :class, :socket])
|> Map.drop(Map.keys(slot_assigns))
|> Enum.into(%{})
# Verify slots were extracted
assert Map.has_key?(slot_assigns, :button)
assert Map.has_key?(slot_assigns, :items)
# Verify props don't contain slots (which would cause JSON encoding errors)
refute Map.has_key?(props, :button)
refute Map.has_key?(props, :items)
# Verify props contain the actual props
assert props[:custom_prop] == "value"
assert props[:another_prop] == 123
# Verify props can be JSON encoded (slots contain functions which can't)
assert {:ok, _} = Jason.encode(props)
end
test "slots with functions cannot be JSON encoded" do
slot = %{__slot__: :test, inner_block: fn -> "content" end}
# This is what was causing the original error
assert_raise Protocol.UndefinedError, fn ->
Jason.encode!(%{slot: [slot]})
end
end
test "props without slots can be JSON encoded" do
props = %{
name: "TestComponent",
count: 42,
active: true,
items: ["a", "b", "c"],
nested: %{foo: "bar"}
}
assert {:ok, json} = Jason.encode(props)
assert is_binary(json)
end
end
end

View file

@ -0,0 +1,23 @@
defmodule LiveSvelte.DynamicSlotsTest do
use ExUnit.Case, async: true
describe "__components__/0" do
test "returns empty map to skip Phoenix validation" do
# LiveSvelte.__components__() should return %{} so that
# __components__()[:svelte] returns nil, skipping validation
assert LiveSvelte.__components__() == %{}
end
test "looking up :svelte returns nil" do
# This is what Phoenix does during validation
# When it returns nil, validation is skipped
assert LiveSvelte.__components__()[:svelte] == nil
end
end
describe "slot validation bypass" do
test "LiveSvelte module has __components__/0 defined" do
assert function_exported?(LiveSvelte, :__components__, 0)
end
end
end

79
test/slots_test.exs Normal file
View file

@ -0,0 +1,79 @@
defmodule LiveSvelte.SlotsTest do
use ExUnit.Case, async: true
alias LiveSvelte.Slots
describe "filter_slots_from_assigns/1" do
test "returns empty map when no slots present" do
assigns = %{name: "Test", props: %{foo: "bar"}}
assert Slots.filter_slots_from_assigns(assigns) == %{}
end
test "filters out slot entries from assigns" do
slot_entry = %{__slot__: :button, inner_block: fn -> "content" end}
assigns = %{
name: "Test",
button: [slot_entry],
props: %{foo: "bar"}
}
result = Slots.filter_slots_from_assigns(assigns)
assert Map.has_key?(result, :button)
assert result[:button] == [slot_entry]
refute Map.has_key?(result, :name)
refute Map.has_key?(result, :props)
end
test "handles multiple slots" do
button_slot = %{__slot__: :button, inner_block: fn -> "button" end}
items_slot = %{__slot__: :items, inner_block: fn -> "items" end}
assigns = %{
name: "Test",
button: [button_slot],
items: [items_slot],
props: %{}
}
result = Slots.filter_slots_from_assigns(assigns)
assert Map.keys(result) |> Enum.sort() == [:button, :items]
end
test "handles slots with multiple entries" do
slot1 = %{__slot__: :item, inner_block: fn -> "item1" end}
slot2 = %{__slot__: :item, inner_block: fn -> "item2" end}
assigns = %{
item: [slot1, slot2],
name: "Test"
}
result = Slots.filter_slots_from_assigns(assigns)
assert result[:item] == [slot1, slot2]
end
test "ignores non-list values" do
assigns = %{
name: "Test",
count: 5,
active: true,
data: %{nested: "value"}
}
assert Slots.filter_slots_from_assigns(assigns) == %{}
end
test "ignores lists without slot maps" do
assigns = %{
items: ["a", "b", "c"],
numbers: [1, 2, 3],
maps: [%{foo: "bar"}]
}
assert Slots.filter_slots_from_assigns(assigns) == %{}
end
end
end