Examples
View SourceThis document provides comprehensive examples and detailed reference for all Cinder table features. For a quick start, see the README.
Overview
Cinder supports two parameter styles:
resource
- Simple usage with Ash resource modulesquery
- Advanced usage with pre-configured Ash queries
Choose resource
for most cases, query
for complex requirements like custom read actions, base filters, or admin interfaces.
Table of Contents
- Basic Usage
- Resource vs Query
- Column Configuration
- Filter Types
- Sorting
- Theming
- URL State Management
- Relationship Fields
- Embedded Resources
- Action Columns
- Table Refresh
- Advanced Configuration
- Performance Optimization
Basic Usage
Minimal Table
The simplest possible table:
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<:col :let={user} field="name">{user.name}</:col>
<:col :let={user} field="email">{user.email}</:col>
</Cinder.Table.table>
Resource vs Query
Cinder supports two ways to specify what data to query: resource
parameter (simple) or query
parameter (advanced).
When to Use Resource
Use the resource
parameter for straightforward tables:
<!-- Simple table with default read action -->
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
</Cinder.Table.table>
Best for:
- Getting started quickly
- Standard use cases without custom requirements
- Default read actions
- Simple authorization scenarios
When to Use Query
Use the query
parameter for advanced scenarios:
<!-- Custom read action -->
<Cinder.Table.table query={Ash.Query.for_read(MyApp.User, :active_users)} actor={@current_user}>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
</Cinder.Table.table>
<!-- Pre-filtered data -->
<Cinder.Table.table query={MyApp.User |> Ash.Query.filter(department: "Engineering")} actor={@current_user}>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="department.name" filter>{user.department.name}</:col>
</Cinder.Table.table>
<!-- Admin interface with complex authorization -->
<Cinder.Table.table
query={MyApp.User
|> Ash.Query.for_read(:admin_read, %{}, actor: @actor, authorize?: @authorizing)
|> Ash.Query.set_tenant(@tenant)
|> Ash.Query.filter(active: true)}
actor={@current_user}>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
<:col :let={user} field="last_login" sort>{user.last_login}</:col>
</Cinder.Table.table>
Best for:
- Custom read actions (e.g.,
:active_users
,:admin_only
) - Pre-filtering data with base filters
- Custom authorization settings
- Tenant-specific queries
- Admin interfaces with complex requirements
- Integration with existing Ash query pipelines
Automatic Label Generation
Cinder automatically generates human-readable labels from field names:
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<:col :let={user} field="first_name">{user.first_name}</:col> <!-- "First Name" -->
<:col :let={user} field="email_address">{user.email_address}</:col> <!-- "Email Address" -->
<:col :let={user} field="created_at">{user.created_at}</:col> <!-- "Created At" -->
<:col :let={user} field="is_active">{user.is_active}</:col> <!-- "Is Active" -->
<:col :let={user} field="phone_number">{user.phone_number}</:col> <!-- "Phone Number" -->
</Cinder.Table.table>
Custom Labels
Override auto-generated labels when needed:
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<:col :let={user} field="name" label="Full Name">{user.name}</:col>
<:col :let={user} field="email" label="Email Address">{user.email}</:col>
<:col :let={user} field="created_at" label="Joined">{user.created_at}</:col>
<:col :let={user} field="is_active" label="Status">{user.is_active}</:col>
</Cinder.Table.table>
Column Configuration
All Column Attributes
Demonstration of every available column attribute:
<Cinder.Table.table resource={MyApp.Product} actor={@current_user}>
<!-- Basic column with all common attributes -->
<:col
:let={product}
field="name"
label="Product Name"
filter={:text}
sort={true}
class="w-1/4 font-semibold"
>
{product.name}
</:col>
<!-- Column with custom filter options -->
<:col
:let={product}
field="category"
filter={:select}
filter_options={[
options: [{"Electronics", "electronics"}, {"Books", "books"}, {"Clothing", "clothing"}],
prompt: "All Categories"
]}
sort
>
{product.category}
</:col>
<!-- Number column with range filter -->
<:col
:let={product}
field="price"
filter={:number_range}
filter_options={[
min: 0,
max: 1000,
step: 0.01
]}
sort
class="text-right"
>
${product.price}
</:col>
<!-- Boolean column with custom labels -->
<:col
:let={product}
field="in_stock"
filter={:boolean}
filter_options={[
labels: %{
all: "Any Stock Status",
true: "In Stock",
false: "Out of Stock"
}
]}
>
{if product.in_stock, do: "In Stock", else: "Out of Stock"}
</:col>
</Cinder.Table.table>
Filter Types
Cinder automatically detects the right filter type based on your Ash resource attributes:
- String fields → Text search
- Enum fields → Select dropdown
- Boolean fields → True/false/any radio buttons
- Date/DateTime fields → Date range picker
- Integer/Decimal fields → Number range inputs
- Array fields → Multi-select tag interface
You can also explicitly specify filter types: :text
, :select
, :multi_select
, :multi_checkboxes
, :boolean
, :date_range
, :number_range
Multi-Select Options
For multiple selection filtering, choose between:
:multi_select
- Modern tag-based interface with dropdown (default for arrays):multi_checkboxes
- Traditional checkbox interface
Text Filter
<Cinder.Table.table resource={MyApp.Article} actor={@current_user}>
<!-- Basic text filter -->
<:col :let={article} field="title" filter>{article.title}</:col>
<!-- Text filter with custom placeholder -->
<:col
:let={article}
field="content"
filter={:text}
filter_options={[placeholder: "Search article content..."]}
>
{String.slice(article.content, 0, 100)}...
</:col>
<!-- Case-sensitive text filter -->
<:col
:let={article}
field="author_name"
filter={:text}
filter_options={[
placeholder: "Author name...",
case_sensitive: true
]}
>
{article.author_name}
</:col>
</Cinder.Table.table>
Select Filter
<Cinder.Table.table resource={MyApp.Order} actor={@current_user}>
<!-- Basic select filter (auto-detects enum options) -->
<:col :let={order} field="status" filter>{String.capitalize(order.status)}</:col>
<!-- Select filter with custom options -->
<:col
:let={order}
field="priority"
filter={:select}
filter_options={[
options: [
{"Low Priority", "low"},
{"Normal Priority", "normal"},
{"High Priority", "high"},
{"Urgent", "urgent"}
],
prompt: "Any Priority"
]}
>
<span class={[
"px-2 py-1 text-xs font-semibold rounded-full",
order.priority == "urgent" && "bg-red-100 text-red-800",
order.priority == "high" && "bg-orange-100 text-orange-800",
order.priority == "normal" && "bg-blue-100 text-blue-800",
order.priority == "low" && "bg-gray-100 text-gray-800"
]}>
{String.capitalize(order.priority)}
</span>
</:col>
<!-- Select with boolean options -->
<:col
:let={order}
field="is_paid"
filter={:select}
filter_options={[
options: [{"Paid", true}, {"Unpaid", false}],
prompt: "Payment Status"
]}
>
{if order.is_paid, do: "Paid", else: "Unpaid"}
</:col>
</Cinder.Table.table>
Multi-Select Filter
Basic Multi-Select (ANY Logic)
By default, multi-select filters use "ANY" logic - records are shown if they contain at least one of the selected values:
<Cinder.Table.table resource={MyApp.Book} actor={@current_user}>
<!-- Multi-select for tags with default ANY logic -->
<:col
field="tags"
filter={:multi_select}
filter_options={[
options: [
{"Fiction", "fiction"},
{"Non-Fiction", "non_fiction"},
{"Science Fiction", "sci_fi"},
{"Romance", "romance"},
{"Mystery", "mystery"},
{"Biography", "biography"}
]
]}
>
{Enum.join(book.tags, ", ")}
</:col>
</Cinder.Table.table>
Multi-Select with ALL Logic
Use match_mode: :all
to show only records that contain ALL selected values:
<Cinder.Table.table resource={MyApp.Book} actor={@current_user}>
<!-- Multi-select requiring ALL selected tags -->
<:col
field="tags"
filter={:multi_select}
filter_options={[
options: [
{"Fiction", "fiction"},
{"Bestseller", "bestseller"},
{"Award Winner", "award_winner"},
{"New Release", "new_release"}
],
match_mode: :all # Records must contain ALL selected tags
]}
>
<div class="flex flex-wrap gap-1">
{for tag <- book.tags do}
<span class="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
{String.capitalize(String.replace(tag, "_", " "))}
</span>
{/for}
</div>
</:col>
<!-- Multi-select for categories with ANY logic (explicit) -->
<:col
:let={book}
field="categories"
filter={:multi_select}
filter_options={[
options: [
{"Bestseller", "bestseller"},
{"New Release", "new_release"},
{"Award Winner", "award_winner"},
{"Staff Pick", "staff_pick"}
],
match_mode: :any # Records with ANY selected category (default)
]}
>
<div class="flex flex-wrap gap-1">
{for category <- book.categories do}
<span class="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
{String.capitalize(String.replace(category, "_", " "))}
</span>
{/for}
</div>
</:col>
</Cinder.Table.table>
Multi-Checkboxes with Match Mode
The multi_checkboxes
filter also supports the same match_mode
options:
<Cinder.Table.table resource={MyApp.Book} actor={@current_user}>
<!-- Multi-checkboxes with ANY logic (default) -->
<:col
field="genres"
filter={:multi_checkboxes}
filter_options={[
options: [
{"Science Fiction", "sci_fi"},
{"Fantasy", "fantasy"},
{"Mystery", "mystery"},
{"Romance", "romance"}
],
match_mode: :any # Books with ANY selected genre
]}
>
{Enum.join(book.genres, ", ")}
</:col>
<!-- Multi-checkboxes with ALL logic -->
<:col
field="awards"
filter={:multi_checkboxes}
filter_options={[
options: [
{"Hugo Award", "hugo"},
{"Nebula Award", "nebula"},
{"World Fantasy Award", "wfa"}
],
match_mode: :all # Books with ALL selected awards
]}
>
<div class="flex flex-wrap gap-1">
{for award <- book.awards do}
<span class="px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded-full">
{award}
</span>
{/for}
</div>
</:col>
</Cinder.Table.table>
Match Mode Comparison
Both multi_select
and multi_checkboxes
support the same match mode options:
match_mode: :any
(default): Shows records containing at least one of the selected values- Example: Selecting "Fiction" and "Romance" shows books tagged with either "Fiction" OR "Romance" (or both)
match_mode: :all
: Shows records containing all of the selected values- Example: Selecting "Fiction" and "Bestseller" shows only books tagged with both "Fiction" AND "Bestseller"
This is particularly useful for:
- ANY mode: Finding books in multiple genres or categories
- ALL mode: Finding books that meet multiple criteria (e.g., "Fiction" AND "Award Winner")
Boolean Filter
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<!-- Basic boolean filter -->
<:col :let={user} field="is_active" filter>
{if user.is_active, do: "Active", else: "Inactive"}
</:col>
<!-- Boolean filter with custom labels -->
<:col
:let={user}
field="email_verified"
filter={:boolean}
filter_options={[
labels: %{
all: "Any Verification Status",
true: "Email Verified",
false: "Email Not Verified"
}
]}
>
<span class={[
"px-2 py-1 text-xs font-semibold rounded-full",
user.email_verified && "bg-green-100 text-green-800",
!user.email_verified && "bg-red-100 text-red-800"
]}>
{if user.email_verified, do: "Verified", else: "Not Verified"}
</span>
</:col>
<!-- Boolean filter for subscription -->
<:col
:let={user}
field="has_subscription"
filter={:boolean}
filter_options={[
labels: %{
all: "All Users",
true: "Subscribers",
false: "Free Users"
}
]}
>
{if user.has_subscription, do: "Subscriber", else: "Free User"}
</:col>
</Cinder.Table.table>
Date Range Filter
<Cinder.Table.table resource={MyApp.Event} actor={@current_user}>
<!-- Basic date range filter -->
<:col :let={event} field="created_at" filter={:date_range}>
{Calendar.strftime(event.created_at, "%B %d, %Y")}
</:col>
<!-- Date range with custom format -->
<:col
:let={event}
field="event_date"
filter={:date_range}
filter_options={[
format: "YYYY-MM-DD",
placeholder_from: "Start date",
placeholder_to: "End date"
]}
>
{Calendar.strftime(event.event_date, "%Y-%m-%d")}
</:col>
<!-- DateTime range filter -->
<:col
:let={event}
field="updated_at"
filter={:date_range}
filter_options={[
include_time: true
]}
>
{Calendar.strftime(event.updated_at, "%B %d, %Y at %I:%M %p")}
</:col>
</Cinder.Table.table>
Number Range Filter
<Cinder.Table.table resource={MyApp.Property} actor={@current_user}>
<!-- Basic number range -->
<:col :let={property} field="price" filter={:number_range}>
${Number.Currency.number_to_currency(property.price)}
</:col>
<!-- Number range with min/max limits -->
<:col
:let={property}
field="square_feet"
filter={:number_range}
filter_options={[
min: 500,
max: 10000,
step: 100,
placeholder_min: "Min sq ft",
placeholder_max: "Max sq ft"
]}
>
{Number.Delimit.number_to_delimited(property.square_feet)} sq ft
</:col>
<!-- Decimal number range -->
<:col
:let={property}
field="rating"
filter={:number_range}
filter_options={[
min: 0.0,
max: 5.0,
step: 0.1
]}
>
<div class="flex items-center">
{for i <- 1..5 do}
<svg class={[
"w-4 h-4",
i <= property.rating && "text-yellow-400",
i > property.rating && "text-gray-300"
]} fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
{/for}
<span class="ml-1 text-sm text-gray-600">{property.rating}</span>
</div>
</:col>
</Cinder.Table.table>
Sorting
Basic Sorting
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<!-- Sortable columns -->
<:col :let={user} field="name" sort>{user.name}</:col>
<:col :let={user} field="email" sort>{user.email}</:col>
<:col :let={user} field="created_at" sort>{user.created_at}</:col>
<!-- Non-sortable column -->
<:col :let={user} field="bio">{user.bio}</:col>
</Cinder.Table.table>
Combined Filter and Sort
<Cinder.Table.table resource={MyApp.Product} actor={@current_user}>
<:col :let={product} field="name" filter sort>{product.name}</:col>
<:col :let={product} field="price" filter={:number_range} sort>${product.price}</:col>
<:col :let={product} field="category" filter={:select} sort>{product.category}</:col>
<:col :let={product} field="created_at" filter={:date_range} sort>{product.created_at}</:col>
</Cinder.Table.table>
Theming
Built-in Themes
<!-- Default theme -->
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
theme="default"
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
</Cinder.Table.table>
<!-- Modern theme -->
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
theme="modern"
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
</Cinder.Table.table>
<!-- Minimal theme -->
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
theme="minimal"
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
</Cinder.Table.table>
Custom Theme - Complete Example
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
theme={%{
# Container styling
container_class: "bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-200",
# Table structure
table_class: "w-full border-collapse",
thead_class: "bg-gradient-to-r from-blue-600 to-blue-700",
tbody_class: "divide-y divide-gray-100",
# Header styling
th_class: "px-6 py-4 text-left text-sm font-bold text-white uppercase tracking-wider",
th_sortable_class: "px-6 py-4 text-left text-sm font-bold text-white uppercase tracking-wider cursor-pointer hover:bg-blue-800 transition-colors",
# Cell styling
td_class: "px-6 py-4 whitespace-nowrap text-sm text-gray-900",
tr_class: "hover:bg-gray-50 transition-colors",
# Filter styling
filter_container_class: "bg-blue-50 border-b border-blue-200 p-6",
filter_row_class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",
filter_col_class: "flex flex-col space-y-2",
filter_label_class: "text-sm font-semibold text-blue-900",
filter_text_input_class: "w-full px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
filter_select_input_class: "w-full px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
filter_clear_button_class: "text-sm text-blue-600 hover:text-blue-800 font-medium",
# Pagination styling
pagination_wrapper_class: "flex items-center justify-between px-6 py-4 bg-gray-50 border-t border-gray-200",
pagination_info_class: "text-sm text-gray-700",
pagination_nav_class: "flex space-x-2",
pagination_button_class: "px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50",
pagination_button_active_class: "px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-md",
pagination_button_disabled_class: "px-3 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-md cursor-not-allowed",
# Loading and empty states
loading_class: "text-center py-12 text-gray-500",
empty_class: "text-center py-12 text-gray-500",
# Sort indicators
sort_asc_class: "inline-block w-4 h-4 ml-1 text-white",
sort_desc_class: "inline-block w-4 h-4 ml-1 text-white"
}}
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter sort>{user.email}</:col>
</Cinder.Table.table>
URL State Management
Complete LiveView Setup
defmodule MyAppWeb.UsersLive do
use MyAppWeb, :live_view
use Cinder.Table.UrlSync
def mount(_params, _session, socket) do
current_user = get_current_user(socket)
{:ok, assign(socket, :current_user, current_user)}
end
def handle_params(params, uri, socket) do
socket = Cinder.Table.UrlSync.handle_params(params, uri, socket)
{:noreply, socket}
end
def render(assigns) do
~H"""
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Users</h1>
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
url_state={@url_state}
id="users-table"
page_size={25}
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
<:col :let={user} field="department.name" filter sort>{user.department.name}</:col>
<:col :let={user} field="is_active" filter={:boolean}>
{if user.is_active, do: "Active", else: "Inactive"}
</:col>
</Cinder.Table.table>
</div>
"""
end
end
URL State Management with Query Parameter
You can also use pre-configured queries with URL sync:
defmodule MyAppWeb.ActiveUsersLive do
use MyAppWeb, :live_view
use Cinder.Table.UrlSync
def mount(_params, _session, socket) do
current_user = get_current_user(socket)
{:ok, assign(socket, :current_user, current_user)}
end
def handle_params(params, uri, socket) do
socket = Cinder.Table.UrlSync.handle_params(params, uri, socket)
{:noreply, socket}
end
def render(assigns) do
~H"""
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Active Users</h1>
<Cinder.Table.table
query={MyApp.User |> Ash.Query.filter(active: true)}
actor={@current_user}
url_state={@url_state}
id="active-users-table"
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
<:col :let={user} field="last_login" sort>{user.last_login}</:col>
</Cinder.Table.table>
</div>
"""
end
end
URL Examples
With URL sync enabled, your table state is preserved in the URL:
# Basic filtering
/users?name=john&department.name=engineering
# With date range
/users?name=smith&created_at_from=2024-01-01&created_at_to=2024-12-31
# With pagination and sorting
/users?email=gmail&page=3&sort=-created_at
# Complex state
/users?name=admin&department.name=IT&is_active=true&page=2&sort=name,-created_at
Relationship Fields
Basic Relationships
<Cinder.Table.table resource={MyApp.Album} actor={@current_user}>
<:col :let={album} field="title" filter sort>{album.title}</:col>
<:col :let={album} field="artist.name" filter sort>{album.artist.name}</:col>
<:col :let={album} field="artist.country" filter>{album.artist.country}</:col>
<:col :let={album} field="record_label.name" filter>{album.record_label.name}</:col>
</Cinder.Table.table>
Deep Relationships
<Cinder.Table.table resource={MyApp.Employee} actor={@current_user}>
<:col :let={employee} field="name" filter sort>{employee.name}</:col>
<:col :let={employee} field="department.name" filter sort>{employee.department.name}</:col>
<:col :let={employee} field="department.manager.name" filter>{employee.department.manager.name}</:col>
<:col :let={employee} field="office.building.address" filter>{employee.office.building.address}</:col>
</Cinder.Table.table>
Relationship Filters with Custom Options
<Cinder.Table.table resource={MyApp.Order} actor={@current_user}>
<:col :let={order} field="number" filter sort>#{order.number}</:col>
<:col :let={order} field="customer.name" filter sort>{order.customer.name}</:col>
<!-- Select filter on relationship enum -->
<:col
:let={order}
field="customer.tier"
filter={:select}
filter_options={[
options: [{"Bronze", "bronze"}, {"Silver", "silver"}, {"Gold", "gold"}],
prompt: "Any Tier"
]}
>
<span class={[
"px-2 py-1 text-xs font-semibold rounded-full",
order.customer.tier == "gold" && "bg-yellow-100 text-yellow-800",
order.customer.tier == "silver" && "bg-gray-100 text-gray-800",
order.customer.tier == "bronze" && "bg-orange-100 text-orange-800"
]}>
{String.capitalize(order.customer.tier)}
</span>
</:col>
<!-- Date range on relationship -->
<:col
:let={order}
field="customer.created_at"
filter={:date_range}
sort
>
{Calendar.strftime(order.customer.created_at, "%B %Y")}
</:col>
</Cinder.Table.table>
Embedded Resources
Cinder provides full support for embedded resources using double underscore notation (__
). Embedded fields are automatically detected and typed, including automatic enum detection for select filters.
Basic Embedded Fields
<Cinder.Table.table resource={MyApp.Album} actor={@current_user}>
<:col :let={album} field="title" filter sort>{album.title}</:col>
<!-- Embedded resource fields use __ notation -->
<:col :let={album} field="publisher__name" filter>{album.publisher.name}</:col>
<:col :let={album} field="publisher__country" filter>{album.publisher.country}</:col>
<:col :let={album} field="metadata__genre" filter>{album.metadata.genre}</:col>
</Cinder.Table.table>
Nested Embedded Fields
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<!-- Deep nested embedded fields -->
<:col :let={user} field="settings__notifications__email" filter>
{if user.settings.notifications.email, do: "✓", else: "✗"}
</:col>
<:col :let={user} field="profile__address__country" filter>{user.profile.address.country}</:col>
<:col :let={user} field="preferences__theme__color" filter>{user.preferences.theme.color}</:col>
</Cinder.Table.table>
Automatic Enum Detection
When embedded fields use Ash.Type.Enum
, Cinder automatically detects them and creates select filters:
# In your embedded resource
defmodule MyApp.Publisher do
use Ash.Resource, data_layer: :embedded
attributes do
attribute :name, :string
attribute :country, MyApp.Country # Enum type
end
end
defmodule MyApp.Country do
use Ash.Type.Enum, values: ["Australia", "India", "Japan", "England", "Canada"]
end
<!-- This automatically becomes a select filter with enum values -->
<Cinder.Table.table resource={MyApp.Album} actor={@current_user}>
<:col :let={album} field="title" filter sort>{album.title}</:col>
<:col :let={album} field="publisher__country" filter>{album.publisher.country}</:col>
</Cinder.Table.table>
Mixed Relationships and Embedded Fields
You can combine relationship navigation (dot notation) with embedded fields (double underscore):
<Cinder.Table.table resource={MyApp.Order} actor={@current_user}>
<:col :let={order} field="number" filter sort>#{order.number}</:col>
<!-- Relationship + embedded field -->
<:col :let={order} field="customer.profile__country" filter>
{order.customer.profile.country}
</:col>
<:col :let={order} field="shipping.address__postal_code" filter>
{order.shipping.address.postal_code}
</:col>
</Cinder.Table.table>
Advanced Examples
Progress Bars and Indicators
<Cinder.Table.table resource={MyApp.Project} actor={@current_user}>
<:col :let={project} field="name" filter sort>
{project.name}
</:col>
<!-- Progress bar -->
<:col :let={project} field="completion_percentage" filter={:number_range} sort>
<div class="flex items-center space-x-2">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={"width: #{project.completion_percentage}%"}
>
</div>
</div>
<span class="text-sm text-gray-600 min-w-0">
{project.completion_percentage}%
</span>
</div>
</:col>
<!-- Health indicator -->
<:col :let={project} field="health_status" filter={:select}>
<div class="flex items-center space-x-2">
<div class={[
"w-3 h-3 rounded-full",
project.health_status == "healthy" && "bg-green-400",
project.health_status == "warning" && "bg-yellow-400",
project.health_status == "critical" && "bg-red-400"
]}>
</div>
<span class="text-sm capitalize">
{project.health_status}
</span>
</div>
</:col>
</Cinder.Table.table>
Action Columns
Action columns allow you to add buttons, links, and other interactive elements to your tables without requiring a database field. Simply omit the field
attribute to create an action column.
Basic Action Column
<Cinder.Table.table resource={MyApp.User} actor={@current_user}>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
<:col :let={user} field="role" filter={:select}>{user.role}</:col>
<!-- Action column - no field required -->
<:col :let={user} label="Actions" class="text-right">
<.link patch={~p"/users/#{user.id}"} class="text-blue-600 hover:text-blue-800 mr-3">
Edit
</.link>
<.link
href={~p"/users/#{user.id}"}
method="delete"
class="text-red-600 hover:text-red-800"
data-confirm="Are you sure?"
>
Delete
</.link>
</:col>
</Cinder.Table.table>
Note: Action columns cannot have filter
or sort
attributes since they don't correspond to database fields. If you try to add these attributes without a field
, you'll get a validation error.
Table Refresh
Refresh table data after CRUD operations while maintaining filters, sorting, and pagination state. This is essential when performing operations that modify the data displayed in your tables.
Basic Refresh
After deleting, updating, or creating records, refresh the specific table:
defmodule MyAppWeb.UsersLive do
use MyAppWeb, :live_view
import Cinder.Table.Refresh # <--
def render(assigns) do
~H"""
<Cinder.Table.table id="users-table" resource={MyApp.User} actor={@current_user}>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
<:col :let={user} field="active" filter>{if user.active, do: "Active", else: "Inactive"}</:col>
<!-- Action column with refresh functionality -->
<:col :let={user} label="Actions">
<button phx-click="delete_user" phx-value-id={user.id}>
Delete
</button>
</:col>
</Cinder.Table.table>
"""
end
def handle_event("delete_user", %{"id" => id}, socket) do
MyApp.User
|> Ash.get!(id, actor: socket.assigns.current_user)
|> Ash.destroy!(actor: socket.assigns.current_user)
# Refresh the specific table - maintains filters, sorting, pagination
{:noreply, refresh_table(socket, "users-table")}
end
end
Multiple Table Refresh
When operations affect multiple tables, refresh them all by providing a list of table IDs:
refresh_tables(socket, ["users-table", "audit-logs-table"])
Advanced Configuration
Complete Configuration Example
Every available option demonstrated:
<Cinder.Table.table
# Required attributes
resource={MyApp.User}
actor={@current_user}
# Component configuration
id="advanced-users-table"
class="my-custom-table-wrapper border rounded-lg"
# Data configuration
page_size={50}
query_opts={[
load: [:profile, :department, :manager],
select: [:id, :name, :email, :created_at, :is_active]
]}
# URL state management
url_state={@url_state}
# UI configuration
theme="modern"
show_filters={true}
show_pagination={true}
# Custom messages
loading_message="Loading users, please wait..."
empty_message="No users found matching your search criteria"
# Callbacks (if needed for custom behavior)
on_state_change={&handle_table_state_change/1}
>
<!-- Text column with all options using :let -->
<:col
:let={user}
field="name"
label="Full Name"
filter={:text}
filter_options={[
placeholder: "Search by name...",
case_sensitive: false
]}
sort={true}
class="w-1/4 font-semibold"
>
<div class="flex items-center space-x-2">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<span class="text-white text-xs font-semibold">
{String.first(user.name)}
</span>
</div>
<span>{user.name}</span>
</div>
</:col>
<!-- Email with mailto link using :let -->
<:col
:let={user}
field="email"
filter={:text}
filter_options={[placeholder: "Email address..."]}
sort
class="w-1/4"
>
<a href={"mailto:#{user.email}"} class="text-blue-600 hover:underline">
{user.email}
</a>
</:col>
<!-- Relationship with select filter -->
<:col
:let={user}
field="department.name"
filter={:select}
filter_options={[
options: [
{"Engineering", "engineering"},
{"Marketing", "marketing"},
{"Sales", "sales"},
{"Support", "support"}
],
prompt: "All Departments"
]}
sort
>
{user.department.name}
</:col>
<!-- Date with range filter using :let -->
<:col
:let={user}
field="created_at"
label="Member Since"
filter={:date_range}
filter_options={[
format: "MM/DD/YYYY",
placeholder_from: "Start date",
placeholder_to: "End date"
]}
sort
>
<div class="text-sm">
<div class="font-medium">
{Calendar.strftime(user.created_at, "%B %d, %Y")}
</div>
<div class="text-gray-500">
{Calendar.strftime(user.created_at, "%H:%M")}
</div>
</div>
</:col>
<!-- Boolean with custom labels using :let -->
<:col
:let={user}
field="is_active"
filter={:boolean}
filter_options={[
labels: %{
all: "All Users",
true: "Active Users",
false: "Inactive Users"
}
]}
>
<span class={[
"px-2 py-1 text-xs font-semibold rounded-full",
user.is_active && "bg-green-100 text-green-800",
!user.is_active && "bg-red-100 text-red-800"
]}>
{if user.is_active, do: "Active", else: "Inactive"}
</span>
</:col>
<!-- Actions column using :let -->
<:col :let={user} field="actions" class="text-right w-32">
<div class="flex gap-1 justify-end">
<.link
navigate={~p"/users/#{user.id}"}
class="p-1 text-blue-600 hover:text-blue-800"
title="View user"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path>
</svg>
</.link>
<.link
navigate={~p"/users/#{user.id}/edit"}
class="p-1 text-green-600 hover:text-green-800"
title="Edit user"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path>
</svg>
</.link>
</div>
</:col>
</Cinder.Table.table>
Performance Optimization
Efficient Data Loading
<Cinder.Table.table
resource={MyApp.Order}
actor={@current_user}
# Preload only what you need
query_opts={[
load: [
:customer,
:order_items,
items: [:product]
],
# Select only required fields for better performance
select: [
:id, :number, :status, :total_amount, :created_at,
customer: [:name, :email],
order_items: [:quantity, product: [:name, :price]]
]
]}
# Optimize page size for your data
page_size={25}
>
<:col field="number" filter sort>Order #</:col>
<:col field="customer.name" filter sort>Customer</:col>
<:col field="total_amount" filter={:number_range} sort>Total</:col>
</Cinder.Table.table>
Strategic Filtering
Only enable filters where users actually need them:
<Cinder.Table.table resource={MyApp.Product} actor={@current_user}>
<!-- Internal ID - no filter needed -->
<:col :let={product} field="id" sort>{product.id}</:col>
<!-- User-searchable fields -->
<:col :let={product} field="name" filter sort>{product.name}</:col>
<:col :let={product} field="category" filter sort>{product.category}</:col>
<:col :let={product} field="price" filter={:number_range} sort>${product.price}</:col>
<!-- Display-only field -->
<:col :let={product} field="sku">{product.sku}</:col>
</Cinder.Table.table>
Custom Query Optimization
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
query_opts={[
# Efficient loading of relationships
load: [:department, :profile],
# Limit fields for better performance
select: [:id, :name, :email, :created_at, :is_active],
# Custom filters for complex queries
filter: [is_active: true],
# Sorting optimization
sort: [:name]
]}
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
<:col :let={user} field="department.name" filter sort>{user.department.name}</:col>
</Cinder.Table.table>
This comprehensive guide demonstrates every available feature and option in Cinder. The combination of intelligent defaults and extensive customization options makes Cinder suitable for simple data display as well as complex, feature-rich table implementations.