Repository: woylie/flop_phoenix
Pull request #231 (2023-07-09 05:44:55)

add on_sort attribute to table component


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 284a404..dc49e18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,13 +8,14 @@
   components. This attribute takes a `Phoenix.LiveView.JS` command as a value.
   The attribute can be combined with the `path` attribute, in which case the
   `patch` command to the new URL will be appended to the given JS command.
+- Similarly, an `on_sort` attribute was added to the `table` component.
 
 ### Deprecated
 
 - The `show` and `hide` attributes of the `:col` slot of the table component are
   now deprecated in favor of the `:if` attribute.
-- The `event` attribute of the pagination and cursor pagination components has
-  been deprecated in favor of `on_paginate`.
+- The `event` attribute of the pagination, cursor pagination and table
+  components has been deprecated in favor of `on_sort`.
 
 ## [0.19.1] - 2023-06-30
 
diff --git a/lib/flop_phoenix.ex b/lib/flop_phoenix.ex
index c0e190a..3b9eaed 100644
--- a/lib/flop_phoenix.ex
+++ b/lib/flop_phoenix.ex
@@ -81,7 +81,8 @@ defmodule Flop.Phoenix do
 
   ## Pagination and sorting with JS commands
 
-  You can pass a `Phoenix.LiveView.JS` command as the `on_paginate` attribute.
+  You can pass a `Phoenix.LiveView.JS` command as `on_paginate` and `on_sort`
+  attributes.
 
   If used with the `path` attribute, a `patch` command to the new URL will be
   appended to the given command.
@@ -92,6 +93,13 @@ defmodule Flop.Phoenix do
   You can set a different target by assigning a `:target`. The value
   will be used as the `phx-target` attribute.
 
+      <Flop.Phoenix.table
+        items={@items}
+        meta={@meta}
+        on_sort={JS.push("sort-pets")}
+        target={@myself}
+      />
+
       <Flop.Phoenix.pagination
         meta={@meta}
         on_paginate={JS.push("paginate-pets")}
@@ -111,7 +119,7 @@ defmodule Flop.Phoenix do
         end
       end
 
-      def handle_event("order_pets", %{"order" => order}, socket) do
+      def handle_event("sort-pets", %{"order" => order}, socket) do
         flop = Flop.push_order(socket.assigns.meta.flop, order)
 
         with {:ok, {pets, meta}} <- Pets.list_pets(flop) do
@@ -866,13 +874,40 @@ defmodule Flop.Phoenix do
   attr :path, :any,
     default: nil,
     doc: """
-    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. If set, links will be
-    rendered with `Phoenix.Components.link/1` with the `patch` attribute. In a
-    LiveView, the parameters will have to be handled in the `handle_params/3`
-    callback of the LiveView module. Alternatively, set `:event`, if you don't
-    want the parameters to appear in the URL.
+    If set, the current view is patched with updated query parameters when a
+    header link for sorting is clicked. In case the `on_sort` attribute is
+    set as well, the `patch` command is appended to the `on_sort` command.
+
+    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 :on_sort, JS,
+    default: nil,
+    doc: """
+    A `Phoenix.LiveView.JS` command that is triggered when a header link for
+    sorting is clicked.
+
+    If used without the `path` attribute, you should include a `push` operation
+    to handle the event with the `handle_event` callback.
+
+        <.table
+          items={@items}
+          meta={@meta}
+          on_sort={
+            JS.dispatch("scroll-to", to: "#pets-table") |> JS.push("sort")
+          }
+        />
+
+    If used with the `path` attribute, a `patch` command is appended to the
+    given JS command.
+
+        <.table
+          meta={@meta}
+          path={~"/pets"}
+          on_sort={JS.dispatch("scroll-to", to: "#pets-table")}
+        />
     """
 
   attr :event, :string,
@@ -880,6 +915,7 @@ defmodule Flop.Phoenix do
     doc: """
     If set, `Flop.Phoenix` will render links with a `phx-click` attribute.
     Alternatively, set `:path`, if you want the parameters to appear in the URL.
