lib/moon/helpers/moon_render.ex

defmodule Moon.Helpers.MoonRender do
  @moduledoc """
  Functions for rendering Moon (or Surface, either LiveView) components in HEEX/SLIM/... (not Surface) templates
  """

  alias Phoenix.LiveView.{TagEngine, HTMLEngine}
  import Phoenix.Component, only: [live_component: 1]

  defp component(func, assigns, caller) do
    module =
      if function_exported?(TagEngine, :component, 3) do
        # phoenix_live_view 0.18.18
        TagEngine
      else
        # phoenix_live_view < 0.18.18
        HTMLEngine
      end

    apply(module, :component, [func, assigns, caller])
  end

  defp get_default_props(module) do
    Enum.reduce(module.__props__(), %{__context__: %{}}, fn
      %{name: name, opts: opts}, acc ->
        Map.put(acc, name, Keyword.get(opts, :default))
    end)
  end

  defp transform_slots(
         props = %{inner_block: [%{__slot__: :inner_block, inner_block: inner_block}]}
       ) do
    props
    |> Map.delete(:inner_block)
    |> Map.put(:default, [%{__slot__: :default, inner_block: inner_block}])
  end

  defp transform_slots(props), do: props

  @doc "Used for rendering stateless Surface componet"
  def surface_component(module, props) do
    component(
      &module.render/1,
      get_default_props(module) |> Map.merge(transform_slots(props)),
      {__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line}
    )
  end

  def surface_component(props = %{module: module}),
    do: surface_component(module, props |> Map.delete(:module))

  @doc "Used for rendering live (stateful) Surface component"
  def surface_live_component(props = %{module: module}) do
    get_default_props(module) |> Map.merge(transform_slots(props)) |> live_component()
  end

  def surface_live_component(module, props),
    do: surface_live_component(Map.put(props, :module, module))

  def is_live(module) do
    %{kind: :component} = module.__live__()
    true
  rescue
    [UndefinedFunctionError, MatchError] ->
      false
  end

  defp get_render_function(module) do
    cond do
      !function_exported?(module, :__props__, 0) ->
        &live_component/1

      is_live(module) ->
        &surface_live_component/1

      true ->
        &surface_component/1
    end
  end

  @doc """
  A function for rendering Surface components inside non-surface templates
  usage: `<.moon module="Button">hit me!</.moon>`
  Please note that only limited functional is supported, e.g. context get & named
  slots are out of scope, sorry.
  """
  def moon(props = %{module: module}) do
    get_render_function(module).(props)
  end
end