LiveTable.Select (live_table v0.4.0)

View Source

A module for handling select-based filters in LiveTable.

This module provides functionality for creating and managing select filters that can handle single or multiple selections. It supports both static options and dynamic option loading, with customizable appearances and templates.

Options

The module accepts the following options:

  • :label - The label text for the select filter
  • :options - Static list of options for the select
  • :options_source - Function or module for dynamic option loading
  • :option_template - Custom template for rendering options
  • :selected - List of pre-selected values
  • :placeholder - Placeholder text for the select
  • :css_classes - CSS classes for the main container
  • :label_classes - CSS classes for the label element

### LiveSelect Options

These options are passed directly to the underlying SutraUI.LiveSelect component:

  • :mode - Selection mode (default: :tags)
    • :single - Select one option, input shows selected label
    • :tags - Multi-select with tag pills, dropdown closes after each selection
    • :quick_tags - Multi-select with tag pills, dropdown stays open for rapid selection
  • :allow_clear - Show clear button in single mode (default: false)
  • :max_selectable - Maximum number of selections allowed, 0 = unlimited (default: 0)
  • :user_defined_options - Allow users to create custom options by typing (default: false)
  • :debounce - Debounce time in ms for search input (default: 100)

For default values, see: LiveTable.Select source code

Working with Options

There are two ways to configure and display options in the select filter:

1. Static Options

The simplest approach using a predefined list of options:

  Select.new(:status, "status_select", %{
    label: "Status",
    options: [
      %{label: "Active", value: [1, "Currently active"]},
      %{label: "Pending", value: [2, "Awaiting processing"]},
      %{label: "Archived", value: [3, "No longer active"]}
    ]
  })

2. Dynamic Options via options_source

Load options dynamically using a function or module. Used for fetching new options based on typed input. Uses apply/3 under the hood to apply the function. Uses live-select-change event to update the options.

    # Point to your custom function
    Select.new({:suppliers, :name}, "supplier_name", %{
      label: "Supplier",
      options_source: {Demo.Catalog, :search_suppliers, []} # Same as you'd use for `apply/3`
    })

    # in your context module
    def search_suppliers(text) do
      Supplier
      |> where([c], ilike(c.name, ^"%#{text}%"))
      |> select([c], {c.name, [c.id, c.contact_info]})
      |> Repo.all()
    end

You could write your function to have other args passed to it as well. Just make sure the first arg is the text.

Return Format Contract

IMPORTANT: The options_source callback MUST return data in one of these formats:

  # Format 1: Tuple with list value (recommended)
  {label, [primary_key, extra_info, ...]}

  # Format 2: Tuple with simple value
  {label, primary_key}

The first element of the value (or the value itself if not a list) is used as the primary key for filtering. This value is used in the WHERE id IN (...) query clause.

Correct Examples

  # Using user ID as the filter value
  def search_users(text) do
    User
    |> where([u], ilike(u.name, ^"%#{text}%"))
    |> select([u], {u.name, [u.id, u.email]})  # id is first element
    |> limit(10)
    |> Repo.all()
  end

  # Simple format with just the primary key
  def search_categories(text) do
    Category
    |> where([c], ilike(c.name, ^"%#{text}%"))
    |> select([c], {c.name, c.id})  # id is the value
    |> limit(10)
    |> Repo.all()
  end

Incorrect Examples

  # WRONG: Using email as the value - will fail when filtering by id
  def search_users(text) do
    User
    |> where([u], ilike(u.name, ^"%#{text}%"))
    |> select([u], {u.name, u.email})  # email is NOT a valid primary key!
    |> Repo.all()
  end

Option Templates

You can provide custom templates for rendering options in two ways:

  1. Using the default template format for options with label and value pairs
  2. Providing a custom template function through the :option_template option

Default Template

The default template expects options in the format:

  %{label: label, value: [id, description]}

The default template can be seen at git link

Custom Template

Custom templates can be provided as functions that take an option map and return rendered HTML:

  def custom_template(option) do
    assigns = %{option: option}
    ~H"""
    <div class="flex flex-col">
      <span class="font-bold"><%= @option.label %></span>
      <span class="text-sm text-gray-500"><%= @option.value |> Enum.at(0) %></span>
    </div>
    """
  end

  # in your filter definition
  Select.new({:suppliers, :name}, "supplier_name", %{
    label: "Supplier",
    placeholder: "Search for suppliers...",
    options_source: {Demo.Catalog, :search_suppliers, []}
    option_template: &custom_template/1
  })

Each method can be combined with others - for example, you could use dynamic or static options with custom templates.

Examples

If the field you want to use is part of the base schema(given to LiveResource), you can simply pass the field name as an atom.

  # Creating a basic select filter (tags mode - default)
  Select.new(:category, "category_select", %{
    label: "Category",
    options: [
      %{label: "Electronics", value: [1, "Electronics"]},
      %{label: "Books", value: [2, "Books"]}
    ]
  })

If its part of a joined schema, you can pass it as a tuple, with the table name(aliased in the query) and field name as shown-

  # Creating a select filter with options loaded from database
  Select.new({:suppliers, :name}, "supplier_name", %{
      label: "Supplier",
      options_source: {Demo.Catalog, :search_suppliers, []}
    })

Selection Mode Examples

  # Single selection mode - pick one option
  Select.new(:status, "status_filter", %{
    label: "Status",
    mode: :single,
    allow_clear: true,
    options: [
      %{label: "Active", value: 1},
      %{label: "Inactive", value: 0}
    ]
  })

  # Tags mode (default) - multi-select with dropdown closing after each selection
  Select.new(:categories, "categories_filter", %{
    label: "Categories",
    mode: :tags,
    max_selectable: 5,
    options_source: {Demo.Catalog, :search_categories, []}
  })

  # Quick tags mode - multi-select with dropdown staying open
  Select.new(:tags, "tags_filter", %{
    label: "Tags",
    mode: :quick_tags,
    user_defined_options: true,  # Allow creating new tags by typing
    options_source: {Demo.Content, :search_tags, []}
  })

Currently, nested relations are not supported.