defmodule Navigator.Component do @moduledoc """ Navigator functionality for `Phoenix.LiveComponent` """ alias Navigator.{Controller, History} alias Phoenix.LiveView alias Phoenix.LiveView.Socket defmacro __using__(_opts) do quote do import unquote(LiveView), except: [ push_navigate: 2, push_patch: 2, push_redirect: 2, redirect: 2, ] import unquote(__MODULE__), only: [ assign_nav: 2, assign_nav: 3, assign_page: 2, assign_page: 3, clear_nav: 2, clear_page: 2, current_url: 1, history: 1, nav_back: 1, nav_back: 2, nav_back_url: 1, nav_back_url: 2, nav_pop_stack: 1, nav_pop_stack: 2, nav_pop_stack_url: 1, push_navigate: 2, push_patch: 2, push_redirect: 2, redirect: 2, ] end end @spec push_navigate(Socket.t, keyword) :: Socket.t def push_navigate(%Socket{root_pid: pid} = socket, opts) when is_pid(pid) do if Keyword.has_key?(opts, :to), do: navigate_forward(pid, :navigate, opts) if opts[:navigate] != false do LiveView.push_navigate(socket, Keyword.take(opts, ~w[to]a)) else socket end end def push_navigate(socket, opts), do: LiveView.push_navigate(socket, opts) @spec push_patch(Socket.t, keyword) :: Socket.t def push_patch(%Socket{root_pid: pid} = socket, opts) when is_pid(pid) do if Keyword.has_key?(opts, :to), do: navigate_forward(pid, :patch, opts) if opts[:navigate] != false do LiveView.push_patch(socket, Keyword.take(opts, ~w[to]a)) else socket end end def push_patch(socket, opts), do: LiveView.push_patch(socket, opts) @doc deprecated: "Use push_navigate/2 instead" # Deprecate in 0.19 @spec push_redirect(Socket.t, keyword) :: Socket.t def push_redirect(%Socket{root_pid: pid} = socket, opts) when is_pid(pid) do if Keyword.has_key?(opts, :to), do: navigate_forward(pid, :navigate, opts) if opts[:navigate] != false do LiveView.push_redirect(socket, Keyword.take(opts, ~w[to]a)) else socket end end def push_redirect(socket, opts), do: LiveView.push_redirect(socket, opts) @spec redirect(Socket.t, keyword) :: Socket.t def redirect(%Socket{root_pid: pid} = socket, opts) when is_pid(pid) do if Keyword.has_key?(opts, :to), do: navigate_forward(pid, :redirect, opts) if opts[:navigate] != false do LiveView.redirect(socket, Keyword.take(opts, ~w[to]a)) else socket end end def redirect(socket, opts), do: LiveView.redirect(socket, opts) @spec nav_back(Socket.t) :: Socket.t @spec nav_back(Socket.t, Navigator.back_index | keyword) :: Socket.t def nav_back(socket, opts \\ []) def nav_back(socket, to) when not is_list(to), do: nav_back(socket, to: to) def nav_back(%Socket{root_pid: pid} = socket, opts) do with_navigator(pid, socket, fn %{history: history} = navigator, socket -> {index, unstack} = case Keyword.fetch(opts, :to) do {:ok, %History{} = spec} -> {spec, false} {:ok, {%History{} = spec, unstack}} when is_boolean(unstack) -> {spec, unstack} {:ok, {index, unstack}} when (is_integer(index) or is_atom(index)) and is_boolean(unstack) -> {index, unstack} {:ok, index} when is_integer(index) or is_atom(index) -> {index, false} _ -> {-2, false} end navigator |> Navigator.navigate_back(History.find(history, index), unstack) |> apply_awaiting(socket, opts[:navigate]) end) end def nav_back(socket, _), do: socket @spec nav_pop_stack(Socket.t) :: Socket.t @spec nav_pop_stack(Socket.t, keyword) :: Socket.t def nav_pop_stack(socket, opts \\ []) def nav_pop_stack(%Socket{root_pid: pid} = socket, opts) do with_navigator(pid, socket, fn %{history: history} = navigator, socket -> navigator |> Navigator.navigate_back(History.stack_preceding(history), false) |> apply_awaiting(socket, opts[:navigate]) end) end def nav_pop_stack(socket, _), do: socket @doc """ Assigns value to page session > WARNING! `assign_page/3` does nothing with local live component assigns. It just updates page session. """ @spec assign_page(Socket.t, atom, any) :: Socket.t def assign_page(%Socket{root_pid: pid} = socket, key, value) when is_pid(pid) and is_atom(key) do with_navigator(pid, socket, fn navigator, socket -> Navigator.update_page_assigns(navigator, [{key, value}], []) notify_view(pid, [:page]) socket end) end def assign_page(socket, _key, _value), do: socket @doc """ """ @spec assign_page(Socket.t, keyword | map) :: Socket.t def assign_page(%Socket{root_pid: pid} = socket, assigns) when is_pid(pid) do with_navigator(pid, socket, fn navigator, socket -> Navigator.update_page_assigns(navigator, assigns, []) notify_view(pid, [:page]) socket end) end def assign_page(socket, _assigns), do: socket @doc """ """ @spec assign_nav(Socket.t, atom, any) :: Socket.t def assign_nav(%Socket{root_pid: pid} = socket, key, value) when is_pid(pid) and is_atom(key) do with_navigator(pid, socket, fn navigator, socket -> Navigator.update_nav_assigns(navigator, [{key, value}], []) notify_view(pid, [:navigator]) socket end) end def assign_nav(socket, _key, _value), do: socket @doc """ """ @spec assign_nav(Socket.t, keyword | map) :: Socket.t def assign_nav(%Socket{root_pid: pid} = socket, assigns) when is_pid(pid) do with_navigator(pid, socket, fn navigator, socket -> Navigator.update_nav_assigns(navigator, assigns, []) notify_view(pid, [:navigator]) socket end) end def assign_nav(socket, _assigns), do: socket @doc """ """ @spec clear_page(Socket.t, atom | [atom]) :: Socket.t def clear_page(%Socket{root_pid: pid} = socket, keys) do with_navigator(pid, socket, fn navigator, socket -> Navigator.update_page_assigns(navigator, [], keys) notify_view(pid, [:page]) socket end) end def clear_page(socket, _keys) do socket end @doc """ """ @spec clear_nav(Socket.t, atom | [atom]) :: Socket.t def clear_nav(%Socket{root_pid: pid} = socket, keys) do with_navigator(pid, socket, fn navigator, socket -> Navigator.update_nav_assigns(navigator, [], keys) notify_view(pid, [:navigator]) socket end) end def clear_nav(socket, _keys) do socket end @doc """ """ @spec history(Socket.t) :: History.t def history(%Socket{root_pid: pid}) do case Controller.get_navigator(pid) do %Navigator{history: history} -> history _ -> [] end end def history(_), do: [] @doc """ """ @spec navigator(Socket.t) :: Navigator.t | nil def navigator(%Socket{root_pid: pid}), do: Controller.get_navigator(pid) def navigator(_), do: nil @doc """ """ @spec current_url(Socket.t) :: binary | nil def current_url(%Socket{root_pid: pid}) do case Controller.get_navigator(pid) do %Navigator{url: url} -> url _ -> nil end end def current_url(_), do: nil @doc """ """ @spec nav_back_url(Socket.t) :: binary | nil @spec nav_back_url(Socket.t, History.index) :: binary | nil def nav_back_url(socket, index \\ -2) def nav_back_url(%Socket{root_pid: pid}, index) do case Controller.get_navigator(pid) do %Navigator{view: view, history: history} -> case History.find(history, index) do %{url: url} -> Navigator.url_path(url) _ -> Navigator.get_fallback_url(view) end _ -> nil end end def nav_back_url(_socket, _index), do: nil @doc """ """ @spec nav_pop_stack_url(Socket.t) :: binary | nil def nav_pop_stack_url(%Socket{root_pid: pid}) do case Controller.get_navigator(pid) do %Navigator{view: view, history: history} -> case History.stack_preceding(history) do %{url: url} -> Navigator.url_path(url) _ -> Navigator.get_fallback_url(view) end _ -> nil end end def nav_pop_stack_url(_socket), do: nil defp navigate_forward(pid, action, opts) do case Controller.get_navigator(pid) do %Navigator{} = navigator -> navigator |> Navigator.navigate_forward(action, opts) _ -> :ok end end defp with_navigator(pid, socket, fun) do case Controller.get_navigator(pid) do %Navigator{} = navigator -> fun.(navigator, socket) _ -> socket end end defp apply_awaiting(_navigator, socket, false), do: socket defp apply_awaiting(%{awaiting: {%{method: :patch}, to, _}}, socket, _) do LiveView.push_patch(socket, to: Navigator.url_path(to)) end defp apply_awaiting(%{awaiting: {%{method: :navigate}, to, _}}, socket, _) do LiveView.push_navigate(socket, to: Navigator.url_path(to)) end defp apply_awaiting(%{awaiting: {%{method: :redirect}, to, _}}, socket, _) do LiveView.redirect(socket, to: Navigator.url_path(to)) end defp notify_view(pid, updates) do send(pid, {Navigator, :reload, updates}) end end