defmodule Surface.Component do
@moduledoc """
Defines a stateless component.
## Example
defmodule Button do
use Surface.Component
prop click, :event
def render(assigns) do
~F"\""
<button class="button" :on-click={@click}>
<#slot/>
</button>
"\""
end
end
> **Note**: Stateless components cannot handle Phoenix LiveView events.
If you need to handle them, please use a `Surface.LiveComponent` instead.
"""
alias Surface.IOHelper
@callback render(assigns :: Socket.assigns()) :: Phoenix.LiveView.Rendered.t()
defmacro __using__(opts \\ []) do
slot_name = Keyword.get(opts, :slot)
if slot_name do
validate_slot_name!(slot_name, __CALLER__)
end
slot_name = slot_name && String.to_atom(slot_name)
quote do
@before_compile Surface.Renderer
@before_compile unquote(__MODULE__)
use Phoenix.Component
import Phoenix.Component, except: [slot: 1, slot: 2]
@behaviour unquote(__MODULE__)
use Surface.BaseComponent, type: unquote(__MODULE__)
use Surface.API, include: [:prop, :slot, :data]
import Phoenix.HTML
@before_compile {Surface.BaseComponent, :__before_compile_init_slots__}
@before_compile {unquote(__MODULE__), :__before_compile_handle_from_context__}
alias Surface.Components.{Context, Raw}
alias Surface.Components.Dynamic.Component
@doc "Built-in assign"
data inner_block, :fun
defmacro __using__(opts) do
alias_opts = Keyword.take(opts, [:as])
quote do
alias unquote(__MODULE__), unquote(alias_opts)
Module.put_attribute(__MODULE__, :__compile_time_deps__, unquote(__MODULE__))
end
end
if unquote(slot_name) != nil do
Module.put_attribute(__MODULE__, :__slot_name__, unquote(slot_name))
def __slot_name__ do
unquote(slot_name)
end
end
end
end
defp validate_slot_name!(name, caller) do
if !is_binary(name) do
message = "invalid value for option :slot. Expected a string, got: #{inspect(name)}"
IOHelper.compile_error(message, caller.file, caller.line)
end
end
defmacro __before_compile__(env) do
quoted_render(env)
end
defmacro __before_compile_handle_from_context__(env) do
props_specs = env.module |> Surface.API.get_props() |> Enum.reverse()
data_specs = env.module |> Surface.API.get_data() |> Enum.reverse()
quoted_props_assigns =
for %{name: name, opts: opts} <- props_specs, key = opts[:from_context] do
quote do
var!(assigns) =
Surface.Components.Context.maybe_copy_assign(var!(assigns), unquote(key), as: unquote(name))
end
end
quoted_data_assigns =
for %{name: name, opts: opts} <- data_specs, key = opts[:from_context] do
quote do
var!(assigns) = Surface.Components.Context.copy_assign(var!(assigns), unquote(key), as: unquote(name))
end
end
quoted_assigns = {:__block__, [], quoted_data_assigns ++ quoted_props_assigns}
if Module.defines?(env.module, {:render, 1}) do
quote do
defoverridable render: 1
def render(var!(assigns)) do
unquote(quoted_assigns)
super(var!(assigns))
end
end
end
end
defp quoted_render(env) do
if !Module.defines?(env.module, {:__slot_name__, 0}) ||
Module.defines?(env.module, {:render, 1}) do
quote do
@doc false
def __renderless__? do
false
end
end
else
quote do
@doc false
def __renderless__? do
true
end
def render(var!(assigns)) do
~F()
end
end
end
end
end