ExLiveTable
View SourceA comprehensive DataTable solution for Phoenix LiveView applications with built-in sorting, filtering, pagination, and responsive design.
Installation
The package can be installed by adding ex_live_table
to your list of dependencies in mix.exs
:
def deps do
[
{:ex_live_table, "~> 0.1.0"}
]
end
Features
- Responsive design with desktop and mobile views
- Sorting by columns
- Quick search filter
- Pagination with Scrivener integration
- CSV/Excel/PDF export functionality
- Customizable styling
Usage
<ExLiveTable.main id="users" rows={@users} params={@params}>
<:col :let={user} label="ID"><%= user.id %></:col>
<:col :let={user} label="Username"><%= user.username %></:col>
<:action :let={user}>
<button phx-click="edit" phx-value-id={user.id}>Edit</button>
</:action>
</ExLiveTable.main>
Documentation
The docs can be found at https://hexdocs.pm/ex_live_table.
LiveTable Usage Guide
LiveTable
is a comprehensive data table solution for Phoenix LiveView applications with built-in sorting, filtering, pagination, and responsive design.
Basic Example
Here's a simple example of using LiveTable in your LiveView application:
Live View Module
defmodule MyAppWeb.UserLive.Index do
use MyAppWeb, :live_view
alias MyApp.Accounts
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(params, _url, socket) do
params = params || %{}
page = Accounts.list_users(params)
{:noreply,
socket
|> assign(:params, params)
|> assign(:users, page)}
end
@impl true
def handle_event("iSearch", %{"isearch" => search_term}, socket) do
params = put_in(socket.assigns.params, ["filter", "isearch"], search_term)
{:noreply, push_patch(socket, to: ~p"/users?#{params}")}
end
@impl true
def handle_event("refresh_table", _, socket) do
{:noreply, push_patch(socket, to: ~p"/users?#{socket.assigns.params}")}
end
@impl true
def handle_event("export", %{"file_type" => file_type}, socket) do
# Export logic for CSV, Excel, or PDF
users = Accounts.list_all_users(socket.assigns.params)
case file_type do
"csv" -> generate_csv(users)
"xlsx" -> generate_xlsx(users)
"pdf" -> generate_pdf(users)
end
{:noreply, socket}
end
end
LiveView Template
<h1 class="text-2xl font-bold mb-4">Users</h1>
<ExLiveTable.main_table id="users" rows={@users} params={@params}>
<:col :let={user} label="ID"><%= user.id %></:col>
<:col :let={user} label="Name"><%= user.name %></:col>
<:col :let={user} label="Email"><%= user.email %></:col>
<:col :let={user} label="Role"><%= user.role %></:col>
<:col :let={user} label="Created">
<%= Calendar.strftime(user.inserted_at, "%Y-%m-%d") %>
</:col>
<:action :let={user}>
<div class="flex items-center space-x-2">
<.link navigate={~p"/users/#{user}/edit"} class="text-blue-600 hover:text-blue-800">
Edit
</.link>
<button phx-click="delete" phx-value-id={user.id} class="text-red-600 hover:text-red-800"
data-confirm="Are you sure you want to delete this user?">
Delete
</button>
</div>
</:action>
</ExLiveTable.main_table>
Context Module
defmodule MyApp.Accounts do
import Ecto.Query
alias MyApp.Repo
alias MyApp.Accounts.User
def list_users(params) do
User
|> ExLiveTable.handle_search(params)
|> ExLiveTable.handle_sorting(params)
|> Repo.paginate(params)
end
def list_all_users(params) do
User
|> ExLiveTable.handle_search(params)
|> ExLiveTable.handle_sorting(params)
|> Repo.all()
end
end
Advanced Options
Customizing Row Click Behavior
<ExLiveTable.main_table
id="users"
rows={@users}
params={@params}
row_click={fn user -> JS.navigate(~p"/users/#{user}") end}>
<!-- columns -->
</ExLiveTable.main_table>
Dynamic Row ID and Item Transformation
<ExLiveTable.main_table
id="users"
rows={@users}
params={@params}
row_id={fn user -> "user-#{user.id}" end}
row_item={fn user -> Map.put(user, :full_name, "#{user.first_name} #{user.last_name}") end}>
<:col :let={user} label="Full Name"><%= user.full_name %></:col>
<!-- more columns -->
</ExLiveTable.main_table>
Loading State
<ExLiveTable.main_table
id="users"
rows={@users}
params={@params}
data_loader={@loading}>
<!-- columns -->
</ExLiveTable.main_table>
Styling Options
The ExLiveTable comes with a default styling system based on Tailwind CSS. You can customize the appearance by:
- Adding custom CSS classes to columns
- Overriding the default styles in your app.css
Example of custom column classes:
<ExLiveTable.main_table id="users" rows={@users} params={@params}>
<:col :let={user} label="ID" class="font-mono"><%= user.id %></:col>
<:col :let={user} label="Status" label_class="text-center" class="text-center">
<span class={"badge #{status_color(user.status)}"}>
<%= user.status %>
</span>
</:col>
<!-- more columns -->
</ExLiveTable.main_table>
Handling Pagination with Scrivener
Make sure to add Scrivener to your dependencies:
defp deps do
[
{:scrivener_ecto, "~> 2.7"}
]
end
Configure Scrivener in your repo:
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
use Scrivener, page_size: 10
end
Use Scrivener in your context:
def list_users(params) do
page = params["page"] || "1"
page_size = params["page_size"] || "10"
params = Map.merge(params, %{
"page" => page,
"page_size" => page_size
})
User
|> ExLiveTable.handle_search(params)
|> ExLiveTable.handle_sorting(params)
|> Repo.paginate(params)
end
Export Functionality
To implement the export functionality, you'll need additional libraries depending on the export format:
- For CSV:
{:csv, "~> 2.4"}
- For Excel:
{:xlsx_creator, "~> 0.4.2"}
- For PDF:
{:pdf_generator, "~> 0.6.2"}
Example implementation:
def generate_csv(users) do
csv_content =
[["ID", "Name", "Email", "Role", "Created At"]] ++
Enum.map(users, fn user ->
[
user.id,
user.name,
user.email,
user.role,
Calendar.strftime(user.inserted_at, "%Y-%m-%d")
]
end)
|> CSV.encode()
|> Enum.to_list()
|> Enum.join()
# Return the CSV for download
%{
content: csv_content,
content_type: "text/csv",
filename: "users_export.csv"
}
end
Custom Search Implementation
For more advanced search needs, you can override the default search behavior:
def list_users(params) do
search_term = get_in(params, ["filter", "isearch"]) || ""
query = from u in User
query =
if search_term != "" do
search_term = "%#{search_term}%"
from u in query,
where:
ilike(u.name, ^search_term) or
ilike(u.email, ^search_term) or
ilike(u.role, ^search_term)
else
query
end
query
|> ExLiveTable.handle_sorting(params)
|> Repo.paginate(params)
end
Performance Considerations
For large datasets:
- Ensure proper database indexing on sorted and filtered columns
- Consider using limit/offset instead of fetching all records
- Use
row_item
for complex transformations to avoid doing them in the template
Troubleshooting
Common issues:
- Pagination not working: Ensure you're using Scrivener correctly and passing the
params
to the table component - Sorting not affecting results: Check that the field names match your database columns
- Search not working: Verify that the
iSearch
event is properly handled and the filter parameters are correctly passed