Repository: woylie/flop_phoenix (elixir)

Issue:
merge pagination/cursor_pagination components
Detect which pagination type was used in the query and render a page-based or cursor-based pagination instead of requiring the user to choose the right component.

Pull request:
deprecate cursor pagination component
- **handle cursor pagination with pagination component**
- **deprecate cursor pagination component**
- **update changelog**

resolves #374 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30bc970..b684c15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,10 +11,15 @@
 
 ### Changed
 
+- Support cursor pagination in `Flop.Phoenix.pagination/1`.
+- Deprecate `Flop.Phoenix.cursor_pagination/1` in favor of
+  `Flop.Phoenix.pagination/1`.
 - Remove the `page_links` option from the `pagination_opts`. Set this option
   with the new `page_links` attribute instead.
 - Change the values of the `page_links` option from
   `:all | :hide | {:ellipsis, pos_integer}` to `:all | :none | pos_integer`.
+- Change default value for `page_links` option of the
+  `Flop.Phoenix.pagination/1` component from `:all` to `5`.
 - Remove dependency on PhoenixHTMLHelpers.
 - Require Phoenix LiveView ~> 1.0.0.
 - Require Elixir ~> 1.14.
diff --git a/README.md b/README.md
index d2b8433..e2c4a3e 100644
--- a/README.md
+++ b/README.md
@@ -174,9 +174,6 @@ Then, you can handle the event in your LiveView, like this:
 
 This method allows you to update the page size while maintaining browser history.
 
-If you wish to implement cursor-based pagination, see
-`Flop.Phoenix.cursor_pagination/1` for setup instructions.
-
 ## Filter forms
 
 Flop Phoenix implements the `Phoenix.HTML.FormData` for the `Flop.Meta` struct.
diff --git a/lib/flop_phoenix.ex b/lib/flop_phoenix.ex
index 97ab40a..4474723 100644
--- a/lib/flop_phoenix.ex
+++ b/lib/flop_phoenix.ex
@@ -300,7 +300,11 @@ defmodule Flop.Phoenix do
           | {:thead_tr_attrs, keyword}
 
   @doc """
-  Generates a pagination element.
+  Renders pagination links for the given `Flop.Meta` struct.
+
+  This component can render both page-based or cursor-based pagination links.
+  Which one is used depends on the pagination type that was used to make the
+  Flop query.
 
   ## Examples
 
@@ -417,6 +421,19 @@ defmodule Flop.Phoenix do
     - `:none` - Does not render any page links.
     - Integer - Renders up to the specified number of page links in addition to
       the first and last page.
+
+    This attribute is only used for page-based pagination.
+    """
+
+  attr :reverse, :boolean,
+    default: false,
+    doc: """
+    By default, the `next` link moves forward with the `:after` parameter set to
+    the end cursor, and the `previous` link moves backward with the `:before`
+    parameter set to the start cursor. If `reverse` is set to `true`, the
+    destinations of the links are switched.
+
+    This attribute is only for for cursor-based pagination.
     """
 
   attr :opts, :list,
@@ -434,11 +451,6 @@ defmodule Flop.Phoenix do
     raise Flop.Phoenix.PathOrJSError, component: :pagination
   end
 
-  def pagination(%{meta: %{start_cursor: start_cursor, end_cursor: end_cursor}})
-      when not is_nil(start_cursor) or not is_nil(end_cursor) do
-    raise Flop.Phoenix.IncorrectPaginationTypeError, component: :pagination
-  end
-
   def pagination(%{opts: opts} = assigns) do
     assigns = assign(assigns, :opts, Pagination.merge_opts(opts))
 
@@ -446,6 +458,7 @@ defmodule Flop.Phoenix do
     <.pagination_for :let={p} meta={@meta} page_links={@page_links} path={@path}>
       <nav {@opts[:wrapper_attrs]}>
         <.pagination_link
+          :if={p.pagination_type in [:page, :offset]}
           disabled={is_nil(p.previous_page)}
           disabled_class={@opts[:disabled_class]}
           target={@target}
@@ -457,6 +470,7 @@ defmodule Flop.Phoenix do
           {@opts[:previous_link_content]}
         </.pagination_link>
         <.pagination_link
+          :if={p.pagination_type in [:page, :offset]}
           disabled={is_nil(p.next_page)}
           disabled_class={@opts[:disabled_class]}
           target={@target}
@@ -468,7 +482,7 @@ defmodule Flop.Phoenix do
           {@opts[:next_link_content]}
         </.pagination_link>
         <.page_links
-          :if={@page_links != :none}
+          :if={p.pagination_type in [:page, :offset] and @page_links != :none}
           current_page={p.current_page}
           ellipsis_end?={p.ellipsis_end?}
           ellipsis_start?={p.ellipsis_start?}
