Simple Table Examples

View Source

This guide shows how to create basic LiveTable implementations for common single-schema use cases.

Basic Product Table

A simple product listing with minimal configuration:

# lib/your_app_web/live/product_live/index.ex
defmodule YourAppWeb.ProductLive.Index do
  use YourAppWeb, :live_view
  use LiveTable.LiveResource, schema: YourApp.Product

  def fields do
    [
      id: %{label: "ID", sortable: true},
      name: %{label: "Product Name", sortable: true, searchable: true},
      price: %{label: "Price", sortable: true},
      stock_quantity: %{label: "Stock", sortable: true},
      active: %{label: "Status", sortable: true, renderer: &render_status/1}
    ]
  end

  def filters do
    [
      active: Boolean.new(:active, "active", %{
        label: "Active Products Only",
        condition: dynamic([p], p.active == true)
      }),
      
      price_range: Range.new(:price, "price_range", %{
        type: :number,
        label: "Price Range",
        min: 0,
        max: 1000
      })
    ]
  end

  defp render_status(active) do
    assigns = %{active: active}
    ~H"""
    <span class={[
      "px-2 py-1 text-xs font-medium rounded-full",
      if(@active, do: "bg-green-100 text-green-700", else: "bg-red-100 text-red-700")
    ]}>
      <%= if @active, do: "Active", else: "Inactive" %>
    </span>
    """
  end
end
# lib/your_app_web/live/product_live/index.html.heex
<div class="p-6">
  <h1 class="text-2xl font-bold mb-6">Products</h1>
  
  <.live_table
    fields={fields()}
    filters={filters()}
    options={@options}
    streams={@streams}
  />
</div>

User Management Table

A user listing with role-based rendering:

# lib/your_app_web/live/user_live/index.ex
defmodule YourAppWeb.UserLive.Index do
  use YourAppWeb, :live_view
  use LiveTable.LiveResource, schema: YourApp.User

  def fields do
    [
      id: %{label: "ID", sortable: true},
      email: %{label: "Email", sortable: true, searchable: true},
      name: %{label: "Name", sortable: true, searchable: true},
      role: %{label: "Role", sortable: true, renderer: &render_role/1},
      inserted_at: %{label: "Joined", sortable: true, renderer: &render_date/1},
      last_sign_in_at: %{label: "Last Active", sortable: true, renderer: &render_last_active/1}
    ]
  end

  def filters do
    [
      active: Boolean.new(:active, "active", %{
        label: "Active Users Only",
        condition: dynamic([u], u.active == true)
      }),
      
      role: Select.new(:role, "role", %{
        label: "User Role",
        options: [
          %{label: "Admin", value: ["admin"]},
          %{label: "Manager", value: ["manager"]},
          %{label: "User", value: ["user"]}
        ]
      }),
      
      signup_date: Range.new(:inserted_at, "signup_range", %{
        type: :date,
        label: "Registration Date",
        min: ~D[2020-01-01],
        max: Date.utc_today()
      })
    ]
  end

  defp render_role(role) do
    assigns = %{role: role}
    ~H"""
    <span class={[
      "px-2 py-1 text-xs font-medium rounded-full uppercase",
      case @role do
        "admin" -> "bg-purple-100 text-purple-700"
        "manager" -> "bg-blue-100 text-blue-700"
        "user" -> "bg-gray-100 text-gray-700"
      end
    ]}>
      <%= @role %>
    </span>
    """
  end

  defp render_date(date) do
    assigns = %{date: date}
    ~H"""
    <span class="text-sm text-gray-600">
      <%= Calendar.strftime(@date, "%b %d, %Y") %>
    </span>
    """
  end

  defp render_last_active(nil) do
    assigns = %{}
    ~H"""
    <span class="text-sm text-gray-400">Never</span>
    """
  end

  defp render_last_active(datetime) do
    assigns = %{datetime: datetime}
    ~H"""
    <span class="text-sm text-gray-600">
      <%= Calendar.strftime(@datetime, "%b %d, %Y at %I:%M %p") %>
    </span>
    """
  end
end

Order History Table

An order listing with status tracking:

