Packages
moon
2.76.1
2.90.5
retired
2.90.4
2.90.3
2.90.2
2.90.1
2.90.0
2.89.4
2.89.1
2.89.0
2.88.0
2.87.11
2.87.10
2.87.9
2.87.8
2.87.7
2.87.6
2.87.5
2.87.4
2.87.3
2.87.2
2.87.1
2.87.0
2.86.1
2.86.0
2.85.1
2.85.0
2.84.0
2.83.0
2.82.0
2.81.4
2.81.3
2.81.2
2.81.1
2.81.0
2.80.2
2.80.1
2.80.0
2.79.13
2.79.12
2.79.11
2.79.10
2.79.9
2.79.8
2.79.7
2.79.6
2.79.5
2.79.4
2.79.3
2.79.2
2.79.1
2.79.0
2.78.4
2.78.3
2.78.2
2.78.1
2.78.0
2.77.0
2.76.5
2.76.4
2.76.3
2.76.2
2.76.1
2.76.0
2.75.0
2.74.1
2.74.0
2.73.8
2.73.7
2.73.6
2.73.5
2.73.4
2.73.3
2.73.2
2.73.1
2.73.0
2.72.5
2.72.4
2.72.3
2.72.2
2.72.1
2.72.0
2.71.1
2.71.0
2.70.0
2.69.2
2.69.1
2.69.0
2.68.11
2.68.10
Components-based design system written in elixir
Current section
Files
Jump to
Current section
Files
lib/moon/design/table.ex
defmodule Moon.Design.Table do
@moduledoc false
use Moon.StatelessComponent
alias Moon.Icon
import Moon.Helpers.MakeList, only: [add_index_as: 1]
@doc "List of selected ids, or just id, or [], or nil"
prop(selected, :any, default: [])
@doc "Sorting of the table, format [field:, \"ASC\"|\"DESC\"]. If given, component will sort given items before displaying"
prop(sort, :keyword, default: [])
@doc "The list of items to be rendered. If item does not have id - than index is used instead. Able to work with streams"
prop(items, :generator, required: true)
@doc "Event that firset on row click"
prop(row_click, :event)
@doc "Callback for generating on_click per row. row and row_id will bi given as parameters"
prop(row_click_cb, :any)
@doc "Sorting stuff"
prop(sorting_click, :event)
@doc "Can be used to add any class to the table"
prop(row_gap, :css_class, default: "border-spacing-y-1")
@doc "Size of the row. not a css class!"
prop(row_size, :string, values!: ~w(xs sm md lg xl 2xl), default: "md")
@doc "If cell has border"
prop(is_cell_border, :boolean, default: false)
@doc "If table has column headers or not"
prop(is_headless, :boolean, default: false)
@doc "If table is styled to present its rows in automatically alternating colours"
prop(is_zebra_style, :boolean, default: false)
@doc "Can be used as an additional class for all the rows"
prop(row_bg, :css_class, default: "bg-goku")
@doc "Can be used as an additional class for selected rows"
prop(selected_bg, :css_class, default: "bg-beerus")
@doc "Can be used as an additional class for header row"
prop(header_row_class, :css_class)
@doc "Can be used as an additional class for even rows in zebra-style table"
prop(even_row_bg, :css_class, default: "bg-transparent")
@doc "Can be used as an additional class for all rows. please use hover:... tailwind's format"
prop(hover_bg, :css_class)
@doc "The list of columns defining the Grid"
slot(cols, generator_prop: :items)
@doc "Just an id for a table tag"
prop(id, :string)
@doc "Data-testid attribute for a table tag"
prop(testid, :string)
@doc "Additional classes for a table tag"
prop(class, :css_class)
@doc "Additional attributes for tbody tag"
prop(body_attrs, :map, default: %{})
def render(assigns) do
~F"""
<table
class={
merge([
[
"text-sm border-spacing-x-0 border-beerus min-w-full bg-gohan rounded-moon-s-sm",
@row_gap,
"border-spacing-y-0": @is_zebra_style
],
@class
]),
"border-separate"
}
{=@id}
data-testid={@testid}
>
<thead :if={!@is_headless}>
<tr class={@header_row_class}>
{#for {col, col_index} <- Enum.with_index(@cols)}
<th
class={merge([
"text-left font-medium whitespace-nowrap",
text_size_classes(@row_size),
[
"#{inter_cell_border()}": @is_cell_border && col_index < Enum.count(@cols) - 1,
"cursor-pointer": col.sortable && @sorting_click
],
col.width
])}
:on-click={(col.sortable && col.name && @sorting_click) || nil}
:values={"sort-key": col.name, "sort-dir": toggle_sort_dir(@sort[:"#{col.name}"])}
data-testid={"sort-column-#{col.name}"}
>
{col.label}
<Icon
:if={col.name && col.sortable}
class={
"text-[1.5em] transition-transform transition-200",
"rotate-180": @sort[:"#{col.name}"] != "ASC",
"opacity-0": !@sort[:"#{col.name}"]
}
name="arrows_up"
/>
</th>
{/for}
</tr>
</thead>
<tbody {...@body_attrs}>
{#for {row_index, item} <- stream_data(assigns)}
<tr
class={merge([
(is_selected_item(item, @selected) && @selected_bg) || @row_bg,
@hover_bg,
"#{@even_row_bg}":
@is_zebra_style && !is_selected_item(item, @selected) && rem(row_index, 2) == 1,
"cursor-pointer": @row_click
])}
:on-click={(@row_click_cb && @row_click_cb.(item, row_index)) || @row_click}
:values={selected: "#{item.id}", domid: dom_id(row_index, @id)}
data-testid={"row-#{row_index}"}
id={dom_id(row_index, @id)}
>
{#for {col, col_index} <- Enum.with_index(@cols)}
<td
class={merge([
"first:rounded-l-moon-s-sm last:rounded-r-moon-s-sm",
col.width,
text_size_classes(@row_size),
["#{inter_cell_border()}": @is_cell_border && col_index < Enum.count(@cols) - 1],
col.class
])}
data-testid={"row-#{row_index}-col-#{col_index}"}
>
<#slot {col} generator_value={item} />
</td>
{/for}
</tr>
{/for}
</tbody>
</table>
"""
end
@doc "Sorts items by sort given"
def sort_items(items, sort) do
import Enum, only: [reverse: 1, reduce: 3, sort_by: 3]
reverse(sort)
|> reduce(items, fn {field, sort_dir}, list ->
sort_by(list, & &1[field], &if(sort_dir == "ASC", do: &1 < &2, else: &1 > &2))
end)
end
defp toggle_sort_dir(sort_dir) do
if "#{sort_dir}" == "ASC" do
"DESC"
else
"ASC"
end
end
defp is_selected_item(item, selected),
do: item[:is_selected] || is_selected(item[:id], selected)
defp is_selected(id, selected) when is_list(selected), do: "#{id}" in selected
defp is_selected(id, selected), do: "#{id}" == "#{selected}"
defp inter_cell_border() do
"after:content-[\"\"] after:absolute after:w-px after:bg-beerus " <>
"after:h-3/5 after:bottom-[20%] after:right-0 after:translate-x-[-50%] relative"
end
defp text_size_classes(row_size) do
case row_size do
"xs" -> "text-moon-12 py-1 px-2"
"sm" -> "text-moon-14 py-1 px-3"
"md" -> "text-moon-14 py-2 px-3"
"lg" -> "text-moon-14 py-3 px-3"
"xl" -> "text-moon-14 py-4 px-3"
"2xl" -> "text-moon-14 py-5 px-3"
end
end
defp stream_data(%{items: stream = %Phoenix.LiveView.LiveStream{}}) do
stream
end
defp stream_data(%{items: items, sort: sort}) when is_list(items) do
items
|> add_index_as()
|> sort_items(sort)
|> Enum.with_index(&{&2, &1})
end
defp dom_id(id, _) when is_binary(id), do: id
defp dom_id(id, id2), do: "#{id2}-row-#{id}"
end