Packages
flop_phoenix
0.25.3
0.26.1
0.26.0
0.25.3
0.25.2
0.25.1
0.25.0
0.24.1
0.24.0
0.23.1
0.23.0
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.2
0.21.1
0.21.0
0.20.0
0.19.1
0.19.0
0.18.2
0.18.1
0.18.0
0.17.2
0.17.1
0.17.0
0.16.0
0.15.2
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.0
0.12.0
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.1
0.8.0
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Phoenix components for pagination, sortable tables and filter forms using Flop.
Current section
Files
Jump to
Current section
Files
lib/flop_phoenix/table.ex
defmodule Flop.Phoenix.Table do
@moduledoc false
use Phoenix.Component
alias Flop.Phoenix.Misc
alias Phoenix.LiveView.JS
require Logger
@spec default_opts() :: [Flop.Phoenix.table_option()]
def default_opts do
[
container: false,
container_attrs: [class: "table-container"],
no_results_content: Phoenix.HTML.raw("<p>No results.</p>"),
symbol_asc: "â–´",
symbol_attrs: [class: "order-direction"],
symbol_desc: "â–¾",
symbol_unsorted: nil,
table_attrs: [],
tbody_attrs: [],
tbody_td_attrs: [],
tbody_tr_attrs: [],
thead_attrs: [],
th_wrapper_attrs: [],
thead_th_attrs: [],
thead_tr_attrs: []
]
end
def 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 :on_sort, JS
attr :target, :string, required: true
attr :caption, :string, required: true
attr :opts, :any, required: true
attr :col, :any
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"""
<table id={@id} {@opts[:table_attrs]}>
<caption :if={@caption}>{@caption}</caption>
<.maybe_colgroup col={@col ++ @action} />
<thead {@opts[:thead_attrs]}>
<tr {@opts[:thead_tr_attrs]}>
<.header_column
:for={col <- @col}
on_sort={@on_sort}
field={col[:field]}
label={col[:label]}
sortable={sortable?(col[:field], @meta.schema)}
directions={col[:directions]}
meta={@meta}
thead_th_attrs={
merge_attrs(@opts[:thead_th_attrs], col, :thead_th_attrs)
}
symbol_asc={@opts[:symbol_asc]}
symbol_desc={@opts[:symbol_desc]}
symbol_unsorted={@opts[:symbol_unsorted]}
symbol_attrs={@opts[:symbol_attrs]}
th_wrapper_attrs={
merge_attrs(@opts[:th_wrapper_attrs], col, :th_wrapper_attrs)
}
path={@path}
target={@target}
/>
<.header_column
:for={action <- @action}
field={nil}
label={action[:label]}
sortable={false}
meta={@meta}
thead_th_attrs={
merge_attrs(@opts[:thead_th_attrs], action, :thead_th_attrs)
}
path={nil}
target={@target}
/>
</tr>
</thead>
<tbody
id={@id <> "-tbody"}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @items) && "stream"}
{@opts[:tbody_attrs]}
>
<tr
:for={item <- @items}
id={@row_id && @row_id.(item)}
{maybe_invoke_options_callback(@opts[:tbody_tr_attrs], item)}
>
<td
:for={col <- @col}
{merge_td_attrs(@opts[:tbody_td_attrs], col, item)}
phx-click={@row_click && @row_click.(item)}
>
{render_slot(col, @row_item.(item))}
</td>
<td
:for={action <- @action}
{merge_td_attrs(@opts[:tbody_td_attrs], action, item)}
>
{render_slot(action, @row_item.(item))}
</td>
</tr>
</tbody>
<tfoot :if={@foot != []}>{render_slot(@foot)}</tfoot>
</table>
"""
end
defp merge_attrs(base_attrs, col, key) when is_atom(key) do
attrs = Map.get(col, key, [])
Keyword.merge(base_attrs, attrs)
end
defp merge_td_attrs(tbody_td_attrs, col, item) do
attrs =
col |> Map.get(:tbody_td_attrs, []) |> maybe_invoke_options_callback(item)
Keyword.merge(tbody_td_attrs, attrs)
end
defp maybe_invoke_options_callback(option, item) when is_function(option) do
option.(item)
end
defp maybe_invoke_options_callback(option, _item), do: option
defp maybe_colgroup(assigns) do
~H"""
<colgroup :if={Enum.any?(@col, &(&1[:col_style] || &1[:col_class]))}>
<col
:for={col <- @col}
{reject_empty_values(style: col[:col_style], class: col[:col_class])}
/>
</colgroup>
"""
end
defp reject_empty_values(attrs) do
Enum.reject(attrs, fn {_, v} -> v in ["", nil] end)
end
attr :meta, Flop.Meta, required: true
attr :field, :atom, required: true
attr :label, :any, required: true
attr :path, :any, required: true
attr :on_sort, JS
attr :target, :string, required: true
attr :sortable, :boolean, required: true
attr :thead_th_attrs, :list, required: true
attr :directions, :any
attr :symbol_asc, :any
attr :symbol_desc, :any
attr :symbol_unsorted, :any
attr :symbol_attrs, :list
attr :th_wrapper_attrs, :list
defp header_column(%{sortable: true} = assigns) do
direction = order_direction(assigns.meta.flop, assigns.field)
assigns = assign(assigns, :order_direction, direction)
sort_path_options =
if directions = assigns[:directions],
do: [directions: directions],
else: []
sort_path =
build_path(
assigns[:path],
assigns[:meta],
assigns[:field],
sort_path_options
)
assigns = assign(assigns, :sort_path, sort_path)
~H"""
<th {@thead_th_attrs} aria-sort={aria_sort(@order_direction)}>
<span {@th_wrapper_attrs}>
<.sort_link
path={@sort_path}
on_sort={@on_sort}
field={@field}
label={@label}
target={@target}
/>
<.arrow
direction={@order_direction}
symbol_asc={@symbol_asc}
symbol_desc={@symbol_desc}
symbol_unsorted={@symbol_unsorted}
{@symbol_attrs}
/>
</span>
</th>
"""
end
defp header_column(%{sortable: false} = assigns) do
~H"""
<th {@thead_th_attrs}>{@label}</th>
"""
end
defp aria_sort(:desc), do: "descending"
defp aria_sort(:desc_nulls_last), do: "descending"
defp aria_sort(:desc_nulls_first), do: "descending"
defp aria_sort(:asc), do: "ascending"
defp aria_sort(:asc_nulls_last), do: "ascending"
defp aria_sort(:asc_nulls_first), do: "ascending"
defp aria_sort(_), do: nil
attr :direction, :atom, required: true
attr :symbol_asc, :any, required: true
attr :symbol_desc, :any, required: true
attr :symbol_unsorted, :any, required: true
attr :rest, :global
defp arrow(%{direction: direction} = assigns)
when direction in [:asc, :asc_nulls_first, :asc_nulls_last] do
~H"<span {@rest}>{@symbol_asc}</span>"
end
defp arrow(%{direction: direction} = assigns)
when direction in [:desc, :desc_nulls_first, :desc_nulls_last] do
~H"<span {@rest}>{@symbol_desc}</span>"
end
defp arrow(%{direction: nil, symbol_unsorted: nil} = assigns) do
~H""
end
defp arrow(%{direction: nil} = assigns) do
~H"<span {@rest}>{@symbol_unsorted}</span>"
end
attr :field, :atom, required: true
attr :label, :string, required: true
attr :path, :string
attr :on_sort, JS
attr :target, :string
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
patch={@path}
phx-click={@on_sort}
phx-target={@target}
phx-value-order={@field}
>
{@label}
</.link>
"""
end
defp order_direction(
%Flop{order_by: [field | _], order_directions: [direction | _]},
field
) do
direction
end
defp order_direction(%Flop{}, _), do: nil
defp sortable?(nil, _), do: false
defp sortable?(_, nil), do: true
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,
opts
) do
Flop.Phoenix.build_path(
path,
Flop.push_order(meta.flop, field, opts),
backend: meta.backend,
for: meta.schema
)
end
end