SelectoComponents
Alpha software. Expect API and behavior changes while the core LiveView package continues to settle.
selecto_components provides Phoenix LiveView components for building interactive query UIs on top of selecto.
It is the package you use when you want users to:
- build filters visually
- choose fields for detail and aggregate views
- switch between built-in result views
- save/export/share views
- email or schedule exports through host-app integrations
What It Includes
SelectoComponents.Exploreras the preferred top-level exploration surfaceSelectoComponents.Formfor query-building and view configurationSelectoComponents.Resultsfor result rendering- built-in views:
DetailAggregateGraph
- extension-driven view support such as map views via
selecto_postgis - exported-view, email-export, and scheduled-export integration points
Requirements
- Phoenix 1.7+
- Elixir ~> 1.18
selecto >= 0.4.6 and < 0.6.0- an adapter package such as
selecto_db_postgresql >= 0.4.4 and < 0.6.0 selecto_mix >= 0.4.6 and < 0.6.0if you want generators and installation helpers
Installation
def deps do
[
{:selecto_components, ">= 0.4.8 and < 0.6.0"},
{:selecto, ">= 0.4.6 and < 0.6.0"},
{:selecto_db_postgresql, ">= 0.4.4 and < 0.6.0"},
{:selecto_mix, ">= 0.4.6 and < 0.6.0"}
]
end
Then run the recommended integration task:
mix deps.get
mix selecto.components.integrate
mix assets.build
That wires:
- colocated SelectoComponents hooks into your LiveSocket config
- Tailwind
@sourcecoverage for component templates
Minimal Usage
SelectoComponents.Explorer is now the preferred render surface.
Current migration note:
- use
SelectoComponents.Formin the parent LiveView for the existing event-handler andget_initial_state/2compatibility path - render
SelectoComponents.Explorerinstead of renderingFormandResultsseparately
defmodule MyAppWeb.ProductLive do
use MyAppWeb, :live_view
use SelectoComponents.Form
alias MyApp.SelectoDomains.ProductDomain
alias MyApp.Repo
def mount(_params, _session, socket) do
selecto = ProductDomain.new(Repo)
views = [
{:detail, SelectoComponents.Views.Detail, "Detail", %{}},
{:aggregate, SelectoComponents.Views.Aggregate, "Aggregate", %{}},
{:graph, SelectoComponents.Views.Graph, "Graph", %{}}
]
{:ok, assign(socket, get_initial_state(views, selecto))}
end
def render(assigns) do
~H"""
<.live_component module={SelectoComponents.Explorer} id="product-explorer" {assigns} />
"""
end
end
Config-Driven Explorer
You can also hand Explorer a smaller config struct while keeping the current parent LiveView compatibility boot path:
config = %SelectoComponents.Explorer.Config{
id: "products",
selecto: ProductDomain.new(Repo),
views: [
SelectoComponents.Views.spec(:detail, SelectoComponents.Views.Detail, "Detail", %{}),
SelectoComponents.Views.spec(:aggregate, SelectoComponents.Views.Aggregate, "Aggregate", %{}),
SelectoComponents.Views.spec(:graph, SelectoComponents.Views.Graph, "Graph", %{})
],
title: "Products Explorer",
presentation: %{timezone: "America/New_York"}
}
The current runtime still expects the parent LiveView to own state/event setup. Explorer.Config is the first host-facing config seam, not a full replacement for that compatibility path yet.
Common Add-Ons
You can start from Explorer and progressively add host-app integrations.
Compatibility note:
Form+Resultsstill work- new docs/examples should prefer
Explorer
Saved Views
Use a host module that persists saved views and assign it to the LiveView.
Typical generation path:
mix selecto.gen.saved_views MyApp.SavedView MyApp.SavedViewContext
Exported Views
Use SelectoComponents.ExportedViews when you want signed iframe/embed snapshots of current views.
Email And Scheduled Exports
Assign these modules when you want the Export tab to send or manage exports:
assign(socket,
export_delivery_module: MyApp.ExportDelivery,
scheduled_export_module: MyApp.ScheduledExports,
scheduled_export_context: scoped_context
)
The host app owns actual delivery and scheduling. selecto_components stays scheduler-neutral.
Recommended execution model: use Oban (or another worker system) to run due scheduled exports via SelectoComponents.ScheduledExports.Service.run_scheduled_export/3.
Extension Views
Map views and other extension-provided view systems are merged from domain extensions rather than hard-coded into the package.
Query Contracts
SelectoComponents.QueryContract.build/1 returns the constrained
Selecto.Domain.query_contract/1 projection for Components-facing tooling. It
accepts an authored domain, a normalized domain, or a configured Selecto struct
without changing the existing Explorer/Form runtime path.
Use SelectoComponents.QueryContract.json_document/1 or encode_json/2 when a
consumer needs a query_contract.json-ready artifact with string keys and
JSON-compatible values. Pass query_contract_url and query_guide_url to add
discovery links for tools that need to move between the JSON contract and its
Markdown guide. The JSON and Markdown Plugs also emit HTTP Link headers for
the same pair of artifacts, plus byte-accurate ETag headers with
If-None-Match support for conditional GETs.
Use SelectoComponents.QueryContract.validate_intent/2 to check a generated
detail, aggregate, or graph query intent against a query contract before handing
it to runtime query code. Host apps can also mount
SelectoComponents.QueryContract.IntentValidator.Plug to expose the same
validation over an HTTP POST endpoint.
Host apps can mount SelectoComponents.QueryContract.Plug for a small JSON
endpoint:
forward "/selecto/orders/query-contract.json",
SelectoComponents.QueryContract.Plug,
domain: MyApp.SelectoDomains.Orders.domain()
For the full host integration path, including generated action forms,
capability policy, choice-source diagnostics, exported views, scheduled exports,
and reload/result semantics, see docs/developer_integration_guide.md.
Generated Domain Action Forms
Domains can expose write-contract actions under :actions. Selecto Components
projects those actions into the existing row-action modal path with generated
ids like domain_action_form_archive. A detail view can select one of those ids
as row_click_action; clicking a row opens SelectoComponents.Modal.ActionFormModal
with the normalized action metadata, target row, inputs, confirmation state, and
preview/apply request template.
The modal does not execute writes directly. The host LiveView handles
{:selecto_action_form_submit, payload} and calls its own preview/apply
adapter, usually through SelectoComponents.ActionFormHost.handle_submit/3.
After a successful apply, the host should refresh the active Selecto query so
the row state reflects the write result.
Bulk-scoped domain actions can be projected separately with
SelectoComponents.Actions.bulk_actions/2. That helper returns the same
live-component payload shape as generated row action forms, but defaults the
target template to selected row ids.
SelectoComponents.EnhancedTable.BulkActions can receive :action_contract,
:write_contract, :domain, or :selecto assigns. Bulk-scoped domain actions
from that contract are added to the bulk menu as generated action forms; opening
one sends the same detail-modal event with target.ids set to the current
selection.
The older explicit actions: bulk-action API has been removed before 1.0. New
bulk actions should be expressed in the domain action contract.
Action DSL Shape
An action is host-owned metadata in the Selecto domain. The execution contract
can use static values, or reference normalized form inputs with {:input, id}:
defaction(:set_estimate, %{
id: :set_estimate,
label: "Set estimate",
type: :update,
scope: :row,
target: :work_item,
capability: "work_items.estimation",
inputs: %{
estimate_hours: %{type: :integer, label: "Estimate hours", required: true, min: 0}
},
confirmation: %{required: false, destructive: false},
execution: %{
kind: :updato,
operation: :update,
set: %{estimate_hours: {:input, :estimate_hours}}
},
links: %{
preview: "/api/v1/updato/work-items/actions/set_estimate/preview",
apply: "/api/v1/updato/work-items/actions/set_estimate/apply"
}
})
The generated modal builds this request shape and sends it to the parent LiveView:
%{
intent: "apply",
action_id: "set_estimate",
action_label: "Set estimate",
action_scope: "row",
action_operation: "update",
capability: "work_items.estimation",
target: %{"id" => 42},
inputs: %{"estimate_hours" => "8"},
confirmation_required?: false,
endpoints: %{
"preview" => %{"href" => "/api/v1/updato/work-items/actions/set_estimate/preview"},
"apply" => %{"href" => "/api/v1/updato/work-items/actions/set_estimate/apply"}
},
request: %{
"action" => "set_estimate",
"target" => %{"id" => 42},
"inputs" => %{"estimate_hours" => "8"},
"confirmed" => false
}
}
The host should treat this as an intent, not as permission to write directly. Typical host responsibilities are:
- Re-check target-aware action availability before showing the modal.
- Preview/apply through a server-owned adapter.
- Normalize and whitelist writable fields in that adapter.
- Refresh the active Selecto query and close or reset the modal after apply.
- Use flash/error messages for the result surface the host wants.
Custom View Systems
selecto_components supports external view packages through SelectoComponents.Views.System.
That is the contract used when you want to publish a package like:
selecto_components_view_<slug>
and register it into a host LiveView with SelectoComponents.Views.spec/4.
Status
Current 0.4.x scope:
- core query UI flows are usable but still alpha
- exported views, one-off email export, and scheduled export management are available
- custom and extension view support exists, but host apps still own persistence and delivery concerns
- advanced graph/dashboard integrations still need real-world hardening
Demos And Tutorials
selecto_livebooksselecto_northwind- hosted demo:
testselecto.fly.dev - runnable example app:
selecto_example