+    Deprecated in favor of `on_sort`.
     """
 
   attr :target, :string,
@@ -1043,6 +1079,7 @@ defmodule Flop.Phoenix do
             caption={@caption}
             col={@col}
             foot={@foot}
+            on_sort={@on_sort}
             event={@event}
             id={@id}
             items={@items}
@@ -1061,6 +1098,7 @@ defmodule Flop.Phoenix do
           caption={@caption}
           col={@col}
           foot={@foot}
+          on_sort={@on_sort}
           event={@event}
           id={@id}
           items={@items}
diff --git a/lib/flop_phoenix/misc.ex b/lib/flop_phoenix/misc.ex
index e346092..dc68e88 100644
--- a/lib/flop_phoenix/misc.ex
+++ b/lib/flop_phoenix/misc.ex
@@ -107,28 +107,6 @@ defmodule Flop.Phoenix.Misc do
   def click_cmd(on_paginate, nil), do: on_paginate
   def click_cmd(on_paginate, path), do: JS.patch(on_paginate, path)
 
-  @doc """
-  Validates that either a path helper in the right format or an event are
-  assigned, but not both.
-  """
-  def validate_path_or_event!(%{path: {module, function, args}, event: nil}, _)
-      when is_atom(module) and is_atom(function) and is_list(args),
-      do: :ok
-
-  def validate_path_or_event!(%{path: {function, args}, event: nil}, _)
-      when is_function(function) and is_list(args),
-      do: :ok
-
-  def validate_path_or_event!(%{path: path, event: nil}, _)
-      when is_binary(path) or is_function(path, 1),
-      do: :ok
-
-  def validate_path_or_event!(%{path: nil, event: event}, _)
-      when is_binary(event),
-      do: :ok
-
-  def validate_path_or_event!(_, error_msg), do: raise(ArgumentError, error_msg)
-
   @doc """
   Validates that either a path attribute or an on_paginate attribute is set.
   """
diff --git a/lib/flop_phoenix/table.ex b/lib/flop_phoenix/table.ex
index 2bc7a79..b09df43 100644
--- a/lib/flop_phoenix/table.ex
+++ b/lib/flop_phoenix/table.ex
@@ -9,13 +9,16 @@ defmodule Flop.Phoenix.Table do
 
   require Logger
 
-  @path_event_error_msg """
-  the :path or :event option is required when rendering a table
+  @path_on_sort_error_msg """
+  path or on_sort attribute is required
 
-  The :path value can be a {module, function_name, args} tuple, a
-  {function, args} tuple, or a 1-ary function.
+  At least one of the mentioned attributes is required for the table
+  component. Combining them will append a JS.patch command to the on_paginate
+  command.
 
-  The :event value needs to be a string.
+  The :path value can be a path as a string, a
+  {module, function_name, args} tuple, a {function, args} tuple, or an 1-ary
+  function.
 
   ## Examples
 
@@ -54,7 +57,16 @@ defmodule Flop.Phoenix.Table do
       <Flop.Phoenix.table
         items={@pets}
         meta={@meta}
-        event="sort-table"
+        on_sort={JS.push("sort-table")}
+      >
+
+  or
+
+      <Flop.Phoenix.table
+        items={@pets}
+        meta={@meta}
+        path={~p"/pets"}
+        on_sort={JS.dispatch("scroll-to", to: "#my-table")}
       >
   """
 
@@ -83,11 +95,14 @@ defmodule Flop.Phoenix.Table do
   Deep merges the given options into the default options.
   """
   @spec init_assigns(map) :: map
-  def init_assigns(%{meta: meta} = assigns) do
-    assigns = assign(assigns, :opts, merge_opts(assigns[:opts] || []))
-    Misc.validate_path_or_event!(assigns, @path_event_error_msg)
+  def init_assigns(%{event: nil, on_sort: nil, path: nil}) do
+    raise ArgumentError, @path_on_sort_error_msg
+  end
 
-    assign_new(assigns, :id, fn ->
+  def init_assigns(%{meta: meta} = assigns) do
+    assigns
+    |> assign(:opts, merge_opts(assigns.opts))
+    |> assign_new(:id, fn ->
       case meta.schema do
         nil ->
           "paginated_table"
@@ -110,6 +125,7 @@ defmodule Flop.Phoenix.Table do
   attr :id, :string, required: true
   attr :meta, Flop.Meta, required: true
   attr :path, :any, required: true
+  attr :on_sort, JS
   attr :event, :string, required: true
   attr :target, :string, required: true
   attr :caption, :string, required: true
@@ -146,6 +162,7 @@ defmodule Flop.Phoenix.Table do
           <%= for col <- @col do %>
             <.header_column
               :if={show_column?(col)}
+              on_sort={@on_sort}
               event={@event}
               field={col[:field]}
               label={col[:label]}
@@ -216,6 +233,7 @@ defmodule Flop.Phoenix.Table do
   attr :field, :atom, required: true
   attr :label, :string, required: true
   attr :path, :any, required: true
+  attr :on_sort, JS
   attr :event, :string, required: true
   attr :target, :string, required: true
   attr :opts, :any, required: true
@@ -228,25 +246,14 @@ defmodule Flop.Phoenix.Table do
     <%= if sortable?(@field, @meta.schema) do %>
       <th {@opts[:thead_th_attrs]} aria-sort={aria_sort(@order_direction)}>
         <span {@opts[:th_wrapper_attrs]}>
-          <%= if @event do %>
-            <.sort_link
-              event={@event}
-              field={@field}
-              label={@label}
-              target={@target}
-            />
-          <% else %>
-            <.link patch={
-              Flop.Phoenix.build_path(
-                @path,
-                Flop.push_order(@meta.flop, @field),
-                backend: @meta.backend,
-                for: @meta.schema
-              )
-            }>
-              <%= @label %>
-            </.link>
-          <% end %>
+          <.sort_link
+            path={build_path(@path, @meta, @field)}
+            on_sort={@on_sort}
+            event={@event}
+            field={@field}
+            label={@label}
+            target={@target}
+          />
           <.arrow direction={@order_direction} opts={@opts} />
         </span>
       </th>
@@ -292,10 +299,12 @@ defmodule Flop.Phoenix.Table do
 
   attr :field, :atom, required: true
   attr :label, :string, required: true
-  attr :event, :string, required: true
-  attr :target, :string, required: true
+  attr :path, :string
+  attr :on_sort, JS
+  attr :event, :string
+  attr :target, :string
 
-  defp sort_link(assigns) do
+  defp sort_link(%{event: event} = assigns) when is_binary(event) do
     ~H"""
     <.link phx-click={@event} phx-target={@target} phx-value-order={@field}>
       <%= @label %>
