Filters API Reference

View Source

Filters provide interactive filtering capabilities for your LiveTable. They allow users to narrow down displayed data using various input types like checkboxes, dropdowns, and range sliders.

Overview

Filters are defined in the filters/0 function as a keyword list where each key represents a filter and maps to a filter struct (Boolean, Range, or Select).

def filters do
  [
    active: Boolean.new(:active, "active", %{
      label: "Active Only",
      condition: dynamic([p], p.active == true)
    }),
    
    price_range: Range.new(:price, "price_range", %{
      type: :number,
      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

Filter Types

LiveTable supports three types of filters:

  • Boolean - Checkbox filters for true/false conditions
  • Range - Slider filters for numeric, date, or datetime ranges
  • Select - Dropdown filters with static or dynamic options

Common Filter Patterns

Filter Constructor Arguments

All filters use the same constructor pattern:

FilterType.new(field, key, options)
  • field - The database field to filter on (atom for simple fields, {:table_alias, :field} for joined fields)
  • key - URL parameter key (string, used for persistence and routing)
  • options - Map of filter-specific configuration options

Field References

Simple Schema Fields

For single-table queries, reference schema fields directly:

# Filter on Product.active field
active_filter: Boolean.new(:active, "active", %{...})

# Filter on Product.price field  
price_filter: Range.new(:price, "price_range", %{...})

Joined Fields (Custom Queries Only)

For custom queries with joins, reference the table alias:

# Filter on joined suppliers table
supplier_filter: Select.new({:suppliers, :name}, "supplier", %{...})

# Filter on joined categories table
category_filter: Boolean.new({:categories, :active}, "category_active", %{...})

Boolean Filters

Boolean filters render as checkboxes and apply dynamic query conditions when checked.

Basic Usage

def filters do
  [
    active: Boolean.new(:active, "active", %{
      label: "Active Products Only",
      condition: dynamic([p], p.active == true)
    }),
    
    in_stock: Boolean.new(:stock_quantity, "in_stock", %{
      label: "In Stock",
      condition: dynamic([p], p.stock_quantity > 0)
    })
  ]
end

Advanced Conditions

def filters do
  [
    # Complex condition with multiple criteria
    premium_products: Boolean.new(:price, "premium", %{
      label: "Premium Products (>$500 & Featured)",
      condition: dynamic([p], p.price > 500 and p.featured == true)
    }),
    
    # Condition using joined tables (for custom queries)
    verified_suppliers: Boolean.new({:suppliers, :verified}, "verified", %{
      label: "Verified Suppliers Only",
      condition: dynamic([p, suppliers: s], s.verified == true and s.active == true)
    })
  ]
end

Boolean Options

Boolean.new(field, key, %{
  label: "Filter Label",           # Required: Display text
  condition: dynamic_expression,   # Required: Ecto dynamic query
  default: false,                  # Optional: Default checked state
  class: "custom-css-class"        # Optional: CSS classes
})

Range Filters

Range filters provide sliders for filtering numeric values, dates, or datetimes within specified ranges.

Numeric Ranges

def filters do
  [
    price_range: Range.new(:price, "price_range", %{
      type: :number,
      label: "Price Range",
      unit: "$",
      min: 0,
      max: 1000,
      step: 10,
      default_min: 0,
      default_max: 1000
    }),
    
    age_range: Range.new(:age, "age_range", %{
      type: :number,
      label: "Age",
      min: 18,
      max: 65,
      step: 1
    })
  ]
end

Date Ranges

def filters do
  [
    created_date: Range.new(:inserted_at, "created_range", %{
      type: :date,
      label: "Created Date",
      min: ~D[2024-01-01],
      max: ~D[2024-12-31],
      default_min: ~D[2024-01-01],
      default_max: ~D[2024-12-31]
    })
  ]
end

DateTime Ranges

def filters do
  [
    updated_time: Range.new(:updated_at, "updated_range", %{
      type: :datetime,
      label: "Last Updated",
      min: ~N[2024-01-01 00:00:00],
      max: ~N[2024-12-31 23:59:59],
      step: 3600  # Step in seconds (1 hour)
    })
  ]
end

Range Options

Range.new(field, key, %{
  type: :number,                    # Required: :number, :date, or :datetime
  label: "Range Label",             # Required: Display text
  min: 0,                          # Required: Minimum value
  max: 100,                        # Required: Maximum value
  step: 1,                         # Optional: Step increment
  default_min: 0,                  # Optional: Default minimum
  default_max: 100,                # Optional: Default maximum
  unit: "$",                       # Optional: Unit display
  css_classes: "custom-wrapper",   # Optional: CSS classes
  slider_classes: "custom-slider", # Optional: Slider CSS classes
  pips: true,                      # Optional: Show slider markers
  event_type: "change"             # Optional: JS event type
})

Select Filters

Select filters provide dropdown selection with static or dynamic options.

Static Options

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"]}
      ]
    }),
    
    priority: Select.new(:priority, "priority", %{
      label: "Priority Level",
      options: [
        %{label: "Low", value: ["low"]},
        %{label: "Medium", value: ["medium"]},
        %{label: "High", value: ["high"]},
        %{label: "Critical", value: ["critical"]}
      ]
    })
  ]