@@ -480,6 +494,32 @@ defmodule Flop.Phoenix do
           target={@target}
           total_pages={p.total_pages}
         />
+        <.cursor_pagination_link
+          :if={p.pagination_type in [:first, :last]}
+          direction={if @reverse, do: :next, else: :previous}
+          meta={@meta}
+          path={@path}
+          on_paginate={@on_paginate}
+          target={@target}
+          disabled={CursorPagination.disable?(@meta, :previous, @reverse)}
+          disabled_class={@opts[:disabled_class]}
+          {@opts[:previous_link_attrs]}
+        >
+          {@opts[:previous_link_content]}
+        </.cursor_pagination_link>
+        <.cursor_pagination_link
+          :if={p.pagination_type in [:first, :last]}
+          direction={if @reverse, do: :previous, else: :next}
+          meta={@meta}
+          path={@path}
+          on_paginate={@on_paginate}
+          target={@target}
+          disabled={CursorPagination.disable?(@meta, :next, @reverse)}
+          disabled_class={@opts[:disabled_class]}
+          {@opts[:next_link_attrs]}
+        >
+          {@opts[:next_link_content]}
+        </.cursor_pagination_link>
       </nav>
     </.pagination_for>
     """
@@ -598,6 +638,63 @@ defmodule Flop.Phoenix do
     """
   end
 
+  attr :direction, :atom, required: true
+  attr :meta, Flop.Meta, required: true
+  attr :path, :any, required: true
+  attr :on_paginate, JS
+  attr :target, :string, required: true
+  attr :disabled, :boolean, default: false
+  attr :disabled_class, :string, required: true
+  attr :rest, :global
+  slot :inner_block
+
+  defp cursor_pagination_link(
+         %{disabled: true, disabled_class: disabled_class} = assigns
+       ) do
+    rest =
+      Map.update(assigns.rest, :class, disabled_class, fn class ->
+        [class, disabled_class]
+      end)
+
+    assigns = assign(assigns, :rest, rest)
+
+    ~H"""
+    <span {@rest} class={@disabled_class}>
+      {render_slot(@inner_block)}
+    </span>
+    """
+  end
+
+  defp cursor_pagination_link(%{on_paginate: nil} = assigns) do
+    ~H"""
+    <.link
+      patch={CursorPagination.pagination_path(@direction, @path, @meta)}
+      {@rest}
+    >
+      {render_slot(@inner_block)}
+    </.link>
+    """
+  end
+
+  defp cursor_pagination_link(
+         %{direction: direction, path: path, meta: meta} = assigns
+       ) do
+    path = CursorPagination.pagination_path(direction, path, meta)
+    assigns = assign(assigns, :path, path)
+
+    ~H"""
+    <.link
+      patch={@path}
+      phx-click={@on_paginate}
+      phx-target={@target}
+      phx-value-to={@direction}
+      {@rest}
+    >
+      {render_slot(@inner_block)}
+    </.link>
+    """
+  end
+
   @doc """
   This component is a pagination builder.
 
@@ -682,40 +779,42 @@ defmodule Flop.Phoenix do
         } = assigns
       )
       when total_pages > 1 do
-    page_link_fun = Pagination.build_page_link_fun(meta, path)
     pagination_type = pagination_type(meta.flop)
 
-    {page_range_start, page_range_end} =
-      page_link_range(page_links, meta.current_page, meta.total_pages)
+    pagination =
+      if pagination_type in [:page, :offset] do
+        page_link_fun = Pagination.build_page_link_fun(meta, path)
+
+        {page_range_start, page_range_end} =
+          page_link_range(page_links, meta.current_page, meta.total_pages)
+
+        %Pagination{
+          current_page: meta.current_page,
+          ellipsis_end?: page_range_end < meta.total_pages - 1,
+          ellipsis_start?: page_range_start > 2,
+          next_page: meta.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: meta.previous_page,
+          total_pages: total_pages
+        }
+      else
+        %Pagination{
+          pagination_type: pagination_type
+        }
+      end
 
     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(:pagination, pagination)
       |> 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(:page_links, nil)
       |> 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
-    })}
+    {render_slot(@inner_block, @pagination)}
     """
   end
 
@@ -733,11 +832,11 @@ defmodule Flop.Phoenix do
     :page
   end
 
-  defp pagination_type(%Flop{first: first}) when is_binary(first) do
+  defp pagination_type(%Flop{first: first}) when is_integer(first) do
     :first
   end
 
-  defp pagination_type(%Flop{last: last}) when is_binary(last) do
+  defp pagination_type(%Flop{last: last}) when is_integer(last) do
     :last
   end
 
