SelectoComponents

⚠️ Alpha Quality Software

selecto_components is under active development. Expect breaking changes, behavior changes, incomplete features, and potentially major bugs.

Phoenix LiveView components for building interactive data query interfaces with Selecto.

Overview

SelectoComponents provides a suite of Phoenix LiveView components that enable users to build complex queries, visualize data, and interact with Ecto-based schemas through a visual interface. The library includes:

Livebooks, Tutorials, and Demo

Release Status (0.4.x)

Requirements

Installation

1. Add Dependencies

In your mix.exs:

def deps do
  [
    {:selecto_components, "~> 0.4.0"},
    {:selecto, "~> 0.4.0"},
    {:selecto_db_postgresql, "~> 0.4.0"},
    # Optional extension package for map/spatial views
    {:selecto_postgis, "~> 0.1"},
    {:selecto_mix, "~> 0.4.0"}  # For generators and integration
  ]
end

Replace selecto_db_postgresql with the adapter package your application uses.

Then install:

mix deps.get

2. Quick Setup (Recommended)

# Automatically integrate hooks and styles
mix selecto.components.integrate

# Build assets
mix assets.build

That's it! The integration task automatically:

3. Manual Setup (Alternative)

If you prefer to configure manually or the integration task doesn't work:

In assets/css/app.css:

/* Add this @source directive for SelectoComponents styles */
@source "../../deps/selecto_components/lib/**/*.{ex,heex}";

In assets/js/app.js:

// Add this import at the top
import {hooks as selectoHooks} from "phoenix-colocated/selecto_components"

// In your LiveSocket configuration, spread the hooks:
const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {
    ...selectoHooks,  // Add this line
    // your other hooks...
  }
})

Then build assets:

mix assets.build

Usage

Extension-Provided Views (Map)

selecto_components can merge extension-provided views from your configured Selecto domain. For PostGIS-backed map views:

  1. Add {:selecto_postgis, "~> 0.1"} to your dependencies.
  2. Add Selecto.Extensions.PostGIS in your domain :extensions list.
  3. Keep your normal base views list (detail, aggregate, graph); the map view is merged automatically when the extension and spatial columns are present.
  4. If your app validates saved-view types, include map in allowed view types.

Step 1: Generate a Domain

Use selecto_mix to generate a domain from your Ecto schema:

# Generate domain configuration only
mix selecto.gen.domain MyApp.Catalog.Product

# Generate with LiveView (recommended - includes integration)
mix selecto.gen.domain MyApp.Catalog.Product --live

# Generate with saved views support
mix selecto.gen.domain MyApp.Catalog.Product --live --saved-views

This creates:

Step 2: Use in LiveView

If you generated with --live, a LiveView is created for you. Otherwise, create one:

defmodule MyAppWeb.ProductLive do
  use MyAppWeb, :live_view
  use SelectoComponents.Form  # Adds form handling utilities
  
  alias MyApp.SelectoDomains.ProductDomain
  alias MyApp.Repo
  
  @impl true
  def mount(_params, _session, socket) do
    # Initialize domain and selecto
    selecto = ProductDomain.new(Repo)
    domain = ProductDomain.domain()
    
    # Configure available views
    views = [
      {:detail, SelectoComponents.Views.Detail, "Table View", %{}},
      {:aggregate, SelectoComponents.Views.Aggregate, "Summary", %{}},
      {:graph, SelectoComponents.Views.Graph, "Charts", %{}}
    ]
    
    # Initialize state (from SelectoComponents.Form)
    state = get_initial_state(views, selecto)
    
    {:ok, assign(socket, state)}
  end
  
  @impl true
  def render(assigns) do
    ~H"""
    <div class="p-4">
      <h1 class="text-2xl mb-4">Product Explorer</h1>
      
      <.live_component
        module={SelectoComponents.Form}
        id="product-form"
        {assigns}
      />
      
      <.live_component
        module={SelectoComponents.Results}
        id="product-results"
        {assigns}
      />
    </div>
    """
  end
end

Recent 0.3.4+ Updates

Exported Views (Iframe Embeds)

SelectoComponents.Form can optionally manage signed exported views for dashboard-style iframe embeds.

Add these assigns to the LiveView that already uses SelectoComponents.Form:

assign(socket,
  exported_view_module: MyApp.ExportedViews,
  exported_view_context: scoped_context,
  exported_view_endpoint: MyAppWeb.Endpoint,
  exported_view_base_url: "/selecto/exported"
)

Your persistence module should implement SelectoComponents.ExportedViews. SelectoComponents will then:

To serve the iframe, add a wrapper LiveView in the host app:

defmodule MyAppWeb.ExportedViewLive do
  use MyAppWeb, :live_view

  def mount(params, session, socket) do
    SelectoComponents.ExportedViews.EmbedLive.mount(
      params,
      session,
      socket,
      adapter: MyApp.ExportedViews,
      endpoint: MyAppWeb.Endpoint
    )
  end

  def handle_info(msg, socket) do
    SelectoComponents.ExportedViews.EmbedLive.handle_info(msg, socket)
  end

  def handle_event(event, params, socket) do
    SelectoComponents.ExportedViews.EmbedLive.handle_event(event, params, socket)
  end

  def render(assigns) do
    SelectoComponents.ExportedViews.EmbedLive.render(assigns)
  end
end

Then wire a single public route:

live "/selecto/exported/:public_id", ExportedViewLive

Filter Processing and Rendering

Filter processing has been expanded for more consistent operator support across form inputs:

Aggregate Group-By Safety

Aggregate group-by display processing now applies COALESCE('[NULL]') only to text-compatible selectors. This prevents SQL type mismatch errors when grouping by numeric, enum, and other non-text fields.

Custom Detail Modal Component

