From 63de6ce8769d2049f1154bdae06f423dc5fe47ea Mon Sep 17 00:00:00 2001 From: Matthew Erhard Date: Fri, 6 Dec 2024 18:08:20 -0500 Subject: [PATCH] Update documentation for the `pagination/1` component Add a code example of how to use the component. Mark the `:params` and `:total_count` attributes as required so the developer gets compile-time hints that these attributes should be set in the template when using the component. Move the LiveComponent to its own module so its functions do not get imported for use in the application's code. Resolves #15 --- .../components/station_ui/html/pagination.ex | 374 +++++++++++------- 1 file changed, 224 insertions(+), 150 deletions(-) diff --git a/sources/lib/components/station_ui/html/pagination.ex b/sources/lib/components/station_ui/html/pagination.ex index c495d5d..6b4eefd 100644 --- a/sources/lib/components/station_ui/html/pagination.ex +++ b/sources/lib/components/station_ui/html/pagination.ex @@ -1,7 +1,8 @@ defmodule StationUI.HTML.Pagination do - @moduledoc """ - The pagination component renders clickable numbered elements to progress through content that spans multiple pages. + use Phoenix.Component + @doc ~S""" + The pagination component renders clickable numbered elements to progress through content that spans multiple pages. The default size for each pagination button is "md" but the size can be changed by supplying the `class` attribute to each slot. @@ -9,15 +10,79 @@ defmodule StationUI.HTML.Pagination do Suggested size classes: xl: gap-x-4 px-8 py-3 text-4xl lg:focus-visible:ring-4 lg: gap-x-4 px-7 py-2.5 text-2xl + md: gap-x-2 px-6 py-2 text-base sm: gap-x-2 px-5 py-2 text-sm xs: gap-x-2 px-4 py-2 text-xs - """ - use Phoenix.LiveComponent + ## Example Usage + + Example contents of a LiveView for the `~p"/posts"` route: + + ```elixir + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, assign_paginated_posts(socket, params)} + end + + defp assign_paginated_posts(socket, params) do + page = get_pagination_param(params, "page", 1) + page_size = get_pagination_param(params, "page_size", 10) + offset = (page - 1) * page_size - @default_params %{"page" => 1, "page_size" => 10} + socket + |> assign(:pagination_params, %{"page" => page, "page_size" => page_size}) + |> assign(:posts, Posts.list_posts(offset: offset, limit: page_size)) + |> assign(:posts_count, Posts.count_posts()) + end + + defp get_pagination_param(params, param, default) do + value = to_string(params[param] || default) - @item_default_classes "gap-x-2 px-6 py-2 text-base" + case Integer.parse(value) do + {page, _} -> max(page, 1) + _ -> default + end + end + ``` + + Example usage in the rendered template: + ```heex + <.pagination + link_fn={&~p"/posts?#{&1}"} + params={@pagination_params} + total_count={@posts_count} + > + <:first> + <.icon class="h-5 w-5" name="hero-chevron-double-left-solid" /> + First + + + <:previous> + <.icon class="h-4 w-4" name="hero-chevron-left-solid" /> + + Prev + + + <:page_links /> + + <:next> + Next + + <.icon class="h-4 w-4" name="hero-chevron-right-solid" /> + + + <:last> + Last + <.icon class="h-5 w-5" name="hero-chevron-double-right-solid" /> + + + ``` + """ attr :class, :string, default: "inline-flex gap-x-2.5" @@ -32,171 +97,179 @@ defmodule StationUI.HTML.Pagination do attr :max_pages, :integer, default: 7, doc: "maximum number of pages to show at a time" attr :params, :map, - default: @default_params, - doc: "map of the current pagination params (page, page_size)" + required: true, + doc: ~s'map of the current pagination params (ex. `%{"page" => 1, "page_size" => 10}`)' attr :total_count, :integer, - default: 0, + required: true, doc: "total number of records in the list being paginated" slot :first do - attr :class, :string, doc: @item_default_classes + attr :class, :string, doc: "gap-x-2 px-6 py-2 text-base" end slot :last do - attr :class, :string, doc: @item_default_classes + attr :class, :string, doc: "gap-x-2 px-6 py-2 text-base" end slot :next do - attr :class, :string, doc: @item_default_classes + attr :class, :string, doc: "gap-x-2 px-6 py-2 text-base" end slot :page_links do - attr :class, :string, doc: @item_default_classes + attr :class, :string, doc: "gap-x-2 px-6 py-2 text-base" end slot :previous do - attr :class, :string, doc: @item_default_classes + attr :class, :string, doc: "gap-x-2 px-6 py-2 text-base" end def pagination(assigns) do ~H""" - <.live_component module={__MODULE__} {assigns} /> + <.live_component module={__MODULE__.LiveComponent} {assigns} /> """ end - @impl true - def update(assigns, socket) do - %{"page" => current_page, "page_size" => page_size} = - params = - Map.merge(@default_params, cast_params(assigns.params)) - - total_page_count = (assigns.total_count / page_size) |> ceil() |> max(1) - - {:ok, - socket - |> assign(assigns) - |> assign(:current_page, current_page) - |> assign(:params, params) - |> assign(:total_page_count, total_page_count) - |> assign_range()} - end - - @impl true - def render(assigns) do - ~H""" - + """ + end + + @impl true + def handle_event("update-pages-range", %{"range-start" => range_start}, socket) do + socket = + case Integer.parse(range_start) do + {parsed_range_start, _} -> assign_range(socket, parsed_range_start) + _ -> socket + end + + {:noreply, socket} + end + + defp assign_range(socket) do + %{current_page: current_page, max_pages: max_pages} = socket.assigns + + assign_range(socket, current_page - ceil(max_pages / 2) + 1) + end + + defp assign_range(socket, range_start) do + %{max_pages: max_pages, total_page_count: total_page_count} = socket.assigns + + range_start = max(range_start, 1) + range_end = min(range_start + max_pages - 1, total_page_count) + range_start = max(range_end - max_pages + 1, 1) + + socket + |> assign(:range_start, range_start) + |> assign(:range_end, range_end) + end + + defp cast_params(params) do + Map.new(params, fn {original_key, original_value} -> + updated_key = to_string(original_key) + + updated_value = + original_value + |> to_string() + |> String.to_integer() + |> max(1) + + {updated_key, updated_value} + end) + end + + defp default_params, do: %{"page" => 1, "page_size" => 10} + + defp item_classes(slot) do + [slot[:class] || item_default_classes(), item_base_classes()] + end + + defp item_default_classes, do: "gap-x-2 px-6 py-2 text-base" + + defp item_base_classes do + ~w" inline-flex items-center justify-center @@ -214,11 +287,12 @@ defmodule StationUI.HTML.Pagination do focus-visible:ring-[--sui-brand-primary-focus] focus-visible:ring-offset-4 " - end + end - defp link_to_page(params, page, link_fn) do - params - |> Map.put("page", page) - |> link_fn.() + defp link_to_page(params, page, link_fn) do + params + |> Map.put("page", page) + |> link_fn.() + end end end