defmodule Flop.Phoenix.Table do @moduledoc false use Phoenix.Component alias Flop.Phoenix.Misc alias Phoenix.HTML alias Phoenix.LiveView.JS require Logger @path_event_error_msg """ the :path or :event option is required when rendering a table The :path value can be a {module, function_name, args} tuple, a {function, args} tuple, or a 1-ary function. The :event value needs to be a string. ## Examples or or or or """ @spec default_opts() :: [Flop.Phoenix.table_option()] def default_opts do [ container: false, container_attrs: [class: "table-container"], no_results_content: HTML.Tag.content_tag(:p, do: "No results."), symbol_asc: "▴", symbol_attrs: [class: "order-direction"], symbol_desc: "▾", symbol_unsorted: nil, table_attrs: [], tbody_attrs: [], tbody_td_attrs: [], tbody_tr_attrs: [], th_wrapper_attrs: [], thead_th_attrs: [], thead_tr_attrs: [] ] end @doc """ 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) assign_new(assigns, :id, fn -> case meta.schema do nil -> "paginated_table" module -> module_name = module |> Module.split() |> List.last() |> Macro.underscore() module_name <> "_table" end end) end defp merge_opts(opts) do default_opts() |> Misc.deep_merge(Misc.get_global_opts(:table)) |> Misc.deep_merge(opts) end attr :id, :string, required: true attr :meta, Flop.Meta, required: true attr :path, :any, required: true attr :event, :string, required: true attr :target, :string, required: true attr :caption, :string, required: true attr :opts, :any, required: true attr :col, :any, required: true attr :items, :list, required: true attr :foot, :any, required: true attr :row_id, :any, default: nil attr :row_click, JS, default: nil attr :row_item, :any, required: true attr :action, :any, required: true def render(assigns) do assigns = with %{items: %Phoenix.LiveView.LiveStream{}} <- assigns do assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) end ~H""" <%= for col <- @col do %> <% end %> <%= for action <- @action do %> <% end %> <%= for col <- @col do %> <.header_column :if={show_column?(col)} event={@event} field={col[:field]} label={col[:label]} meta={@meta} opts={@opts} path={@path} target={@target} /> <% end %> <%= for action <- @action do %> <.header_column :if={show_column?(action)} event={@event} field={nil} label={action[:label]} meta={@meta} opts={@opts} path={nil} target={@event} /> <% end %> <%= for col <- @col do %> <% end %> <%= render_slot(@foot) %>
<%= @caption %>
<%= render_slot(col, @row_item.(item)) %> <%= render_slot(action, @row_item.(item)) %>
""" end defp show_column?(%{hide: true}), do: false defp show_column?(%{show: false}), do: false defp show_column?(_), do: true attr :meta, Flop.Meta, required: true attr :field, :atom, required: true attr :label, :string, required: true attr :path, :any, required: true attr :event, :string, required: true attr :target, :string, required: true attr :opts, :any, required: true defp header_column(assigns) do index = order_index(assigns.meta.flop, assigns.field) direction = order_direction(assigns.meta.flop.order_directions, index) assigns = assigns |> assign(:order_index, index) |> assign(:order_direction, direction) ~H""" <%= if sortable?(@field, @meta.schema) do %> <%= 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 %> <% end %> <.arrow direction={@order_direction} opts={@opts} /> <% else %> <%= @label %> <% end %> """ end defp aria_sort(0, direction), do: direction_to_aria(direction) defp aria_sort(_, _), do: nil defp direction_to_aria(:desc), do: "descending" defp direction_to_aria(:desc_nulls_last), do: "descending" defp direction_to_aria(:desc_nulls_first), do: "descending" defp direction_to_aria(:asc), do: "ascending" defp direction_to_aria(:asc_nulls_last), do: "ascending" defp direction_to_aria(:asc_nulls_first), do: "ascending" attr :direction, :atom, required: true attr :opts, :list, required: true defp arrow(assigns) do ~H""" <%= if @direction in [:asc, :asc_nulls_first, :asc_nulls_last] do %> <%= @opts[:symbol_asc] %> <% end %> <%= if @direction in [:desc, :desc_nulls_first, :desc_nulls_last] do %> <%= @opts[:symbol_desc] %> <% end %> <%= if is_nil(@direction) && !is_nil(@opts[:symbol_unsorted]) do %> <%= @opts[:symbol_unsorted] %> <% end %> """ end attr :field, :atom, required: true attr :label, :string, required: true attr :event, :string, required: true attr :target, :string, required: true defp sort_link(assigns) do ~H""" <.link phx-click={@event} phx-target={@target} phx-value-order={@field}> <%= @label %> """ end defp order_index(%Flop{order_by: nil}, _), do: nil defp order_index(%Flop{order_by: order_by}, field) do Enum.find_index(order_by, &(&1 == field)) end defp order_direction(_, nil), do: nil defp order_direction(nil, _), do: :asc defp order_direction(directions, index), do: Enum.at(directions, index) defp sortable?(nil, _), do: false defp sortable?(_, nil), do: true defp sortable?(field, module) do field in (module |> struct() |> Flop.Schema.sortable()) end end