You can now provide a custom modal component instead of the built-in SelectoComponents.Modal.DetailModal:

<.live_component
  module={SelectoComponents.Form}
  id="product-form"
  detail_modal_component={MyAppWeb.ProductDetailModal}
  {assigns}
/>

Your custom modal receives detail_data and is rendered whenever enable_modal_detail and show_detail_modal are true.

Debug Information Panel (Opt-In)

Debug UI visibility is request-gated:

If you want the debug panel always enabled in development, pass debug params to SelectoComponents.Results from your LiveView.

Available Components

Views Module

View Types

Custom View Systems

SelectoComponents now supports a formal view-system contract via SelectoComponents.Views.System.

You can publish external view packages (for example selecto_components_workflow or selecto_components_faceted_product) by exposing a top-level view module that implements the behavior.

defmodule SelectoComponentsWorkflow.Views.Workflow do
  use SelectoComponents.Views.System,
    process: SelectoComponentsWorkflow.Views.Workflow.Process,
    form: SelectoComponentsWorkflow.Views.Workflow.Form,
    component: SelectoComponentsWorkflow.Views.Workflow.Component
end

Then register it like any built-in view:

views = [
  SelectoComponents.Views.spec(
    :workflow,
    SelectoComponentsWorkflow.Views.Workflow,
    "Workflow",
    %{drill_down: :detail}
  ),
  SelectoComponents.Views.spec(
    :faceted_product,
    SelectoComponentsFacetedProduct.Views.FacetedProduct,
    "Faceted Product",
    %{}
  )
]

Custom views should implement SelectoComponents.Views.System directly or use use SelectoComponents.Views.System, ....

Implementing A New View System

Use this process for any new view package (for example selecto_components_view_workflow_inbox or selecto_components_view_faceted_product).

  1. Create a package with name selecto_components_view_<slug>.
  2. Add dependency on selecto_components (path dep for local/vendor, Hex dep for published use).
  3. Implement a top-level view module that uses SelectoComponents.Views.System.
  4. Implement the three modules referenced by the top-level view: Process, Form, Component.
  5. Register the view in your LiveView views list with SelectoComponents.Views.spec/4.
  6. Add the new view type to saved-view validation in your host app (if your app validates allowed view types).
  7. Compile and test the LiveView by switching to the new tab and submitting.

Expected package layout:

vendor/selecto_components_view_<slug>/
  mix.exs
  lib/selecto_components_view_<slug>.ex
  lib/selecto_components_view_<slug>/views/<slug>.ex
  lib/selecto_components_view_<slug>/views/<slug>/process.ex
  lib/selecto_components_view_<slug>/views/<slug>/form.ex
  lib/selecto_components_view_<slug>/views/<slug>/component.ex

Top-level view module:

defmodule SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox do
  use SelectoComponents.Views.System,
    process: SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox.Process,
    form: SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox.Form,
    component: SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox.Component
end

Process callback contract:

@callback initial_state(selecto :: term(), options :: map()) :: map()
@callback param_to_state(params :: map(), options :: map()) :: map()
@callback view(
  options :: map(),
  params :: map(),
  columns_map :: map(),
  filtered :: term(),
  selecto :: term()
) :: {view_set :: map(), view_meta :: map()}

0.3.4 note: built-in views were further compartmentalized with optional view-local helper modules (for example options normalization, drill-down actions, query pagination helpers). This does not change the formal SelectoComponents.Views.System callback contract above.

Minimal registration in a LiveView:

views = [
  SelectoComponents.Views.spec(:detail, SelectoComponents.Views.Detail, "Detail View", %{}),
  SelectoComponents.Views.spec(
    :workflow_inbox,
    SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox,
    "Workflow Inbox",
    %{}
  )
]

If your app persists saved views by type, include your new type. Example:

@view_types ~w(detail aggregate graph workflow_inbox faceted_product)

Verification checklist:

  1. mix compile succeeds after adding deps and modules.
  2. Open the LiveView, toggle View Controller, confirm your tab appears.
  3. Select the tab, submit config, confirm results render.
  4. Save and reload a saved view for the new type.
  5. Confirm invalid/missing config shows a user-visible error state.

Core Components

Support Modules

JavaScript Hooks

SelectoComponents uses Phoenix LiveView's colocated JavaScript feature. The hooks are embedded directly in the components and extracted during compilation:

  1. .TreeBuilder - Drag-and-drop functionality for the query builder
  2. .GraphComponent - Interactive charting with Chart.js

These hooks are automatically available after running mix selecto.components.integrate or manually adding the import to your app.js.

Troubleshooting

Hooks Not Working

  1. Run the integration task:

    mix selecto.components.integrate --check  # Check if integrated
    mix selecto.components.integrate          # Apply integration
  2. Verify app.js has the import:

    import {hooks as selectoHooks} from "phoenix-colocated/selecto_components"
    // ...
    hooks: { ...selectoHooks }
  3. Rebuild assets:

    mix assets.build
  4. Check browser console for JavaScript errors

Styles Not Applied

  1. Verify app.css has the @source directive:

    @source "../../deps/selecto_components/lib/**/*.{ex,heex}";
  2. Rebuild Tailwind:

    mix assets.build

Integration Task Issues

If mix selecto.components.integrate fails:

Development

This library is part of the Selecto ecosystem and is typically developed alongside:

For local workspace development against an unreleased selecto, set:

SELECTO_ECOSYSTEM_USE_LOCAL=true

When enabled, selecto_components resolves {:selecto, path: "../selecto"}. This is the same local-development switch used across Selecto ecosystem repos.

Property Testing

Run the SelectoComponents property suite:

SELECTO_ECOSYSTEM_USE_LOCAL=true mix test test/selecto_components/property_roundtrip_test.exs

License

MIT License - see LICENSE file for details.