diff --git a/lib/flop_phoenix.ex b/lib/flop_phoenix.ex index d32f059..95a4770 100644 --- a/lib/flop_phoenix.ex +++ b/lib/flop_phoenix.ex @@ -428,115 +428,115 @@ defmodule Flop.Phoenix do raise Flop.Phoenix.IncorrectPaginationTypeError, component: :pagination end - def pagination(%{meta: meta, opts: opts, path: path} = assigns) do - assigns = - assigns - |> assign(:opts, Pagination.merge_opts(opts)) - |> assign( - :page_link_helper, - Pagination.build_page_link_helper(meta, path) - ) - |> assign(:path, nil) + def pagination(%{opts: opts} = assigns) do + assigns = assign(assigns, :opts, Pagination.merge_opts(opts)) ~H""" - + <.pagination_for + :let={p} + meta={@meta} + page_links={@opts[:page_links]} + path={@path} + > + + """ end - attr :meta, Flop.Meta, required: true + attr :current_page, :integer, required: true + attr :ellipsis_end?, :boolean, required: true + attr :ellipsis_start?, :boolean, required: true attr :on_paginate, JS - attr :page_link_helper, :any, required: true - attr :target, :string, required: true attr :opts, :list, required: true + attr :page_link_fun, :any, required: true + attr :page_range_end, :integer, required: true + attr :page_range_start, :integer, required: true + attr :target, :string, required: true + attr :total_pages, :integer, required: true - defp page_links(%{meta: meta} = assigns) do - max_pages = - Pagination.max_pages(assigns.opts[:page_links], assigns.meta.total_pages) - - range = - first..last//_ = - Pagination.get_page_link_range( - meta.current_page, - max_pages, - meta.total_pages - ) - - assigns = assign(assigns, first: first, last: last, range: range) - + defp page_links(assigns) do ~H""" @@ -592,6 +592,154 @@ defmodule Flop.Phoenix do """ end + @doc """ + This component is a pagination builder. + + It does not render anything by itself. Instead, it prepares all the necessary + information needed to render a pagination component and passes it to the + inner block. + + For an example implementation, see `pagination/1`. + + ## Example + + ```heex + <.pagination_for + :let={p} + meta={@meta} + page_links={{:ellipsis, 4}} + path={~p"/birds"} + > + <%!-- + The variable passed to the inner block via `:let` looks similar to: + + %{ + current_page: 6, + ellipsis_start?: true, + ellipsis_end?: true, + next_page: 7, + page_range_end: 8, + page_link_fun: #Function<42.18682967/1 in :erl_eval.expr/6>, + page_range_start: 5, + pagination_type: :page, + previous_page: 5, + total_pages: 10 + } + %> + + ``` + """ + @doc section: :components + @doc since: "0.24.0" + @spec pagination_for(map) :: Phoenix.LiveView.Rendered.t() + + attr :meta, Flop.Meta, + required: true, + doc: """ + The meta information of the query as returned by the `Flop` query functions. + """ + + attr :path, :any, + default: nil, + doc: """ + If set, a function that takes a page number and returns a link with + pagination, filter, and sort parameters based on the given path is passed + as `page_link_fun` to the inner block. + + The value must be either a URI string (Phoenix verified route), an MFA or FA + tuple (Phoenix route helper), or a 1-ary path builder function. See + `Flop.Phoenix.build_path/3` for details. + """ + + attr :page_links, :any, + default: :all, + doc: """ + Specifies how many page links should be rendered. + + Default: `#{inspect(Pagination.default_opts()[:page_links])}`. + + - `:all` - Renders all page links. + - `{:ellipsis, n}` - Renders `n` page links. Renders ellipsis elements if + there are more pages than displayed. + - `:hide` - Does not render any page links. + + A `page_range_start` and `page_range_end` attribute are passed to the + inner block based on this option. If this attribute is set to `:hide`, both + of those values will be `nil`. + """ + + slot :inner_block, required: true + + def pagination_for( + %{ + meta: %Flop.Meta{errors: []} = meta, + page_links: page_links, + path: path + } = assigns + ) do + page_link_fun = Pagination.build_page_link_fun(meta, path) + pagination_type = pagination_type(meta.flop) + + {page_range_start, page_range_end} = + Pagination.get_page_link_range( + page_links, + meta.current_page, + meta.total_pages + ) + + assigns = + assigns + |> assign(:current_page, meta.current_page) + |> assign(:ellipsis_end?, page_range_end < meta.total_pages - 1) + |> assign(:ellipsis_start?, page_range_start > 2) + |> assign(:meta, nil) + |> assign(:next_page, meta.next_page) + |> assign(:page_link_fun, page_link_fun) + |> assign(:page_range_end, page_range_end) + |> assign(:page_range_start, page_range_start) + |> assign(:pagination_type, pagination_type) + |> assign(:path, nil) + |> assign(:previous_page, meta.previous_page) + |> assign(:total_pages, meta.total_pages) + + ~H""" + {render_slot(@inner_block, %{ + current_page: @current_page, + ellipsis_end?: @ellipsis_end?, + ellipsis_start?: @ellipsis_start?, + next_page: @next_page, + page_range_end: @page_range_end, + page_link_fun: @page_link_fun, + page_range_start: @page_range_start, + pagination_type: @pagination_type, + previous_page: @previous_page, + total_pages: @total_pages + })} + """ + end + + def pagination_for(assigns) do + ~H"" + end + + defp pagination_type(%Flop{limit: limit, offset: offset}) + when is_integer(limit) and is_integer(offset) do + :offset + end + + defp pagination_type(%Flop{page: page, page_size: page_size}) + when is_integer(page) and is_integer(page_size) do + :page + end + + defp pagination_type(%Flop{first: first}) when is_binary(first) do + :first + end + + defp pagination_type(%Flop{last: last}) when is_binary(last) do + :last + end + @doc """ Renders a cursor pagination element. diff --git a/lib/flop_phoenix/pagination.ex b/lib/flop_phoenix/pagination.ex index 186b687..e734286 100644 --- a/lib/flop_phoenix/pagination.ex +++ b/lib/flop_phoenix/pagination.ex @@ -44,36 +44,35 @@ defmodule Flop.Phoenix.Pagination do |> Misc.deep_merge(opts) end - def max_pages(:all, total_pages), do: total_pages - def max_pages(:hide, _), do: 0 - def max_pages({:ellipsis, max_pages}, _), do: max_pages - @spec get_page_link_range( - non_neg_integer(), + :all | :hide | {:ellipsis, non_neg_integer()}, non_neg_integer(), non_neg_integer() - ) :: Range.t() - def get_page_link_range(current_page, max_pages, total_pages) do + ) :: {non_neg_integer() | nil, non_neg_integer() | nil} + def get_page_link_range(:all, _, total_pages), do: {1, total_pages} + def get_page_link_range(:hide, _, _), do: {nil, nil} + + def get_page_link_range({:ellipsis, max_pages}, current_page, total_pages) do # number of additional pages to show before or after current page additional = ceil(max_pages / 2) cond do max_pages >= total_pages -> - 1..total_pages + {1, total_pages} current_page + additional > total_pages -> - (total_pages - max_pages + 1)..total_pages + {total_pages - max_pages + 1, total_pages} true -> first = max(current_page - additional + 1, 1) last = min(first + max_pages - 1, total_pages) - first..last + {first, last} end end - def build_page_link_helper(_meta, nil), do: fn _ -> nil end + def build_page_link_fun(_meta, nil), do: fn _ -> nil end - def build_page_link_helper(meta, path) do + def build_page_link_fun(meta, path) do query_params = build_query_params(meta) fn page -> @@ -121,11 +120,11 @@ defmodule Flop.Phoenix.Pagination do defp maybe_put_page(params, 1), do: Keyword.delete(params, :page) defp maybe_put_page(params, page), do: Keyword.put(params, :page, page) - def attrs_for_page_link(page, %{current_page: page}, opts) do + def attrs_for_page_link(page, page, opts) do add_page_link_aria_label(opts[:current_link_attrs], page, opts) end - def attrs_for_page_link(page, _meta, opts) do + def attrs_for_page_link(page, _current_page, opts) do add_page_link_aria_label(opts[:pagination_link_attrs], page, opts) end diff --git a/test/flop/pagination_test.exs b/test/flop/pagination_test.exs index 36ab701..4bdc051 100644 --- a/test/flop/pagination_test.exs +++ b/test/flop/pagination_test.exs @@ -4,34 +4,42 @@ defmodule Flop.Phoenix.PaginationTest do alias Flop.Phoenix.Pagination describe "get_page_link_range/3" do + test "returns nil values for :hide option" do + assert Pagination.get_page_link_range(:hide, 1, 10) == {nil, nil} + end + + test "returns full range for :all option" do + assert Pagination.get_page_link_range(:all, 4, 10) == {1, 10} + end + test "returns page range with odd max pages" do - assert Pagination.get_page_link_range(1, 3, 10) == 1..3 - assert Pagination.get_page_link_range(2, 3, 10) == 1..3 - assert Pagination.get_page_link_range(3, 3, 10) == 2..4 - assert Pagination.get_page_link_range(4, 3, 10) == 3..5 - assert Pagination.get_page_link_range(5, 3, 10) == 4..6 - assert Pagination.get_page_link_range(6, 3, 10) == 5..7 - assert Pagination.get_page_link_range(7, 3, 10) == 6..8 - assert Pagination.get_page_link_range(8, 3, 10) == 7..9 - assert Pagination.get_page_link_range(9, 3, 10) == 8..10 - assert Pagination.get_page_link_range(10, 3, 10) == 8..10 + assert Pagination.get_page_link_range({:ellipsis, 3}, 1, 10) == {1, 3} + assert Pagination.get_page_link_range({:ellipsis, 3}, 2, 10) == {1, 3} + assert Pagination.get_page_link_range({:ellipsis, 3}, 3, 10) == {2, 4} + assert Pagination.get_page_link_range({:ellipsis, 3}, 4, 10) == {3, 5} + assert Pagination.get_page_link_range({:ellipsis, 3}, 5, 10) == {4, 6} + assert Pagination.get_page_link_range({:ellipsis, 3}, 6, 10) == {5, 7} + assert Pagination.get_page_link_range({:ellipsis, 3}, 7, 10) == {6, 8} + assert Pagination.get_page_link_range({:ellipsis, 3}, 8, 10) == {7, 9} + assert Pagination.get_page_link_range({:ellipsis, 3}, 9, 10) == {8, 10} + assert Pagination.get_page_link_range({:ellipsis, 3}, 10, 10) == {8, 10} end test "returns page range with even max pages" do - assert Pagination.get_page_link_range(1, 4, 10) == 1..4 - assert Pagination.get_page_link_range(2, 4, 10) == 1..4 - assert Pagination.get_page_link_range(3, 4, 10) == 2..5 - assert Pagination.get_page_link_range(4, 4, 10) == 3..6 - assert Pagination.get_page_link_range(5, 4, 10) == 4..7 - assert Pagination.get_page_link_range(6, 4, 10) == 5..8 - assert Pagination.get_page_link_range(7, 4, 10) == 6..9 - assert Pagination.get_page_link_range(8, 4, 10) == 7..10 - assert Pagination.get_page_link_range(9, 4, 10) == 7..10 - assert Pagination.get_page_link_range(10, 4, 10) == 7..10 + assert Pagination.get_page_link_range({:ellipsis, 4}, 1, 10) == {1, 4} + assert Pagination.get_page_link_range({:ellipsis, 4}, 2, 10) == {1, 4} + assert Pagination.get_page_link_range({:ellipsis, 4}, 3, 10) == {2, 5} + assert Pagination.get_page_link_range({:ellipsis, 4}, 4, 10) == {3, 6} + assert Pagination.get_page_link_range({:ellipsis, 4}, 5, 10) == {4, 7} + assert Pagination.get_page_link_range({:ellipsis, 4}, 6, 10) == {5, 8} + assert Pagination.get_page_link_range({:ellipsis, 4}, 7, 10) == {6, 9} + assert Pagination.get_page_link_range({:ellipsis, 4}, 8, 10) == {7, 10} + assert Pagination.get_page_link_range({:ellipsis, 4}, 9, 10) == {7, 10} + assert Pagination.get_page_link_range({:ellipsis, 4}, 10, 10) == {7, 10} end test "does not return range beyond total pages" do - assert Pagination.get_page_link_range(1, 3, 2) == 1..2 + assert Pagination.get_page_link_range({:ellipsis, 3}, 1, 2) == {1, 2} end end end diff --git a/test/flop_phoenix_test.exs b/test/flop_phoenix_test.exs index 9c082ae..d95585b 100644 --- a/test/flop_phoenix_test.exs +++ b/test/flop_phoenix_test.exs @@ -776,6 +776,7 @@ defmodule Flop.PhoenixTest do build( :meta_on_second_page, flop: %Flop{ + page: 1, page_size: 20, order_by: [:name], order_directions: [:asc]