# lib/your_app_web/live/order_live/index.ex
defmodule YourAppWeb.OrderLive.Index do
  use YourAppWeb, :live_view
  use LiveTable.LiveResource, schema: YourApp.Order

  def fields do
    [
      id: %{label: "Order #", sortable: true},
      customer_email: %{label: "Customer", sortable: true, searchable: true},
      total_amount: %{label: "Total", sortable: true, renderer: &render_currency/1},
      status: %{label: "Status", sortable: true, renderer: &render_order_status/1},
      inserted_at: %{label: "Order Date", sortable: true, renderer: &render_date/1},
      actions: %{label: "Actions", sortable: false, renderer: &render_actions/2}
    ]
  end

  def filters do
    [
      status: Select.new(:status, "status", %{
        label: "Order Status",
        options: [
          %{label: "Pending", value: ["pending"]},
          %{label: "Processing", value: ["processing"]},
          %{label: "Shipped", value: ["shipped"]},
          %{label: "Delivered", value: ["delivered"]},
          %{label: "Cancelled", value: ["cancelled"]}
        ]
      }),
      
      order_total: Range.new(:total_amount, "total_range", %{
        type: :number,
        label: "Order Total",
        min: 0,
        max: 5000
      }),
      
      recent_orders: Boolean.new(:inserted_at, "recent", %{
        label: "Last 30 Days",
        condition: dynamic([o], o.inserted_at >= ago(30, "day"))
      })
    ]
  end

  def table_options do
    %{
      pagination: %{
        enabled: true,
        sizes: [10, 25, 50],
        default_size: 25
      },
      sorting: %{
        default_sort: [inserted_at: :desc]
      },
      exports: %{
        enabled: true,
        formats: [:csv, :pdf]
      }
    }
  end

  defp render_currency(amount) do
    assigns = %{amount: amount}
    ~H"""
    <span class="font-mono text-green-600">
      $<%= :erlang.float_to_binary(@amount, decimals: 2) %>
    </span>
    """
  end

  defp render_order_status(status) do
    assigns = %{status: status}
    ~H"""
    <div class="flex items-center gap-2">
      <div class={[
        "w-2 h-2 rounded-full",
        case @status do
          "pending" -> "bg-yellow-400"
          "processing" -> "bg-blue-400"
          "shipped" -> "bg-purple-400"
          "delivered" -> "bg-green-400"
          "cancelled" -> "bg-red-400"
        end
      ]}></div>
      <span class="capitalize text-sm"><%= @status %></span>
    </div>
    """
  end

  defp render_date(date) do
    assigns = %{date: date}
    ~H"""
    <span class="text-sm">
      <%= Calendar.strftime(@date, "%b %d, %Y") %>
    </span>
    """
  end

  defp render_actions(_value, record) do
    assigns = %{record: record}
    ~H"""
    <div class="flex gap-2">
      <.link 
        navigate={~p"/orders/#{@record.id}"} 
        class="text-blue-600 hover:text-blue-800 text-sm font-medium"
      >
        View
      </.link>
      <%= if @record.status in ["pending", "processing"] do %>
        <button 
          phx-click="cancel_order" 
          phx-value-id={@record.id}
          class="text-red-600 hover:text-red-800 text-sm font-medium"
          data-confirm="Are you sure you want to cancel this order?"
        >
          Cancel
        </button>
      <% end %>
    </div>
    """
  end

  def handle_event("cancel_order", %{"id" => id}, socket) do
    order = YourApp.Orders.get_order!(id)
    case YourApp.Orders.cancel_order(order) do
      {:ok, _order} ->
        {:noreply, put_flash(socket, :info, "Order cancelled successfully")}
      {:error, _changeset} ->
        {:noreply, put_flash(socket, :error, "Unable to cancel order")}
    end
  end
end

Inventory Management Table

A product inventory table with stock alerts:

