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
- Custom Content
- 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
<Cinder.Table.table resource={MyApp.Book} actor={@current_user}>
<!-- Multi-select for tags -->
<: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>
<!-- Multi-select for categories -->
<: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"}
]
]}
>
<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>
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(socket, params, uri)
{: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(socket, params, uri)
{: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>
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>
Image Thumbnails and Rich Content
<Cinder.Table.table resource={MyApp.Product} actor={@current_user}>
<!-- Product with thumbnail -->
<:col :let={product} field="name" filter sort class="w-1/3">
<div class="flex items-center space-x-3">
{if product.image_url do}
<img
src={product.image_url}
alt={product.name}
class="w-12 h-12 rounded-lg object-cover flex-shrink-0"
/>
{else}
<div class="w-12 h-12 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"></path>
</svg>
</div>
{/if}
<div class="min-w-0 flex-1">
<div class="text-sm font-medium text-gray-900 truncate">
{product.name}
</div>
<div class="text-sm text-gray-500 truncate">
SKU: {product.sku}
</div>
</div>
</div>
</:col>
<!-- Price with currency formatting -->
<:col :let={product} field="price" filter={:number_range} sort class="text-right">
<div class="text-right">
<div class="text-lg font-semibold text-gray-900">
{Money.to_string(product.price)}
</div>
{if product.sale_price do}
<div class="text-sm text-red-600 line-through">
{Money.to_string(product.sale_price)}
</div>
{/if}
</div>
</:col>
<!-- Stock status with inventory count -->
<:col :let={product} field="inventory_count" filter={:number_range} sort>
<div class="text-center">
<div class={[
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
product.inventory_count > 50 && "bg-green-100 text-green-800",
product.inventory_count > 10 && product.inventory_count <= 50 && "bg-yellow-100 text-yellow-800",
product.inventory_count <= 10 && "bg-red-100 text-red-800"
]}>
{product.inventory_count} in stock
</div>
</div>
</:col>
</Cinder.Table.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.