memEx/lib/memex_web/components/table_component.ex

160 lines
5.0 KiB
Elixir
Raw Permalink Normal View History

2022-07-25 19:31:54 -04:00
defmodule MemexWeb.Components.TableComponent do
2022-03-05 23:13:37 -05:00
@moduledoc """
Livecomponent that presents a resortable table
It takes the following required assigns:
- `:columns`: An array of maps containing the following keys
- `:label`: A gettext'd or otherwise user-facing string label for the
column. Can be nil
2022-11-16 23:05:59 -05:00
- `:key`: An atom key used for sorting
2022-03-05 23:13:37 -05:00
- `:class`: Extra classes to be applied to the column element, if desired.
Optional
- `:sortable`: If false, will prevent the user from sorting with it.
Optional
- `:values`: An array of maps containing data for each row. Each map is
string-keyed with the associated column key to the following values:
- A single element, like string, integer or Phoenix.LiveView.Rendered
object, like returned from the ~H sigil
- A tuple, containing a custom value used for sorting, and the displayed
content.
"""
2022-07-25 19:31:54 -04:00
use MemexWeb, :live_component
2022-03-05 23:13:37 -05:00
alias Phoenix.LiveView.Socket
2022-11-16 23:05:59 -05:00
require Integer
2022-03-05 23:13:37 -05:00
@impl true
@spec update(
%{
required(:columns) =>
list(%{
required(:label) => String.t() | nil,
2022-11-16 23:05:59 -05:00
required(:key) => atom() | nil,
2022-03-05 23:13:37 -05:00
optional(:class) => String.t(),
2022-11-16 23:05:59 -05:00
optional(:row_class) => String.t(),
optional(:alternate_row_class) => String.t(),
optional(:sortable) => false,
optional(:type) => module()
2022-03-05 23:13:37 -05:00
}),
required(:rows) =>
list(%{
2022-11-16 23:05:59 -05:00
(key :: atom()) => any() | {custom_sort_value :: String.t(), value :: any()}
2022-03-05 23:13:37 -05:00
}),
2022-11-16 23:05:59 -05:00
optional(:inital_key) => atom(),
optional(:initial_sort_mode) => atom(),
2022-03-05 23:13:37 -05:00
optional(any()) => any()
},
Socket.t()
) :: {:ok, Socket.t()}
def update(%{columns: columns, rows: rows} = assigns, socket) do
2022-11-16 23:05:59 -05:00
initial_key =
if assigns |> Map.has_key?(:initial_key) do
assigns.initial_key
else
columns |> List.first(%{}) |> Map.get(:key)
end
initial_sort_mode =
if assigns |> Map.has_key?(:initial_sort_mode) do
assigns.initial_sort_mode
else
:asc
end
type = columns |> Enum.find(%{}, fn %{key: key} -> key == initial_key end) |> Map.get(:type)
rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode, type)
2022-03-05 23:13:37 -05:00
socket =
socket
|> assign(assigns)
2022-11-16 23:05:59 -05:00
|> assign(
columns: columns,
rows: rows,
key: initial_key,
2022-11-16 23:05:59 -05:00
last_sort_key: initial_key,
sort_mode: initial_sort_mode
)
2022-11-17 22:24:28 -05:00
|> assign_new(:row_class, fn -> "bg-primary-900" end)
|> assign_new(:alternate_row_class, fn -> "bg-primary-900" end)
2022-03-05 23:13:37 -05:00
{:ok, socket}
end
@impl true
def handle_event(
"sort_by",
%{"sort-key" => key},
%{
assigns: %{
columns: columns,
rows: rows,
last_sort_key: last_sort_key,
sort_mode: sort_mode
}
} = socket
2022-03-05 23:13:37 -05:00
) do
2022-11-16 23:05:59 -05:00
key = key |> String.to_existing_atom()
2022-03-05 23:13:37 -05:00
2022-11-16 23:05:59 -05:00
sort_mode =
case {key, sort_mode} do
{^last_sort_key, :asc} -> :desc
{^last_sort_key, :desc} -> :asc
{_new_sort_key, _last_sort_mode} -> :asc
end
type =
columns |> Enum.find(%{}, fn %{key: column_key} -> column_key == key end) |> Map.get(:type)
rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode, type)
2022-11-16 23:05:59 -05:00
{:noreply, socket |> assign(last_sort_key: key, sort_mode: sort_mode, rows: rows)}
2022-03-05 23:13:37 -05:00
end
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, type)
when type in [Date, DateTime] do
rows
|> Enum.sort_by(
fn row ->
case row |> Map.get(key) do
{custom_sort_key, _value} -> custom_sort_key
value -> value
end
end,
{sort_mode, type}
)
end
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, _type) do
2022-03-05 23:13:37 -05:00
rows
|> Enum.sort_by(
fn row ->
case row |> Map.get(key) do
{custom_sort_key, _value} -> custom_sort_key
value -> value
end
end,
sort_mode
)
end
2023-03-23 00:22:04 -04:00
@doc """
Conditionally composes elements into the columns list, supports maps and
lists. Works tail to front in order for efficiency
iex> []
...> |> maybe_compose_columns(%{label: "Column 3"}, true)
...> |> maybe_compose_columns(%{label: "Column 2"}, false)
...> |> maybe_compose_columns(%{label: "Column 1"})
[%{label: "Column 1"}, %{label: "Column 3"}]
"""
@spec maybe_compose_columns(list(), element_to_add :: list() | map()) :: list()
@spec maybe_compose_columns(list(), element_to_add :: list() | map(), boolean()) :: list()
def maybe_compose_columns(columns, element_or_elements, add? \\ true)
def maybe_compose_columns(columns, elements, true) when is_list(elements),
do: Enum.concat(elements, columns)
def maybe_compose_columns(columns, element, true) when is_map(element), do: [element | columns]
def maybe_compose_columns(columns, _element_or_elements, false), do: columns
2022-03-05 23:13:37 -05:00
end