# lib/your_app_web/live/inventory_live/index.ex
defmodule YourAppWeb.InventoryLive.Index do
  use YourAppWeb, :live_view
  use LiveTable.LiveResource, schema: YourApp.Product

  def fields do
    [
      sku: %{label: "SKU", sortable: true, searchable: true},
      name: %{label: "Product", sortable: true, searchable: true},
      stock_quantity: %{label: "Current Stock", sortable: true, renderer: &render_stock/2},
      reorder_point: %{label: "Reorder Point", sortable: true},
      last_restocked: %{label: "Last Restocked", sortable: true, renderer: &render_date/1},
      supplier_name: %{label: "Supplier", sortable: true, searchable: true},
      actions: %{label: "Actions", sortable: false, renderer: &render_inventory_actions/2}
    ]
  end

  def filters do
    [
      low_stock: Boolean.new(:stock_quantity, "low_stock", %{
        label: "Low Stock Alert",
        condition: dynamic([p], p.stock_quantity <= p.reorder_point)
      }),
      
      out_of_stock: Boolean.new(:stock_quantity, "out_of_stock", %{
        label: "Out of Stock",
        condition: dynamic([p], p.stock_quantity == 0)
      }),
      
      stock_range: Range.new(:stock_quantity, "stock_range", %{
        type: :number,
        label: "Stock Quantity",
        min: 0,
        max: 1000
      }),
      
      needs_reorder: Boolean.new(:stock_quantity, "needs_reorder", %{
        label: "Needs Reorder",
        condition: dynamic([p], p.stock_quantity <= p.reorder_point and p.stock_quantity > 0)
      })
    ]
  end

  def table_options do
    %{
      pagination: %{
        enabled: true,
        sizes: [20, 50, 100],
        default_size: 50
      },
      sorting: %{
        default_sort: [stock_quantity: :asc]  # Show low stock first
      },
      exports: %{
        enabled: true,
        formats: [:csv]
      }
    }
  end

  defp render_stock(stock_quantity, record) do
    assigns = %{stock: stock_quantity, record: record}
    ~H"""
    <div class="flex items-center gap-2">
      <span class={[
        "font-medium",
        cond do
          @stock == 0 -> "text-red-600"
          @stock <= @record.reorder_point -> "text-orange-600"
          @stock <= @record.reorder_point * 2 -> "text-yellow-600"
          true -> "text-green-600"
        end
      ]}>
        <%= @stock %>
      </span>
      
      <%= cond do %>
        <% @stock == 0 -> %>
          <span class="bg-red-100 text-red-700 text-xs px-2 py-1 rounded">OUT</span>
        <% @stock <= @record.reorder_point -> %>
          <span class="bg-orange-100 text-orange-700 text-xs px-2 py-1 rounded">LOW</span>
        <% @stock <= @record.reorder_point * 2 -> %>
          <span class="bg-yellow-100 text-yellow-700 text-xs px-2 py-1 rounded">WATCH</span>
        <% true -> %>
          <span class="bg-green-100 text-green-700 text-xs px-2 py-1 rounded">OK</span>
      <% end %>
    </div>
    """
  end

  defp render_date(nil) do
    assigns = %{}
    ~H"""
    <span class="text-gray-400 text-sm">Never</span>
    """
  end

  defp render_date(date) do
    assigns = %{date: date}
    ~H"""
    <span class="text-sm">
      <%= Calendar.strftime(@date, "%b %d, %Y") %>
    </span>
    """
  end

  defp render_inventory_actions(_value, record) do
    assigns = %{record: record}
    ~H"""
    <div class="flex gap-2">
      <button 
        phx-click="restock" 
        phx-value-id={@record.id}
        class="bg-blue-600 text-white px-3 py-1 rounded text-sm hover:bg-blue-700"
      >
        Restock
      </button>
      
      <%= if @record.stock_quantity <= @record.reorder_point do %>
        <button 
          phx-click="auto_reorder" 
          phx-value-id={@record.id}
          class="bg-green-600 text-white px-3 py-1 rounded text-sm hover:bg-green-700"
        >
          Auto Reorder
        </button>
      <% end %>
      
      <.link 
        navigate={~p"/inventory/#{@record.id}/adjust"} 
        class="text-gray-600 hover:text-gray-800 text-sm"
      >
        Adjust
      </.link>
    </div>
    """
  end

  def handle_event("restock", %{"id" => id}, socket) do
    # Handle manual restock
    {:noreply, redirect(socket, to: ~p"/inventory/#{id}/restock")}
  end

  def handle_event("auto_reorder", %{"id" => id}, socket) do
    product = YourApp.Inventory.get_product!(id)
    case YourApp.Inventory.create_reorder(product) do
      {:ok, _reorder} ->
        {:noreply, put_flash(socket, :info, "Reorder created successfully")}
      {:error, _changeset} ->
        {:noreply, put_flash(socket, :error, "Failed to create reorder")}
    end
  end
end

Simple Read-Only Table

A minimal table for displaying reference data:

# lib/your_app_web/live/category_live/index.ex
defmodule YourAppWeb.CategoryLive.Index do
  use YourAppWeb, :live_view
  use LiveTable.LiveResource, schema: YourApp.Category

  def fields do
    [
      id: %{label: "ID", sortable: true},
      name: %{label: "Category Name", sortable: true, searchable: true},
      description: %{label: "Description", sortable: false, searchable: true},
      product_count: %{label: "Products", sortable: true, renderer: &render_count/1},
      active: %{label: "Status", sortable: true, renderer: &render_active_status/1}
    ]
  end

  def filters do
    [
      active: Boolean.new(:active, "active", %{
        label: "Active Categories Only",
        condition: dynamic([c], c.active == true)
      })
    ]
  end

  def table_options do
    %{
      pagination: %{enabled: false},  # Show all categories
      sorting: %{default_sort: [name: :asc]},
      exports: %{enabled: false},     # No exports needed
      search: %{
        enabled: true,
        placeholder: "Search categories..."
      }
    }
  end

  defp render_count(count) do
    assigns = %{count: count}
    ~H"""
    <span class="bg-gray-100 text-gray-700 px-2 py-1 rounded text-sm">
      <%= @count %>
    </span>
    """
  end

  defp render_active_status(active) do
    assigns = %{active: active}
    ~H"""
    <span class={[
      "w-3 h-3 rounded-full inline-block",
      if(@active, do: "bg-green-400", else: "bg-gray-300")
    ]}></span>
    """
  end
end

Key Patterns for Simple Tables

1. Minimal Setup

  • Use the LiveResource module with schema: parameter
  • Define basic fields with labels and sortability
  • Add simple filters for common use cases

2. Custom Renderers

  • Use renderer: &function/1 for simple formatting
  • Use renderer: &function/2 when you need access to the full record
  • Keep renderers focused and lightweight

3. Practical Filters

  • Boolean filters for status toggles
  • Range filters for numeric and date fields
  • Select filters for enumerated values

4. Sensible Defaults

  • Enable pagination for large datasets
  • Set reasonable default sort orders
  • Configure exports based on user needs

5. Action Columns

  • Use sortable: false for action columns
  • Implement event handlers for interactive actions
  • Provide clear user feedback for actions

These examples demonstrate how LiveTable can handle common table requirements with minimal code while providing rich functionality for users.