end

Dynamic Options

For options loaded from the database:

def filters do
  [
    category: Select.new({:categories, :name}, "category", %{
      label: "Product Category",
      options_source: {YourApp.Catalog, :search_categories, []}
    }),
    
    supplier: Select.new({:suppliers, :name}, "supplier", %{
      label: "Supplier",
      placeholder: "Search suppliers...",
      options_source: {YourApp.Suppliers, :search_suppliers, []}
    })
  ]
end

The corresponding context functions must return tuples:

# lib/your_app/catalog.ex
defmodule YourApp.Catalog do
  def search_categories(search_text \\ "") do
    Category
    |> where([c], ilike(c.name, ^"%#{search_text}%"))
    |> select([c], {c.name, [c.id, c.description]})
    |> Repo.all()
  end
end

# lib/your_app/suppliers.ex  
defmodule YourApp.Suppliers do
  def search_suppliers(search_text \\ "") do
    Supplier
    |> where([s], ilike(s.name, ^"%#{search_text}%"))
    |> select([s], {s.name, [s.id, s.contact_email]})
    |> Repo.all()
  end
end

Custom Option Templates

def filters do
  [
    supplier: Select.new({:suppliers, :name}, "supplier", %{
      label: "Supplier",
      options_source: {YourApp.Suppliers, :search_suppliers, []},
      option_template: &custom_supplier_template/1
    })
  ]
end

defp custom_supplier_template(option) do
  assigns = %{option: option}
  ~H"""
  <div class="flex flex-col">
    <span class="font-semibold text-sm"><%= @option.label %></span>
    <span class="text-xs text-gray-500">ID: <%= @option.value |> Enum.at(0) %></span>
    <span class="text-xs text-gray-400"><%= @option.value |> Enum.at(1) %></span>
  </div>
  """
end

Select Options

Select.new(field, key, %{
  label: "Select Label",                    # Required: Display text
  options: [],                             # Static options list
  options_source: {Module, :function, []}, # Dynamic options source
  option_template: &template_function/1,   # Custom option template
  placeholder: "Choose an option...",      # Placeholder text
  loading_text: "Loading...",              # Loading state text
  css_classes: "custom-wrapper",           # CSS classes
  label_classes: "custom-label",           # Label CSS classes
  select_classes: "custom-select"          # Select input CSS classes
})

Real-World Examples

E-commerce Product Filters

def filters do
  [
    # Active products toggle
    active: Boolean.new(:active, "active", %{
      label: "Show Active Products Only",
      condition: dynamic([p], p.active == true)
    }),
    
    # Price range slider
    price_range: Range.new(:price, "price_range", %{
      type: :number,
      label: "Price Range",
      unit: "$",
      min: 0,
      max: 2000,
      step: 25,
      default_min: 0,
      default_max: 500
    }),
    
    # Category dropdown with search
    category: Select.new({:categories, :name}, "category", %{
      label: "Category",
      placeholder: "Search categories...",
      options_source: {Shop.Catalog, :search_categories, []}
    }),
    
    # Stock level toggle
    low_stock: Boolean.new(:stock_quantity, "low_stock", %{
      label: "Low Stock Alert (< 10 items)",
      condition: dynamic([p], p.stock_quantity < 10 and p.stock_quantity > 0)
    }),
    
    # Recently added toggle
    new_arrivals: Boolean.new(:inserted_at, "new_arrivals", %{
      label: "New Arrivals (Last 30 Days)",
      condition: dynamic([p], p.inserted_at >= ago(30, "day"))
    })
  ]