@@ -303,6 +312,26 @@ defmodule Flop.Phoenix.Table do
     """
   end
 
+  defp sort_link(%{on_sort: nil, path: path} = assigns)
+       when is_binary(path) do
+    ~H"""
+    <.link patch={@path}><%= @label %></.link>
+    """
+  end
+
+  defp sort_link(%{} = assigns) do
+    ~H"""
+    <.link
+      href={@path}
+      phx-click={Misc.click_cmd(@on_sort, @path)}
+      phx-target={@target}
+      phx-value-order={@field}
+    >
+      <%= @label %>
+    </.link>
+    """
+  end
+
   defp order_direction(
          %Flop{order_by: [field | _], order_directions: [direction | _]},
          field
@@ -318,4 +347,15 @@ defmodule Flop.Phoenix.Table do
   defp sortable?(field, module) do
     field in (module |> struct() |> Flop.Schema.sortable())
   end
+
+  defp build_path(nil, _, _), do: nil
+
+  defp build_path(path, meta, field) do
+    Flop.Phoenix.build_path(
+      path,
+      Flop.push_order(meta.flop, field),
+      backend: meta.backend,
+      for: meta.schema
+    )
+  end
 end


Test patch:
diff --git a/test/flop_phoenix_test.exs b/test/flop_phoenix_test.exs
index 16c4c56..1777013 100644
--- a/test/flop_phoenix_test.exs
+++ b/test/flop_phoenix_test.exs
@@ -57,6 +57,7 @@ defmodule Flop.PhoenixTest do
   end
 
   attr :caption, :string, default: nil
+  attr :on_sort, JS, default: nil
   attr :event, :string, default: nil
   attr :id, :string, default: nil
   attr :meta, Flop.Meta, default: %Flop.Meta{flop: %Flop{}}
@@ -75,6 +76,7 @@ defmodule Flop.PhoenixTest do
     ~H"""
     <Flop.Phoenix.table
       caption={@caption}
+      on_sort={@on_sort}
       event={@event}
       id={@id}
       items={@items}
@@ -1906,6 +1908,46 @@ defmodule Flop.PhoenixTest do
              ]
     end
 
+    test "uses phx-click with on_sort without path" do
+      html =
+        render_table(
+          path: nil,
+          on_sort: JS.push("sort")
+        )
+
+      assert [a] = Floki.find(html, "th a:fl-contains('Name')")
+      assert Floki.attribute(a, "data-phx-link") == []
+      assert Floki.attribute(a, "data-phx-link-state") == []
+      assert Floki.attribute(a, "href") == ["#"]
+      assert Floki.attribute(a, "phx-value-order") == ["name"]
+      assert [phx_click] = Floki.attribute(a, "phx-click")
+      assert Jason.decode!(phx_click) == [["push", %{"event" => "sort"}]]
+    end
+
+    test "uses phx-click with on_paginate and path" do
+      html =
+        render_pagination(
+          meta: build(:meta_on_second_page),
+          path: "/pets",
+          on_paginate: JS.push("paginate")
+        )
+
+      link = Floki.find(html, "a:fl-contains('Previous')")
+      expected_path = "/pets?page_size=10"
+
+      assert Floki.attribute(link, "class") == ["pagination-previous"]
+      assert Floki.attribute(link, "data-phx-link") == []
+      assert Floki.attribute(link, "data-phx-link-state") == []
+      assert Floki.attribute(link, "href") == [expected_path]
+      assert Floki.attribute(link, "phx-value-page") == ["1"]
+      assert [phx_click] = Floki.attribute(link, "phx-click")
+
+      assert Jason.decode!(phx_click) == [
+               ["push", %{"event" => "paginate"}],
+               ["patch", %{"href" => "/pets?page_size=10", "replace" => false}]
+             ]
+    end
+
     test "supports a function/args tuple as path" do
       html = render_table(path: {&route_helper/3, @route_helper_opts})
       assert [a] = Floki.find(html, "th a:fl-contains('Name')")
@@ -2346,7 +2388,7 @@ defmodule Flop.PhoenixTest do
 
     test "raises if neither path nor event are passed" do
       assert_raise ArgumentError,
-                   ~r/^the :path or :event option is required/,
+                   ~r/^path or on_sort attribute is required/,
                    fn ->
                      render_component(&table/1,
                        __changed__: nil,