mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
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:
parent
fa242ab419
commit
2f5ecbc077
7 changed files with 230 additions and 5 deletions
|
|
@ -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
26
lib/dynamic_slots.ex
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
17
lib/slots.ex
17
lib/slots.ex
|
|
@ -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
72
test/components_test.exs
Normal 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
|
||||
23
test/dynamic_slots_test.exs
Normal file
23
test/dynamic_slots_test.exs
Normal 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
79
test/slots_test.exs
Normal 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
|
||||
Loading…
Reference in a new issue