LiveTable

repository·master·Indexed 42 days ago

https://github.com/gurujada/live_table

A Phoenix LiveView component library for building interactive, real-time data tables. Features include advanced filtering (Boolean, Range, Select, Transformers), multi-column sorting, pagination, and CSV/PDF exports via Oban and Typst. Supports both simple Ecto schema-backed tables and complex tables using custom data providers with joins and aggregations.

Tokens
54.6K
Snippets
170
Records
210
Agent score
52%

What's inside LiveTable

  1. What is LiveTable?

    master
    LiveTable is a Phoenix LiveView component library designed for building interactive, high-performance data tables. It is optimized for large datasets (tested with 1M+ records) and provides built-in support for filtering, sorting, pagination, and professional exports (CSV/PDF). It is ideal for admin dashboards, e-commerce catalogs, and analytics platforms.
  2. Overview of LiveTable features

    master

    LiveTable is a Phoenix LiveView component library designed for building interactive data tables. Key capabilities include:

    • Advanced Filtering: Text search, range filters, select dropdowns, boolean toggles, and transformers.
    • Smart Sorting: Multi-column sorting with shift-click support.
    • Flexible Pagination: Supports both standard pagination and infinite scroll.
    • Export Capabilities: CSV and PDF exports with background processing.
    • Real-time Updates: Built specifically for Phoenix LiveView for instant feedback.
    • Multiple View Modes: Supports both Table and Card layouts with custom components.
    • Complex Queries: Supports joins, aggregations, and computed fields.
  3. Overview of LiveTable

    master

    LiveTable is a Phoenix LiveView component library designed for building dynamic, interactive data tables with real-time updates. It is suitable for admin panels, dashboards, e-commerce catalogs, and analytics platforms.

    Core Architecture

    LiveTable uses a three-tier architecture:

    1. Data Layer: Supports Simple Tables (using schema: YourApp.Model for single-table queries) and Complex Tables (using :data_provider for custom queries with joins).
    2. Configuration Layer: Uses fields/0 to define data display, filters/0 for interactive filtering, and table_options/0 to control behavior.
    3. Presentation Layer: Offers Table Mode (rows/columns), Card Mode (rich card displays), and support for Custom Components to override any part of the UI.
  4. Choose between Simple and Complex Table patterns

    master

    LiveTable uses two distinct implementation patterns. Choosing the wrong one will cause field key mismatches or runtime errors.

    Pattern A: Simple Tables (Single Schema)

    Use this when querying a single Ecto schema directly.

    • Implementation: Use use LiveTable.LiveResource, schema: YourApp.Schema.
    • Field Keys: Must match the Ecto schema field names exactly.
    • Data Provider: No custom data_provider assignment is needed in mount/3.

    Pattern B: Complex Tables (Custom Queries)

    Use this for joins, computed fields, or complex logic involving multiple tables.

    • Implementation: Use use LiveTable.LiveResource (do not pass a schema: parameter).
    • Field Keys: Must match the keys returned in your custom SQL/Ecto select clause exactly.
    • Data Provider: You must assign a custom data provider in mount/3 using the format {Module, Function, Arguments}.
    # Pattern A: Simple
    use LiveTable.LiveResource, schema: YourApp.Product
    
    # Pattern B: Complex
    use LiveTable.LiveResource
    def mount(_params, _session, socket) do
      socket = assign(socket, :data_provider, {YourApp.Orders, :list_with_details, []})
      {:ok, socket}
    end
  5. How filters work in LiveTable

    master

    Filters are defined in the filters/0 function of your LiveTable module as a keyword list. Each entry in the list maps a filter name to a specific filter struct. LiveTable supports three primary filter types:

    1. Boolean: Renders as checkboxes for true/false conditions.
    2. Range: Renders as numeric sliders.
    3. Select: Renders as dropdowns with either static or dynamic options.

    Filters use a consistent constructor pattern: FilterType.new(field, key, options)

    • field: The database field to filter on. Use an atom for simple fields (e.g., :active) or a tuple for joined fields (e.g., {:suppliers, :name}).
    • key: The URL parameter key (string) used for persistence and routing.
    • options: A map of configuration settings specific to the filter type.
    def filters do
      [
        active: Boolean.new(:active, "active", %{
          label: "Active Only",
          condition: dynamic([p], p.active == true)
        }),
        
        price_range: Range.new(:price, "price_range", %{
          label: "Price Range",
          min: 0,
          max: 1000
        }),
        
        category: Select.new({:category, :name}, "category", %{
          label: "Category",
          options: [
            %{label: "Electronics", value: ["electronics"]},
            %{label: "Books", value: ["books"]}
          ]
        })
      ]
    end
  6. Styling and UI customization

    master

    LiveTable is built on SutraUI and uses Tailwind CSS.

    • DaisyUI Integration: If your application uses DaisyUI, LiveTable components will automatically adopt your active Daisy theme.
    • Dark Mode: Supports Tailwind dark: classes and DaisyUI theme tokens.
    • Complete UI Overrides: If you need to move away from the default look, you can provide custom components for the following areas:
      • custom_header
      • custom_controls
      • custom_content
      • custom_footer
  7. How LiveTable architecture works

    master

    LiveTable uses a three-tier architecture to separate data concerns from presentation:

    1. Data Layer: Handles data retrieval via Ecto schema integration, custom queries, or flexible Data Providers (functions that return queryable data).
    2. Configuration Layer: Defines the table's behavior through Fields (what to display), Filters (how to narrow data), and Options (pagination, sorting, and display modes).
    3. Presentation Layer: Manages the UI using customizable components, templates, and Tailwind CSS styling.

    This separation allows you to switch between simple CRUD tables and complex analytics dashboards by simply changing the configuration and data provider.

  8. Configure Display Modes: Table vs Card

    master

    Choose between two primary layout modes:

    • :table: A traditional row-and-column layout.
    • :card: A grid layout using custom card components.

    When using :card mode, you must provide a card_component function.

    # Table Mode
    mode: :table
    
    # Card Mode
    mode: :card,
    card_component: &product_card/1
  9. Customize cell display with renderers or components

    master

    You can transform how data appears in cells using either a renderer or a component.

    Using renderer

    renderer is a function that receives the cell value. It can optionally receive the entire record as a second argument.

    • function/1: Receives only the cell value.
    • function/2: Receives the cell value AND the entire record. Use this when you need context from other fields in the same row (e.g., for conditional logic or row actions).

    Using component

    component is a function component that receives an assigns map containing :value and :record keys.

    Comparison:

    Featurerenderercomponent
    ArgumentsValue (and optionally record)Assigns map (@value, @record)
    Usage&my_func/1 or &my_func/2&my_comp/1
    # Using function/1 - only gets the cell value
    status: %{
      label: "Status", 
      renderer: &format_status/1
    }
    
    defp format_status(status) do
      assigns = %{status: status}
      ~H"""
      <span class={status_class(@status)}>
        <%= String.capitalize(@status) %>
      </\
      </span>
      """
    end
    
    # Using function/2 - gets cell value AND entire record
    priority: %{
      label: "Priority",
      renderer: &format_priority_with_context/2
    }
    
    defp format_priority_with_context(priority, record) do
      assigns = %{priority: priority, record: record}
      ~H"""
      <div class="flex items-center gap-2">
        <span class={priority_class(@priority)}>
          <%= String.upcase(@priority) %>
        </\
        </span>
        <%= if @record.is_urgent do %>
          <span class="text-red-500 text-xs">URGENT</\
        <% end %>
      </\
      </\
      """
    end
  10. Define complex Ecto queries for LiveTable

    master

    When providing data to LiveTable, your Ecto query should use named bindings (as: :name) for all joins. This allows LiveTable filters to reference joined data using the dynamic macro.

    For calculated fields (like avg_daily_usage or days_of_supply), use fragment/1 within the select block to perform SQL-level aggregations. This ensures that sorting and filtering on these calculated values remain performant at the database level.

    defmodule YourApp.Inventory do
      import Ecto.Query
    
      def comprehensive_report do
        from p in YourApp.Product,
          join: s in YourApp.Supplier, on: p.supplier_id == s.id, as: :suppliers,
          join: w in YourApp.Warehouse, on: p.warehouse_id == w.id, as: :warehouses,
          left_join: im in YourApp.InventoryMovement, 
            on: im.product_id == p.id and im.inserted_at >= ago(30, "day"),
          group_by: [p.id, s.id, w.id],
          select: %{
            product_name: p.name,
            supplier_name: s.name,
            avg_daily_usage: fragment("CASE WHEN COUNT(?) > 0 THEN ... END", im.id, im, im)
          }
      end
    end
  11. Patterns for Complex Queries in LiveTable

    master

    When building advanced reporting features, follow these architectural patterns:

    1. Data Provider Assignment

    Assign the :data_provider in the mount/3 callback before LiveTable processes URL parameters. Use the tuple format {Module, :function, [args]} to allow for dynamic parameter injection (e.g., passing a cohort year from the URL).

    2. Query Design

    • Aliases: Use the as: keyword in Ecto joins. This is critical for allowing filters to reference joined tables.
    • Calculations: Use fragment/2 for complex SQL logic (window functions, CASE statements, etc.) to keep the heavy lifting in the database.
    • Grouping: Always group_by all non-aggregated fields in your select statement.

    3. Field and Filter Mapping

    • Associations: Use assoc: {:alias_name, :field} in your fields/0 definition to map columns to joined data.
    • Field Keys: Ensure the keys in your select: block in the Ecto query match the keys defined in your fields/0 exactly.

    4. Custom Rendering

    Use renderer: &function/1 to create rich UI. The function receives the specific record for that row, allowing you to implement conditional logic (e.g., changing colors based on a score) or complex HTML structures.

  12. Define a Provider Contract with Ecto Queries

    master

    LiveTable manages query execution. Your provider functions must return an Ecto.Query and must not call Repo.all/1 internally.

    Key Requirements:

    1. Return Queries: Always return a query struct.
    2. Named Bindings: When performing joins that need to be searchable, sortable, or filterable, use named bindings (as: :name) so LiveTable can reference them.
    3. Field Matching: The keys defined in fields/0 must exactly match the keys selected in your query's select block.
    # Provider function returning a query with a named binding
    def list_product_rows do
      from p in Product,
        join: b in assoc(p, :brand),
        as: :brand,
        select: %{
          id: p.id,
          name: p.name,
          brand_name: b.name
        }
    end
    
    # Corresponding fields definition
    def fields do
      [
        name: %{label: "Product", searchable: true},
        brand_name: %{label: "Brand", searchable: true, sortable: true, assoc: {:brand, :name}}
      ]
    end