ExLiveTable

View Source

A 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:

  1. Adding custom CSS classes to columns
  2. 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:

  1. Ensure proper database indexing on sorted and filtered columns
  2. Consider using limit/offset instead of fetching all records
  3. Use row_item for complex transformations to avoid doing them in the template

Troubleshooting

Common issues:

  1. Pagination not working: Ensure you're using Scrivener correctly and passing the params to the table component
  2. Sorting not affecting results: Check that the field names match your database columns
  3. Search not working: Verify that the iSearch event is properly handled and the filter parameters are correctly passed