defmodule SurfaceUtils do
@moduledoc """
SurfaceUtils provides a include/2 macro which can import props from other components.
Use the `included_props/2` function to pass included props to the component that is included.
For Example:
```
defmodule ComponentToInclude do
use Surface.Component
@doc "a boolean"
prop(boolean, :boolean)
def render(assigns) do
~F\"\"\"
ComponentToInclude
{#for prop <- __MODULE__.__props__()}
{assigns[prop.name]}
{/for}
\"\"\"
end
end
defmodule TestComponent do
use Surface.Component
import SurfaceUtils
include(ComponentToInclude)
prop(own_prop, :any)
def render(assigns) do
~F\"\"\"
{"my_props: \#{@own_prop}"}
<ComponentToInclude {...included_props(assigns, ComponentToInclude)} />
\"\"\"
end
end
```
"""
@doc """
Allows a component to include props from another component.
Props can be selectively included similarly to Elixir's `import` using `only: [list_of_names]` and `except: [list_of_names]`.
"""
defmacro include(other_component, opts \\ []) do
only = Keyword.get(opts, :only)
except = Keyword.get(opts, :except)
rename = Keyword.get(opts, :rename, [])
if is_list(only) and is_list(except) do
raise "Cannot use only and except together."
end
names =
Enum.map(
(is_list(only) && only) || except || [],
fn
{name, _} -> name
name -> name
end
)
filter =
cond do
is_list(only) ->
quote do
fn props -> Enum.filter(props, &(&1.name in unquote(names))) end
end
is_list(except) ->
quote do
fn props -> Enum.reject(props, &(&1.name in unquote(names))) end
end
true ->
quote do
fn props -> props end
end
end
maybe_rename_props =
quote do
fn props, rename_mapping ->
Enum.map(props, fn
%{name: name} = prop ->
%{prop | name: rename_mapping[name] || name}
end)
end
end
quote do
import SurfaceUtils, only: [included_props: 2]
for included_prop <-
unquote(other_component).__props__()
|> unquote(filter).()
|> unquote(maybe_rename_props).(unquote(rename)) do
Module.put_attribute(
__MODULE__,
:prop,
included_prop
)
Module.put_attribute(
__MODULE__,
:assigns,
included_prop
)
end
end
end
@doc """
Will extract the values for the props that are in the assigns for the specified component. Usefull when wrapping another component and the values for the props need to be passed on.
"""
def included_props(assigns, other_component) do
Enum.reduce(other_component.__props__(), %{}, fn %{name: name}, acc ->
if Map.get(assigns, name) do
Map.put(acc, name, assigns[name])
else
acc
end
end)
end
end