Current section
Files
Jump to
Current section
Files
lib/components/table.ex
defmodule SigmaKit.Components.Table do
use Phoenix.LiveComponent
import SigmaKit.Components.Icons, only: [icon: 1]
import SigmaKit.Components.Buttons, only: [action_dropdown: 1]
@doc """
Renders a table with generic styling.
## Examples
<.table id="users" rows={@users}>
<:col :let={user} label="id">{user.id}</:col>
<:col :let={user} label="username">{user.username}</:col>
</.table>
"""
attr :id, :string, required: true, doc: "the unique id of the table"
attr :rows, :list, required: true, doc: "A list of row data to render"
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
attr :alternate, :boolean, default: false, doc: "whether to alternate row colors"
attr :paginate, :boolean, default: false, doc: "whether to show pagination"
attr :page_number, :integer, doc: "the current page number"
attr :total_pages, :integer, doc: "the total number of pages"
attr :page_size, :integer, doc: "the number of rows per page"
attr :total_entries, :integer, doc: "the total number of entries"
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"
slot :col, required: true, doc: "A column to render for the table" do
attr :label, :string, doc: "The label for the column header"
attr :shrink, :boolean, doc: "Whether to shrink the column to fit the content"
attr :expand, :boolean, doc: "Whether to expand the column to fit the content"
end
slot :action, doc: "An action item to present in a dropdown menu" do
attr :label, :string, doc: "The label for the menu item"
attr :event, :any, doc: "The phx-click event for the menu item"
attr :target, :string, doc: "The phx-target for the menu item"
attr :icon, :string, doc: "The icon for the menu item"
attr :divider, :boolean, doc: "Whether to this item is a divider"
end
def table(%{rows: %Scrivener.Page{}} = assigns) do
assigns
|> assign(rows: assigns.rows.entries)
|> assign(page_number: assigns.rows.page_number)
|> assign(total_pages: assigns.rows.total_pages)
|> assign(total_entries: assigns.rows.total_entries)
|> assign(page_size: assigns.rows.page_size)
|> assign(paginate: true)
|> table()
end
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0 w-full">
<table :if={!Enum.empty?(@rows)} class="w-full">
<thead class="text-sm text-left leading-6 text-zinc-600">
<tr>
<th
:for={col <- @col}
class="uppercase p-0 py-1 pr-6 font-bold border-b border-gray-300 text-xs"
>
{col[:label]}
</th>
<th
:if={!SigmaKit.Util.blank?(@action)}
class="uppercase p-0 py-1 pr-6 font-bold border-b border-gray-300 text-xs"
>
</th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr
:for={{row, row_idx} <- Enum.with_index(@rows)}
id={"row-#{@id}-#{(@row_id && @row_id.(row)) || row_idx}"}
class={[
"group",
@row_click && "hover:bg-primary-100",
@alternate && "even:bg-gray-50 odd:bg-white"
]}
>
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && row_click(row, @row_click)}
phx-value-id={(@row_id && @row_id.(row)) || row_idx}
class={[
"relative p-0",
@row_click && "hover:cursor-pointer",
col[:shrink] && ["md:w-1 md:text-nowrap"],
col[:expand] && ["md:w-full md:text-nowrap"]
]}
>
<div class="block py-1 pr-6">
<span
:if={i == 0}
class={[
"absolute -inset-y-px right-0 -left-4 rounded-l",
@row_click && "group-hover:bg-primary-100"
]}
/>
<span
:if={i == length(@rows) - 1}
class={[
"absolute -inset-y-px left-0 -right-4 rounded-r bg-red",
@row_click && "group-hover:bg-primary-100"
]}
/>
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
{render_slot(col, @row_item.(row))}
</span>
</div>
</td>
<td
:if={!SigmaKit.Util.blank?(@action)}
class={[
"relative w-14 p-0",
@row_click && "group-hover:bg-primary-100"
]}
>
<span class={[
"absolute -inset-y-px left-0 -right-4 rounded-r bg-red",
@row_click && "group-hover:bg-primary-100"
]} />
<.action_dropdown
id={"actions-#{@id}-#{(@row_id && @row_id.(row)) || row_idx}"}
class={[
"text-black rounded",
@row_click && "group-hover:bg-primary-100"
]}
>
<:action
:for={action <- @action}
label={action[:label]}
icon={action[:icon]}
event={row_click(row, action[:event])}
target={action[:target]}
divider={action[:divider]}
value_id={row.id}
/>
</.action_dropdown>
</td>
</tr>
</tbody>
</table>
<div
:if={@paginate && @total_pages > 1}
class="px-2 pt-6 mt-4 border-t border-gray-400"
id={"#{@id}-pager"}
>
<.pagination
id={@id}
page_number={@page_number}
total_pages={@total_pages}
page_size={@page_size}
total_entries={@total_entries}
/>
</div>
<div :if={Enum.empty?(@rows)} class="p-8 text-center border-2 border-dashed bg-zinc-50">
<div class="mb-1">
<.icon name="hero-information-circle" class="h-12 w-12 text-zinc-400" />
</div>
<h4 class="font-medium text-md">No Data Available</h4>
</div>
</div>
"""
end
defp row_click(row, v) when is_function(v), do: v.(row)
defp row_click(_row, v), do: v
attr :id, :any, default: nil
attr :page_number, :integer, default: 1
attr :total_pages, :integer, required: true
attr :page_size, :integer, required: true
attr :total_entries, :integer, required: true
def pagination(assigns) do
~H"""
<div class="flex justify-between items-center w-full">
<div class="text-xs text-zinc-800 hidden md:block">
Showing {@page_number * @page_size} of {@total_entries}
</div>
<div class="flex border border-gray-200 rounded overflow-hidden">
<button
class="py-1 px-2 hover:bg-gray-700 hover:text-zinc-50 disabled:text-zinc-500 disabled:hover:bg-transparent transition-all"
phx-click="paginate"
disabled={@page_number == 1}
phx-value-page={@page_number - 1}
phx-value-id={@id}
>
<.icon name="hero-chevron-left" class="w-4 h-4" />
</button>
<button
:for={page <- page_button_range(@page_number, @total_pages)}
phx-click="paginate"
phx-value-page={page}
disabled={page > @total_pages}
phx-value-id={@id}
class={[
"py-1 px-2 w-[2.5em] text-center font-bold transition-all",
page == @page_number && "bg-blue-200/50 text-blue-800",
page != @page_number && "text-gray-700 hover:bg-gray-700 hover:text-zinc-50",
"disabled:hover:bg-transparent disabled:text-gray-800"
]}
>
{page}
</button>
<button
class="py-1 px-2 hover:bg-gray-700 hover:text-zinc-50 disabled:text-zinc-500 disabled:hover:bg-transparent transition-all"
phx-click="paginate"
disabled={@page_number == @total_pages}
phx-value-page={@page_number + 1}
phx-value-id={@id}
>
<.icon name="hero-chevron-right" class="w-4 h-4" />
</button>
</div>
</div>
"""
end
defp page_button_range(n, _max) when n <= 3, do: [1, 2, 3, 4, 5]
defp page_button_range(n, max) when n >= max - 2, do: [max - 4, max - 3, max - 2, max - 1, max]
defp page_button_range(n, _max) when n >= 4, do: [n - 2, n - 1, n, n + 1, n + 2]
end