Packages
noora
0.6.0
0.84.1
0.84.0
0.83.2
0.83.1
0.83.0
0.82.7
0.82.6
0.82.5
0.82.4
0.82.3
0.82.2
0.82.1
0.82.0
0.81.4
0.81.3
0.81.2
0.81.1
0.81.0
0.80.1
0.80.0
0.79.1
0.79.0
0.78.1
0.78.0
0.77.4
0.77.3
0.77.2
0.77.1
0.77.0
0.76.0
0.75.0
0.74.0
0.73.0
0.72.0
0.71.0
0.70.0
0.69.1
0.69.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.2
0.64.1
0.64.0
0.63.2
0.63.1
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.1
0.53.0
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.3
0.44.2
0.44.1
0.44.0
0.43.0
0.42.0
0.41.0
0.40.6
0.40.5
0.40.4
0.40.3
0.40.2
0.40.1
0.40.0
0.39.1
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.1
0.32.0
0.31.0
0.30.0
0.29.2
0.29.1
0.29.0
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.0
0.26.1
0.26.0
0.25.0
0.24.0
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.2
0.2.0
0.1.0
0.1.0-rc.2
0.1.0-rc.1
0.1.0-alpha.6
0.1.0-alpha.5
0.1.0-alpha.4
0.1.0-alpha.3
0.1.0-alpha.2
0.1.0-alpha.1
A component library for Phoenix LiveView applications
Current section
Files
Jump to
Current section
Files
lib/noora/table.ex
defmodule Noora.Table do
@moduledoc """
Noora's table component.
The table component is used to display data in a tabular format. It supports various types of cells, such as text, badges, and buttons.
## Usage
The component expects its content to be passed as a list of rows or a LiveView stream as the `rows` attribute. Each row is a map with its
values.
Columns are defined using the `col` slot, which takes a label and an optional icon. Inside the columns, a large number of different cells
are supported.
## Example
```
<.table
id="table-single-cell-types"
rows={[%{id: 1, label: "Row One", status: "error"}, %{id: 2, label: "Row Two", status: "success"}]}
>
<:col :let={i} label="Text">
<.text_cell label={i.label} sublabel="(Internal)" icon="alert_circle" />
</:col>
<:col :let={i} label="Status badge">
<.status_badge_cell label={i.status} status={i.status />
</:col>
</.table>
```
"""
use Phoenix.Component
import Noora.Badge
import Noora.Button
import Noora.Icon
import Noora.Tag
import Noora.Time
import Noora.Utils
attr(:id, :string, required: true, doc: "A uniqie identifier for the table")
attr(:rows, :list, required: true, doc: "The table content")
attr(:row_key, :fun,
default: nil,
doc:
"A function to generate the row key. Required when using a LiveView stream. If using streams and not provided, defaults to the `id` key of the stream."
)
attr(:row_navigate, :fun,
default: nil,
doc: "A function to generate the link to navigate to when clicking on a row."
)
attr(:row_click, :fun,
default: nil,
doc: "A function to generate the click handler for a row."
)
slot(:empty_state, required: false)
slot :col, required: true do
attr(:label, :string, required: false, doc: "The label of the column")
attr(:icon, :string, doc: "An icon to render next to the label")
attr(:patch, :string, doc: "A patch to apply to the column")
end
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns,
row_key: assigns.row_key || fn {id, _item} -> id end
)
end
~H"""
<div id={@id} class="noora-table">
<table>
<thead>
<tr>
<th :for={col <- @col}>
<%= if col[:patch] do %>
<.link patch={col[:patch]} data-part="sort-link">
<span>{col[:label]}</span>
<span :if={col[:icon]} data-part="icon"><.icon name={col[:icon]} /></span>
</.link>
<% else %>
<span>{col[:label]}</span>
<span :if={col[:icon]} data-part="icon"><.icon name={col[:icon]} /></span>
<% end %>
</th>
</tr>
</thead>
<tbody
id={"#{@id}-body"}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
>
<tr
:for={row <- @rows}
id={@row_key && @row_key.(row)}
{if @row_click, do: @row_click.(row) || %{}, else: %{}}
>
<td
:for={col <- @col}
data-selectable={
not is_nil(@row_navigate) or (not is_nil(@row_click) and not is_nil(@row_click.(row)))
}
>
<%= if @row_navigate do %>
<.link navigate={@row_navigate.(row)} data-part="link">
{render_slot(col, row)}
</.link>
<% else %>
{render_slot(col, row)}
<% end %>
</td>
</tr>
<tr :if={has_slot_content?(@empty_state, assigns) and Enum.empty?(@rows)}>
<td colspan={length(@col)}>
{render_slot(@empty_state)}
</td>
</tr>
</tbody>
</table>
</div>
"""
end
attr(:label, :string, default: nil, doc: "The label of the cell")
attr(:icon, :string,
default: nil,
doc: "An optional icon to render next to the label. Mutually exclusive with `image`."
)
attr(:sublabel, :string, default: nil, doc: "An optional sublabel")
attr(:rest, :global)
slot(:image,
required: false,
doc: "An optional image to render next to the label. Mutually exclusive with `icon`. Takes precedence over `icon`."
)
def text_cell(assigns) do
~H"""
<div data-part="cell" data-type="text" {@rest}>
<div :if={@icon || has_slot_content?(@image, assigns)} data-part="icon">
<.icon :if={@icon && !has_slot_content?(@image, assigns)} name={@icon} />
<%= if has_slot_content?(@image, assigns) do %>
{render_slot(@image)}
<% end %>
</div>
<span :if={@label} data-part="label">{@label}</span>
<span :if={@sublabel} data-part="sublabel">{@sublabel}</span>
</div>
"""
end
attr(:label, :string, default: nil, doc: "The label of the cell")
attr(:icon, :string,
default: nil,
doc: "An optional icon to render next to the label. Mutually exclusive with `image`."
)
attr(:description, :string, default: nil, doc: "The description of the cell")
attr(:secondary_description, :string, default: nil, doc: "The secondary description of the cell")
attr(:rest, :global)
slot(:image,
required: false,
doc: "An optional image to render next to the label. Takes precedence over `icon`."
)
def text_and_description_cell(assigns) do
~H"""
<div data-part="cell" data-type="text_and_description" {@rest}>
<div :if={@icon || has_slot_content?(@image, assigns)} data-part="icon">
<.icon :if={@icon && !has_slot_content?(@image, assigns)} name={@icon} />
<%= if has_slot_content?(@image, assigns) do %>
{render_slot(@image)}
<% end %>
</div>
<div data-part="column">
<span data-part="label">{@label}</span>
<span data-part="description">
{@description}
<span :if={@secondary_description} data-part="dot">•</span>
<span :if={@secondary_description} data-part="secondary_description">
{@secondary_description}
</span>
</span>
</div>
</div>
"""
end
attr(:label, :string, default: nil, doc: "The label of the badge")
attr(:icon, :string,
default: nil,
doc: "An optional icon to render next to the label."
)
attr(:style, :string,
values: ~w(fill light-fill),
default: "fill",
doc: "The style of the badge"
)
attr(:color, :string,
values: ~w(neutral destructive warning attention success information focus primary secondary),
default: "neutral",
doc: "The color of the badge"
)
attr(:rest, :global)
def badge_cell(assigns) do
~H"""
<div data-part="cell" data-type="badge" {@rest}>
<.badge style={@style} color={@color} size="large" label={@label}>
<:icon>
<.icon :if={@icon} name={@icon} />
</:icon>
</.badge>
</div>
"""
end
attr(:label, :string, default: nil, doc: "The label of the badge")
attr(:icon, :string,
default: nil,
doc: "An optional icon to render next to the label."
)
attr(:rest, :global)
def tag_cell(assigns) do
~H"""
<div data-part="cell" data-type="tag" {@rest}>
<.tag label={@label} icon={@icon} />
</div>
"""
end
attr(:status, :string,
values: ~w(success error warning disabled attention),
required: true,
doc: "The status of the badge"
)
attr(:label, :string, default: nil, doc: "The label of the badge")
attr(:rest, :global)
def status_badge_cell(assigns) do
~H"""
<div data-part="cell" data-type="status_badge" {@rest}>
<.status_badge status={@status} label={@label} />
</div>
"""
end
attr(:rest, :global)
slot(:button, required: true, doc: "The button or buttons to render")
def button_cell(assigns) do
~H"""
<div data-part="cell" data-type="button">
<%= for button <- @button do %>
{render_slot(button)}
<% end %>
</div>
"""
end
attr(:label, :string, required: true, doc: "The label of the button")
attr(:variant, :string,
values: button_variants(),
default: "primary",
doc: "Determines the style"
)
attr(:underline, :boolean, default: false, doc: "Determines if the button is underlined")
attr(:rest, :global)
slot(:icon_left, doc: "Icon displayed on the left of an item")
slot(:icon_right, doc: "Icon displayed on the right of an item")
def link_button_cell(assigns) do
~H"""
<div data-part="cell" data-type="link_button" {@rest}>
<.link_button label={@label} variant={@variant} underline={@underline} size="large">
<:icon_left>
{render_slot(@icon_left)}
</:icon_left>
<:icon_right>
{render_slot(@icon_right)}
</:icon_right>
</.link_button>
</div>
"""
end
attr(:time, DateTime, required: true, doc: "The time to render.")
attr(:show_time, :boolean, default: false, doc: "Whether to show the time or date only.")
attr(:relative, :boolean, default: false, doc: "Whether to show the time relative to now.")
attr(:rest, :global)
def time_cell(assigns) do
~H"""
<div data-part="cell" data-type="text" {@rest}>
<span data-part="label">
<.time time={@time} show_time={@show_time} relative={@relative} />
</span>
</div>
"""
end
attr(:icon, :string, default: nil, doc: "Icon to show in the empty state.")
attr(:title, :string, default: nil, doc: "Title of the empty state.")
attr(:subtitle, :string, default: nil, doc: "Subtitle of the empty state.")
slot(:inner_block, doc: "Custom empty state content. Supersedes all attributes.")
def table_empty_state(assigns) do
~H"""
<div class="noora-table-empty-state">
<%= if has_slot_content?(@inner_block, assigns) do %>
{render_slot(@inner_block)}
<% else %>
<div :if={@icon} data-part="icon">
<.icon name={@icon} />
</div>
<div :if={@title} data-part="title">
{@title}
</div>
<div :if={@subtitle} data-part="subtitle">
{@subtitle}
</div>
<% end %>
</div>
"""
end
end