end

User Management Filters

def filters do
  [
    # Active users
    active_users: Boolean.new(:active, "active", %{
      label: "Active Users Only",
      condition: dynamic([u], u.active == true)
    }),
    
    # Registration date range
    signup_date: Range.new(:inserted_at, "signup_range", %{
      type: :date,
      label: "Registration Date",
      min: ~D[2020-01-01],
      max: Date.utc_today()
    }),
    
    # Role selection
    role: Select.new(:role, "role", %{
      label: "User Role",
      options: [
        %{label: "Admin", value: ["admin"]},
        %{label: "Manager", value: ["manager"]},
        %{label: "User", value: ["user"]},
        %{label: "Guest", value: ["guest"]}
      ]
    }),
    
    # Email verified toggle
    verified: Boolean.new(:email_verified_at, "verified", %{
      label: "Email Verified",
      condition: dynamic([u], not is_nil(u.email_verified_at))
    })
  ]
end

Order Analytics Filters

def filters do
  [
    # Order status
    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 value range
    order_total: Range.new(:total_amount, "total_range", %{
      type: :number,
      label: "Order Total",
      unit: "$",
      min: 0,
      max: 10000,
      step: 100
    }),
    
    # Date range for order placement
    order_date: Range.new(:inserted_at, "order_date_range", %{
      type: :date,
      label: "Order Date",
      min: Date.add(Date.utc_today(), -365),
      max: Date.utc_today()
    }),
    
    # High priority orders
    priority_orders: Boolean.new(:priority, "priority", %{
      label: "Priority Orders Only",
      condition: dynamic([o], o.priority in ["high", "urgent"])
    }),
    
    # Customer type (for custom queries with joins)
    customer_type: Select.new({:customers, :type}, "customer_type", %{
      label: "Customer Type",
      options: [
        %{label: "Individual", value: ["individual"]},
        %{label: "Business", value: ["business"]},
        %{label: "Enterprise", value: ["enterprise"]}
      ]
    })
  ]
end

Filter Persistence

Filters automatically persist their state in URL parameters, allowing users to:

  • Bookmark filtered views
  • Share filtered URLs
  • Navigate back/forward with filter state intact
  • Refresh the page without losing filters

The key parameter in each filter constructor determines the URL parameter name:

# Creates URL parameter: ?active=true
active: Boolean.new(:active, "active", %{...})

# Creates URL parameter: ?price_range[min]=100&price_range[max]=500  
price_range: Range.new(:price, "price_range", %{...})

# Creates URL parameter: ?category[]=electronics&category[]=books
category: Select.new(:category, "category", %{...})

Performance Tips

Optimize Filter Queries

# ✅ Good: Simple field conditions
condition: dynamic([p], p.active == true)
condition: dynamic([p], p.price > 100)

# ⚠️ Be careful: Complex subqueries
condition: dynamic([p], fragment("(SELECT COUNT(*) FROM orders WHERE product_id = ?) > 5", p.id))

# ✅ Better: Use database views or computed fields for complex logic

Index Filtered Fields

Make sure frequently filtered fields have database indexes:

-- Add indexes for better filter performance
CREATE INDEX idx_products_active ON products(active);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_products_category_id ON products(category_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_inserted_at ON orders(inserted_at);

Troubleshooting

Filter Not Appearing

  • Check that the filter is included in your filters/0 function
  • Verify the filter syntax and required options
  • Ensure Boolean/Range/Select are properly imported

Filter Not Working

  • For simple queries: ensure field exists in your schema
  • For custom queries: ensure field/alias exists in your query
  • Check dynamic query syntax in Boolean filter conditions
  • Verify options_source functions return correct tuple format for Select filters

Options Not Loading (Select Filters)

  • Check that your context function is accessible
  • Verify the function returns {label, [value1, value2]} tuples
  • Ensure the module/function exists and is properly imported
  • Check for any errors in your context function

URL Parameters Not Persisting

  • Verify each filter has a unique key parameter
  • Check that keys don't conflict with other URL parameters
  • Ensure LiveView handle_params is properly set up