@@ -849,6 +948,7 @@ defmodule Flop.Phoenix do
   `[:family_name, :given_name, :id]`.
   """
   @doc section: :components
+  @doc deprecated: "Use Flop.Phoenix.pagination/1 instead."
   @spec cursor_pagination(map) :: Phoenix.LiveView.Rendered.t()
 
   attr :meta, Flop.Meta,
@@ -982,63 +1082,6 @@ defmodule Flop.Phoenix do
     """
   end
 
-  attr :direction, :atom, required: true
-  attr :meta, Flop.Meta, required: true
-  attr :path, :any, required: true
-  attr :on_paginate, JS
-  attr :target, :string, required: true
-  attr :disabled, :boolean, default: false
-  attr :disabled_class, :string, required: true
-  attr :rest, :global
-  slot :inner_block
-
-  defp cursor_pagination_link(
-         %{disabled: true, disabled_class: disabled_class} = assigns
-       ) do
-    rest =
-      Map.update(assigns.rest, :class, disabled_class, fn class ->
-        [class, disabled_class]
-      end)
-
-    assigns = assign(assigns, :rest, rest)
-
-    ~H"""
-    <span {@rest} class={@disabled_class}>
-      {render_slot(@inner_block)}
-    </span>
-    """
-  end
-
-  defp cursor_pagination_link(%{on_paginate: nil} = assigns) do
-    ~H"""
-    <.link
-      patch={CursorPagination.pagination_path(@direction, @path, @meta)}
-      {@rest}
-    >
-      {render_slot(@inner_block)}
-    </.link>
-    """
-  end
-
-  defp cursor_pagination_link(
-         %{direction: direction, path: path, meta: meta} = assigns
-       ) do
-    path = CursorPagination.pagination_path(direction, path, meta)
-    assigns = assign(assigns, :path, path)
-
-    ~H"""
-    <.link
-      patch={@path}
-      phx-click={@on_paginate}
-      phx-target={@target}
-      phx-value-to={@direction}
-      {@rest}
-    >
-      {render_slot(@inner_block)}
-    </.link>
-    """
-  end
-
   @doc """
   Generates a table with sortable columns.
 
diff --git a/lib/flop_phoenix/pagination.ex b/lib/flop_phoenix/pagination.ex
index f3f4f41..f191cf4 100644
--- a/lib/flop_phoenix/pagination.ex
+++ b/lib/flop_phoenix/pagination.ex
@@ -5,6 +5,19 @@ defmodule Flop.Phoenix.Pagination do
 
   require Logger
 
+  defstruct [
+    :current_page,
+    :ellipsis_end?,
+    :ellipsis_start?,
+    :next_page,
+    :page_range_end,
+    :page_link_fun,
+    :page_range_start,
+    :pagination_type,
+    :previous_page,
+    :total_pages
+  ]
+
   @spec default_opts() :: [Flop.Phoenix.pagination_option()]
   def default_opts do
     [


Test patch:
diff --git a/test/flop_phoenix_test.exs b/test/flop_phoenix_test.exs
index db0e880..999df2b 100644
--- a/test/flop_phoenix_test.exs
+++ b/test/flop_phoenix_test.exs
@@ -93,16 +93,6 @@ defmodule Flop.PhoenixTest do
              """) == []
     end
 
-    test "raises an error if cursor pagination is in use" do
-      assigns = %{meta: build(:meta_with_cursors)}
-
-      assert_raise Flop.Phoenix.IncorrectPaginationTypeError, fn ->
-        parse_heex(~H"""
-        <Flop.Phoenix.pagination meta={@meta} on_paginate={JS.push("paginate")} />
-        """)
-      end
-    end
-
     test "allows to overwrite wrapper class" do
       assigns = %{meta: build(:meta_on_first_page)}
 
@@ -1125,13 +1115,13 @@ defmodule Flop.PhoenixTest do
     end
   end
 
-  describe "cursor_pagination/1" do
+  describe "pagination/1 with cursor pagination" do
     test "renders pagination wrapper" do
       assigns = %{meta: build(:meta_with_cursors)}
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" />
         """)
 
       assert nav = find_one(html, "nav")
@@ -1140,16 +1130,6 @@ defmodule Flop.PhoenixTest do
       assert attribute(nav, "role") == "navigation"
     end
 
-    test "raises an error if an offest result set is provided" do
-      assigns = %{meta: build(:meta_on_first_page)}
-
-      assert_raise Flop.Phoenix.IncorrectPaginationTypeError, fn ->
-        parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} on_paginate={JS.push("paginate")} />
-        """)
-      end
-    end
-
     test "allows to overwrite wrapper class" do
       assigns = %{
         meta: build(:meta_with_cursors),
@@ -1158,7 +1138,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" opts={@opts} />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" opts={@opts} />
         """)
 
       assert wrapper = find_one(html, "nav")
@@ -1175,7 +1155,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" opts={@opts} />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" opts={@opts} />
         """)
 
       assert wrapper = find_one(html, "nav")
