lib/ex_live_url.ex

defmodule ExLiveUrl do
  @moduledoc ~S'''
  `ExLiveUrl` is just some simple Phoenix LiveView lifecycle hooks and helper functions. It helps you store the live view's current query params and path in your assigns. Additionally, it exposes ways to work with those values both synchronously (only from the root live view) and asynchronously (from anywhere).

  # Installation

  You can install `ExLiveUrl` like so:

  ```elixir
  defp deps do
    [
      {:ex_live_url, "~> 0.2.0"}
    ]
  end
  ```

  To use `ExLiveUrl` you will need to call `Phoenix.LiveView.on_mount/1` with the module, `ExLiveUrl` in your live view. For example:

  ```elixir
  defmodule YourLiveView do
    use Phoenix.LiveView

    on_mount ExLiveUrl

    # your live view implementation
  end
  ```
  '''
  @moduledoc since: "0.1.0"

  @doc false
  def on_mount(key, _params, _session, socket) do
    {:cont,
     socket
     |> Phoenix.LiveView.attach_hook(__MODULE__, :handle_params, fn params, uri, socket ->
       {:cont, Phoenix.Component.assign(socket, key, ExLiveUrl.Url.new(params, uri))}
     end)
     |> Phoenix.LiveView.attach_hook(__MODULE__, :handle_info, fn maybe_operation, socket ->
       if ExLiveUrl.Operation.is_operation?(maybe_operation) do
         {:halt, ExLiveUrl.Operation.apply(maybe_operation, socket.assigns[key], socket)}
       else
         {:cont, socket}
       end
     end)}
  end

  @doc """
  Asynchronously annotates the socket for navigation within the current LiveView. Whenever this operation is eventually executed it calls `Phoenix.LiveView.push_navigate/2` internally.

  ## Options

  - `:to` - a function which takes the current `ExLiveUrl.Url` and must return either a new `ExLiveUrl.Url` or a relative url string.
  - `:replace` - the flag to replace the current history or push a new state. Defaults false.

  ## Examples

  ```elixir
  ExLiveUrl.push_patch(to: fn url -> %ExLiveUrl.Url{url | path: ExLiveUrl.Path.new("/")} end)
  ExLiveUrl.push_patch(to: fn _url -> "/"} end)
  ExLiveUrl.push_patch(
    to: fn url -> %ExLiveUrl.Url{url | path: ExLiveUrl.Path.new("/")} end,
    replace: true
  )
  ```
  """
  @doc since: "0.3.0"
  def push_patch(pid \\ self(), opts) do
    opts |> ExLiveUrl.PushPatchOperation.new() |> ExLiveUrl.Operation.send(pid)
  end

  @doc """
  Asynchronously annotates the socket for navigation to another LiveView. Whenever this operation is eventually executed it calls `Phoenix.LiveView.push_navigate/2` internally.

  ## Options

  - `:to` - a function which takes the current `ExLiveUrl.Url` and must return either a new `ExLiveUrl.Url` or a relative url string.
  - `:replace` - the flag to replace the current history or push a new state. Defaults false.

  ## Examples

  ```elixir
  ExLiveUrl.push_navigate(to: fn url -> %ExLiveUrl.Url{url | path: ExLiveUrl.Path.new("/")} end)
  ExLiveUrl.push_navigate(to: fn _url -> "/"} end)
  ExLiveUrl.push_navigate(
    to: fn url -> %ExLiveUrl.Url{url | path: ExLiveUrl.Path.new("/")} end,
    replace: true
  )
  ```
  """
  @doc since: "0.3.0"
  def push_navigate(pid \\ self(), opts) do
    opts |> ExLiveUrl.PushNavigateOperation.new() |> ExLiveUrl.Operation.send(pid)
  end

  @doc """
  Asynchronously annotates the socket for redirect to a destination path. Whenever this operation is eventually executed it calls `Phoenix.LiveView.redirect/2` internally.

  > Note: LiveView redirects rely on instructing client to perform a `window.location` update on the provided redirect location. The whole page will be reloaded and all state will be discarded.

  ## Options

  - `:to` - a function which takes the current `ExLiveUrl.Url` and must return either a new `ExLiveUrl.Url` or a relative url string. It must always be a local path.
  - `:external` - a function which takes the current `ExLiveUrl.Url` and must return either a new `ExLiveUrl.Url` or a fully qualified url string.

  ## Examples

  ```elixir
  ExLiveUrl.redirect(to: fn url -> %ExLiveUrl.Url{url | path: ExLiveUrl.Path.new("/")} end)
  ExLiveUrl.redirect(to: fn _url -> "/"} end)
  ExLiveUrl.redirect(external: fn _url -> "https://google.com"} end)
  ```
  """
  @doc since: "0.3.0"
  def redirect(pid \\ self(), opts) do
    opts |> ExLiveUrl.RedirectOperation.new() |> ExLiveUrl.Operation.send(pid)
  end
end