defmodule EasyPage.Tabber do @moduledoc ~s""" A server side tabination component with lifecycle hooks. You probably do NOT need this, as most use-cases for tabs would be better off with a client-side javascript solution. One possible use case for tabber is when you want to lazily load data. I.e., call the database on the first time you enter a tab. # Usage ```elixir # Now we create a tab component. defmodule TabA do @behaviour EasyPage.Tabber.Tab # All tab callbacks are optional. # They each take a socket, and return a socket @impl true def on_first_enter(socket) do dbg "some setup stuff" socket end end # And to tie it all together, you must: # 1. Initialize it in your liveview, # 2. Call the Tabber.Body.body/1 component, and pass each tab into it as a slot. defmodule MyAppWeb.Parent do use MyAppWeb, :live_view alias EasyPage.Components.Tabber def mount(_, _, socket) do socket = Tabber.init_tabs(socket, [ {"Tab A", TabA}, # <- Each title must be unique {"Tab B", TabB}, # But the module need not. {"Tab C", TabC} ], default: "Tab A") {:ok, socket} end # Let tabber handle it's own events. @impl true def handle_event("tabber_" <> _ = evt, params, socket) do EasyPage.Tabber.EventHandlers.handle_event(evt, params, socket) end def render(assigns) do ~H\"\"\" <:tab title="Tab A">

Rendering Tab A

<:tab title="Tab B">

Rendering Tab B

<:tab title="Tab C">

Rendering Tab C

\"\"\" end end ``` To learn more about Tab modules, look at `EasyPage.Tabber.Tab` """ alias EasyPage.Tabber.{Assigns, Tab} @doc ~s""" Use this to ensure a specific tab is open upon render. ```elixir def mount(_, _, socket) do socket = EasyPage.Tabber.init_tabs(socket, [ {"My Tab", module: SomeTab}, {"Another Tab", module: AnotherTab}, ]) {:ok, socket} end ``` ## Options `default (string | nil)` This must match one of the tab titles """ def init_tabs(socket, tab_tups, opts \\ []) do tabs = tab_tups |> Enum.with_index() |> Enum.map(&Tab.new/1) tabber_assigns = Assigns.new(%{tabs: tabs}, opts) socket = Phoenix.Component.assign(socket, :tabber, tabber_assigns) socket = Enum.reduce(tabs, socket, fn tab, socket -> Code.ensure_loaded(tab.module) Tab.on_init(tab.module, socket) end) click_default(socket, opts) end defp click_default(socket, opts) do with title when is_binary(title) <- Keyword.get(opts, :default), tab when is_map(tab) <- Enum.find(socket.assigns.tabber.tabs, fn %{title: t} -> t == title end), idx when is_integer(idx) <- Map.get(tab, :idx) do click_tab(socket, idx) else _ -> socket end end @doc ~s""" Change the current tab, calling all appropriate callback functions as needed. Takes either the title, or idx of a tab. Keep in mind, you probably never need to use this function directly, it is called automatically via "tabber_" events. ```elixir def handle_event("click", %{"tab" => tab}, socket) do socket = EasyPage.Tabber.click_tab(socket, tab.idx) {:noreply, socket} end ``` """ def click_tab(socket, idx) when is_integer(idx) do socket = exit_tab(socket, idx) tab = get_tab(socket, idx) socket = enter_tab(socket, tab) Assigns.put_ctl(socket, :current_tab, tab) end def click_tab(socket, title) when is_binary(title) do idx = Enum.find_value(socket.assigns.tabber.tab, fn %{title: t, idx: idx} -> if t == title, do: idx, else: nil end) click_tab(socket, idx) end defp get_tab(socket, idx), do: Enum.at(socket.assigns.tabber.tabs, idx) defp exit_tab(socket, next_idx) do prev_tab = socket.assigns.tabber.ctl.current_tab if is_integer(prev_tab.idx) and prev_tab.idx != next_idx do Tab.on_exit(prev_tab.module, socket) else socket end end defp enter_tab(socket, tab) do socket = if tab.never_opened? do Tab.on_first_enter(tab.module, socket) else Tab.on_not_first_enter(tab.module, socket) end socket = put_has_been_entered(socket, tab) if socket.assigns.tabber.ctl.current_tab.idx != tab.idx do Tab.on_enter(tab.module, socket) else socket end end defp put_has_been_entered(socket, tab) do Assigns.update_tab(socket, tab.idx, fn tab -> Map.put(tab, :never_opened?, false) end) end end