Packages
corex
0.1.0-rc.1
0.1.2
0.1.1
0.1.0
0.1.0-rc.1
0.1.0-rc.0
0.1.0-beta.5
0.1.0-beta.4
0.1.0-beta.3
0.1.0-beta.2
0.1.0-beta.1
0.1.0-alpha.33
0.1.0-alpha.32
0.1.0-alpha.31
0.1.0-alpha.30
0.1.0-alpha.29
0.1.0-alpha.28
0.1.0-alpha.27
0.1.0-alpha.26
0.1.0-alpha.25
0.1.0-alpha.24
0.1.0-alpha.23
0.1.0-alpha.22
0.1.0-alpha.21
0.1.0-alpha.20
0.1.0-alpha.19
0.1.0-alpha.18
0.1.0-alpha.17
0.1.0-alpha.16
0.1.0-alpha.15
0.1.0-alpha.14
0.1.0-alpha.13
0.1.0-alpha.12
0.1.0-alpha.11
0.1.0-alpha.10
0.1.0-alpha.9
0.1.0-alpha.8
0.1.0-alpha.7
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
Accessible and unstyled UI components library written in Elixir and TypeScript that integrates Zag.js state machines into the Phoenix Framework.
Current section
Files
Jump to
Current section
Files
lib/components/data_list.ex
defmodule Corex.DataList do
@moduledoc ~S'''
Phoenix component for rendering semantic data lists.
## Anatomy
<!-- tabs-open -->
### Minimal
Pass a list of `%Corex.Content.Item{}` structs via `Corex.Content.new/1`.
```heex
<.data_list
class="data-list"
items={
Corex.Content.new([
%{label: "Name", content: "Marie Curie"},
%{label: "Field", content: "Physics"},
%{label: "Born", content: "1867"}
])
}
/>
```
### Manual slots
With an empty `items` list, use `:label` and `:content` slots with a matching `value` on each pair.
```heex
<.data_list class="data-list">
<:label value="lorem">Lorem ipsum dolor sit amet</:label>
<:content value="lorem">Consectetur adipiscing elit.</:content>
<:label value="duis">Duis dictum gravida odio ac pharetra?</:label>
<:content value="duis">Nullam eget vestibulum ligula.</:content>
</.data_list>
```
### Custom slots
Combine `items` with `:label` and `:content` slots using `:let={item}` for custom markup.
```heex
<.data_list
class="data-list"
items={
Corex.Content.new([
%{value: "status", label: "Status", content: "Active", meta: %{color: "green"}},
%{value: "role", label: "Role", content: "Admin", meta: %{color: "blue"}}
])
}
>
<:label :let={item}>{item.label}</:label>
<:content :let={item}>{item.content}</:content>
</.data_list>
```
### Empty
Optional `<:empty>` when there are no rows. The empty block renders beside the `<dl>` (not inside it) and is hidden by CSS when items exist (stream-friendly).
```heex
<.data_list class="data-list" items={[]}>
<:empty>No entries</:empty>
</.data_list>
```
<!-- tabs-close -->
## Stream
Keep a plain list assign for `items` and update it alongside `stream_insert/3` or `stream/3` reset.
Pass `items={Corex.Content.new(@items_list)}` to the component. Include `<:empty>` so an empty list shows the empty state without counting stream entries.
## Style
```css
[data-scope="data-list"][data-part="root"] {}
[data-scope="data-list"][data-part="item"] {}
[data-scope="data-list"][data-part="label"] {}
[data-scope="data-list"][data-part="content"] {}
[data-scope="data-list"][data-part="empty"] {}
```
'''
@doc type: :component
use Phoenix.Component
@doc """
Renders a semantic data list.
## Anatomy
<!-- tabs-open -->
### Minimal
```heex
<.data_list
class="data-list"
items={
Corex.Content.new([
%{label: "Name", content: "Marie Curie"},
%{label: "Field", content: "Physics"}
])
}
/>
```
### Manual slots
```heex
<.data_list class="data-list">
<:label value="lorem">Lorem ipsum dolor sit amet</:label>
<:content value="lorem">Consectetur adipiscing elit.</:content>
</.data_list>
```
### Custom slots
```heex
<.data_list class="data-list" items={Corex.Content.new([...])}>
<:label :let={item}>{item.label}</:label>
<:content :let={item}>{item.content}</:content>
</.data_list>
```
### Empty
```heex
<.data_list class="data-list" items={[]}>
<:empty>No entries</:empty>
</.data_list>
```
<!-- tabs-close -->
"""
attr(:items, :list,
default: [],
doc: "List of %Corex.Content.Item{} structs from Corex.Content.new/1"
)
attr(:rest, :global)
attr(:orientation, :string,
default: "vertical",
values: ["vertical", "horizontal"],
doc: "Layout orientation of items"
)
attr(:dir, :string,
default: nil,
values: [nil, "ltr", "rtl"],
doc: "Text direction"
)
slot :label,
required: false,
doc: "Manual: label slot with `value`. Custom: `:let={item}` with `items`." do
attr(:value, :string)
attr(:class, :string)
end
slot :content,
required: false,
doc: "Manual: content slot with `value`. Custom: `:let={item}` with `items`." do
attr(:value, :string)
attr(:class, :string)
end
slot(:empty, doc: "Optional content when the list has no rows")
def data_list(assigns) do
assigns =
assigns
|> assign(:items, assigns.items || [])
|> data_list_assign_manual_mode!()
|> then(fn a -> assign(a, :data_list_has_items, data_list_has_items?(a)) end)
~H"""
<div {@rest}>
<div
:if={@empty != []}
data-scope="data-list"
data-part="empty"
>
{render_slot(@empty)}
</div>
<dl
:if={@data_list_has_items}
data-scope="data-list"
data-part="root"
data-orientation={@orientation}
dir={@dir}
>
<div
:if={@data_list_manual_mode}
:for={panel <- @data_list_manual_panels}
class={panel_label_class(panel)}
data-scope="data-list"
data-part="item"
>
<dt data-scope="data-list" data-part="label">
{render_slot(panel.label)}
</dt>
<dd data-scope="data-list" data-part="content">
{render_slot(panel.content)}
</dd>
</div>
<div
:if={!@data_list_manual_mode}
:for={entry <- @items}
data-scope="data-list"
data-part="item"
>
<dt data-scope="data-list" data-part="label">
{if @label != [], do: render_slot(@label, entry), else: entry.label}
</dt>
<dd data-scope="data-list" data-part="content">
{if @content != [], do: render_slot(@content, entry), else: entry.content}
</dd>
</div>
</dl>
</div>
"""
end
defp data_list_assign_manual_mode!(assigns) do
manual? = manual_data_list_mode?(assigns)
if manual? and assigns.items != [] do
raise ArgumentError,
"DataList: use either items={Corex.Content.new([...])} or manual :label/:content slots, not both"
end
if not manual? and assigns.items != [] and manual_slot_entries?(assigns) do
raise ArgumentError,
"DataList: manual :label/:content slots with value require an empty items list; use :let={item} with items"
end
if manual? do
panels =
Corex.Slot.resolve_panels!(
%{label: assigns.label, content: assigns.content},
required: [:label, :content],
component: "DataList"
)
assign(assigns, :data_list_manual_mode, true)
|> assign(:data_list_manual_panels, panels)
else
assign(assigns, :data_list_manual_mode, false)
|> assign(:data_list_manual_panels, [])
end
end
defp manual_data_list_mode?(assigns) do
Enum.empty?(assigns.items) and assigns.label != [] and assigns.content != []
end
defp manual_slot_entries?(assigns) do
Enum.any?(assigns.label, &Map.get(&1, :value)) or
Enum.any?(assigns.content, &Map.get(&1, :value))
end
defp data_list_has_items?(assigns) do
(assigns.data_list_manual_mode and assigns.data_list_manual_panels != []) or
(!assigns.data_list_manual_mode and assigns.items != [])
end
defp panel_label_class(%{label: label}) when is_map(label) do
Map.get(label, :class)
end
defp panel_label_class(_), do: nil
end