lib/live_component.ex

defmodule Rephex.LiveComponent do
  @moduledoc """
  Implement utility functions and handling functions for Rephex by `use Rephex.LiveComponent`.

  ## Example

      defmodule ExampleWeb.AccountLive.ComponentA do
        use ExampleWeb, :live_component
        use Rephex.LiveComponent

        @initial_local_state %{
          count_double: 0
        }

        @impl true
        def mount(%Socket{} = socket) do
          {:ok, socket |> assign(@initial_local_state)}
        end

        @impl true
        def update(assigns, socket) do
          {:ok,
          socket
          |> propagate_rephex(assigns)
          |> assign(:count_double, assigns.rpx.count * 2)}
        end

        def handle_event("event_in_component", params, socket) do
          # Mutate Rephex state in root LiveView
          {:noreply,
          socket
          |> call_in_root(fn state -> mutation(state) end)}
        end
      end
  """
  import Phoenix.Component

  @root Rephex.root()

  alias Phoenix.LiveView.Socket
  alias Rephex.Api.KernelApi

  defmacro __using__(_opt \\ []) do
    quote do
      # Do not use Rephex.LiveComponent.Handler because it is for LiveView.
      import Rephex.LiveComponent
    end
  end

  @doc """
  Assign rephex state to socket assigns in LiveComponent.
  WARN: Do NOT use this function in LiveView (root).

  ## Example

      def update(%{rpx: _} = assigns, socket) do
        {:ok,
        socket
        |> propagate_rephex(assigns)
        |> assign(other_state)}
      end
  """
  @spec propagate_rephex(Socket.t(), map()) :: Socket.t()
  def propagate_rephex(%Socket{} = socket, %{@root => %{} = state} = _assigns) do
    socket |> assign(@root, state)
  end

  @doc """
  Call function in root LiveView.

  ## Example

      def handle_event("event_in_component", params, socket) do
        {:noreply,
        socket
        |> call_in_root(fn state -> mutation(state) end)}
      end
  """
  @spec call_in_root(v, (Socket.t() -> Socket.t())) :: v when v: any()
  def call_in_root(any, fun) when is_function(fun, 1) do
    KernelApi.send(self(), {{Rephex.LiveComponent, :call_in_root}, fun})
    any
  end
end

defmodule Rephex.LiveComponent.Handler do
  @moduledoc false
  alias Phoenix.LiveView.Socket

  defmacro __using__(_opt \\ []) do
    quote do
      @impl true
      def handle_info({{Rephex.LiveComponent, :call_in_root}, _fun} = msg, %Socket{} = socket) do
        Rephex.LiveComponent.Handler.handle_info_by_call_in_root(msg, socket)
      end
    end
  end

  def handle_info_by_call_in_root(
        {{Rephex.LiveComponent, :call_in_root}, fun} = _msg,
        %Socket{} = socket
      ) do
    if socket.parent_pid != nil,
      do: raise("Must not receive message in async on propagated state.")

    if not is_function(fun, 1), do: raise({:not_function, fun})

    {:noreply, fun.(socket)}
  end
end