@@ -1190,7 +1170,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1206,7 +1186,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} on_paginate={@js} />
+        <Flop.Phoenix.pagination meta={@meta} on_paginate={@js} />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1224,7 +1204,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination
+        <Flop.Phoenix.pagination
           meta={@meta}
           path="/pets"
           on_paginate={JS.push("paginate")}
@@ -1252,7 +1232,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path={@path} />
+        <Flop.Phoenix.pagination meta={@meta} path={@path} />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1265,7 +1245,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path={&path_func/1} />
+        <Flop.Phoenix.pagination meta={@meta} path={&path_func/1} />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1278,7 +1258,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1291,7 +1271,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} on_paginate={JS.push("paginate")} />
+        <Flop.Phoenix.pagination meta={@meta} on_paginate={JS.push("paginate")} />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1306,7 +1286,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination
+        <Flop.Phoenix.pagination
           meta={@meta}
           on_paginate={JS.push("paginate")}
           target="here"
@@ -1323,7 +1303,7 @@ defmodule Flop.PhoenixTest do
       # default
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1336,7 +1316,7 @@ defmodule Flop.PhoenixTest do
       # reverse
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" reverse={true} />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" reverse={true} />
         """)
 
       assert link = find_one(html, "a:fl-contains('Previous')")
@@ -1356,7 +1336,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path={@path} />
+        <Flop.Phoenix.pagination meta={@meta} path={@path} />
         """)
 
       assert previous = find_one(html, "a:fl-contains('Previous')")
@@ -1380,7 +1360,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" opts={@opts} />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" opts={@opts} />
         """)
 
       assert link = find_one(html, "a[title='p-p-previous']")
@@ -1399,7 +1379,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" />
         """)
 
       assert previous_link = find_one(html, "span:fl-contains('Previous')")
@@ -1413,7 +1393,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} on_paginate={JS.push("paginate")} />
+        <Flop.Phoenix.pagination meta={@meta} on_paginate={JS.push("paginate")} />
         """)
 
       assert previous_link = find_one(html, "span:fl-contains('Previous')")
@@ -1427,7 +1407,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination
+        <Flop.Phoenix.pagination
           meta={@meta}
           on_paginate={JS.push("paginate")}
           opts={[
@@ -1448,7 +1428,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" />
         """)
 
       assert link = find_one(html, "a:fl-contains('Next')")
@@ -1464,7 +1444,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} on_paginate={JS.push("paginate")} />
+        <Flop.Phoenix.pagination meta={@meta} on_paginate={JS.push("paginate")} />
         """)
 
       assert link = find_one(html, "a:fl-contains('Next')")
@@ -1480,7 +1460,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination
+        <Flop.Phoenix.pagination
           meta={@meta}
           on_paginate={JS.push("paginate")}
           target="here"
@@ -1496,7 +1476,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination
+        <Flop.Phoenix.pagination
           meta={@meta}
           path="/pets"
           opts={[
@@ -1522,7 +1502,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} path="/pets" />
+        <Flop.Phoenix.pagination meta={@meta} path="/pets" />
         """)
 
       assert next = find_one(html, "span:fl-contains('Next')")
@@ -1535,7 +1515,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination meta={@meta} on_paginate={JS.push("paginate")} />
+        <Flop.Phoenix.pagination meta={@meta} on_paginate={JS.push("paginate")} />
         """)
 
       assert next = find_one(html, "span:fl-contains('Next')")
@@ -1548,7 +1528,7 @@ defmodule Flop.PhoenixTest do
 
       html =
         parse_heex(~H"""
-        <Flop.Phoenix.cursor_pagination
+        <Flop.Phoenix.pagination
           meta={@meta}
           on_paginate={JS.push("paginate")}
           opts={[
@@ -1569,7 +1549,7 @@ defmodule Flop.PhoenixTest do
       assert_raise Flop.Phoenix.PathOrJSError,
                    fn ->
                      rendered_to_string(~H"""
-                     <Flop.Phoenix.cursor_pagination meta={@meta} />
+                     <Flop.Phoenix.pagination meta={@meta} />
                      """)
                    end
     end
@@ -1580,7 +1560,7 @@ defmodule Flop.PhoenixTest do
       assigns = %{meta: meta}
 
       assert parse_heex(~H"""
-             <Flop.Phoenix.cursor_pagination meta={@meta} path="/path" />
+             <Flop.Phoenix.pagination meta={@meta} path="/path" />
              """) == []
     end
   end