lib/love/events.ex

defmodule Love.Events do
  @moduledoc """
  Send and receive event messages between views and components.

  The `use Love.View` and `use Love.Component` macros automatically use this behaviour.
  """

  @doc """
  Handle an event message sent by `Love.Component.emit/3` or `send_message/4`.

  When sent from a component via `Love.Component.emit/3`, the `source` takes the form `{module, id}`.

  ## Example

      # Emitted from a component
      def handle_message(:on_selected, {MyComponent, _id}, %{profile_id: profile_id}, socket), do: ...

  """
  @callback handle_message(
              name :: atom,
              source :: any,
              payload :: any,
              socket :: LiveView.Socket.t()
            ) ::
              socket :: LiveView.Socket.t()

  @optional_callbacks handle_message: 4

  @type destination :: pid | {module, String.t() | atom}

  @doc """
  Sends an event message to a `Love.View` or a `Love.Component`.

  To send to a `Love.View` (or any other process), specify a `pid` (usually `self()`) as the `destination`.
  To send to a `Love.Component`, specify `{module, id}` as the `destination`.

  It can be handled by the `c:Love.Events.handle_message/4` callback.

  When sending to an arbitrary process, the message will be an `Love.Events.Message` struct.

  ## Options

  - `:source` - where the event originated from; defaults to `nil`

  ## Examples

      send_message(self(), :on_selected, %{profile_id: 123})
      # => def handle_message(:on_selected, _source, %{profile_id: id}, socket), do: ...

      send_message({MyComponent, "my-id"}, :on_selected, %{profile_id: 123})
      # => def handle_message(:on_selected, _source, %{profile_id: id}, socket), do: ...
  """
  @spec send_message(destination :: destination(), name :: atom, payload :: any, opts :: keyword) ::
          :ok
  def send_message(destination, name, payload, opts \\ []) do
    message = %Love.Events.Message{name: name, source: opts[:source], payload: payload}

    case destination do
      pid when is_pid(pid) ->
        send(pid, message)

      {module, id} when is_atom(module) ->
        Phoenix.LiveView.send_update(module, id: id, __message__: message)
    end

    :ok
  end
end