Sort bar for list LiveViews — a field-picker <.select> plus a direction-
toggle button (chevron up / down). No wrapper chrome so it integrates
cleanly into a <.table_default toggleable> :toolbar_title slot.
How events fire
Race-free by design. Each control sends ONLY the field it controls:
- Field
<.select>firesphx-changewithparams == %{"sort_by" => "..."} - Direction button fires
phx-clickwithparams == %{"sort_dir" => "..."}
The LV handler derives the missing field from socket.assigns instead of
trusting stale DOM — so clicking the arrow while a change event is mid-
flight can never clobber the in-flight change.
def handle_event("sort_form", params, socket) do
field_str = params["sort_by"] || Atom.to_string(socket.assigns.sort_by)
dir_str = params["sort_dir"] || Atom.to_string(socket.assigns.sort_dir)
# cast / validate, then push_patch with both values
...
endLoading state
The button shows a spinner during in-flight clicks via Phoenix's auto-
applied .phx-click-loading class (no consumer code required). When the
field select fires, the form gets .phx-change-loading and the select
dims via the same mechanism. Both states fade automatically when the LV
acks the event.
Attributes
sort_by— Current sort field. Accepts atom or string. Required.sort_dir— Current direction (:asc|:desc|"asc"|"desc"). Anything else falls back to:asc. Required.options— List of{field, label}tuples for the field select.fieldmay be atom or string;labelis coerced viato_string/1. Bad rows (non-tuple) are silently dropped. Empty list renders an empty<select>. Required.event— Phoenix event name fired on both field change and direction flip. Default"sort_form".target— Optionalphx-targetfor LiveComponents.class— Extra classes on the inner<form>element.
Edge cases handled
- Atom-or-string inputs:
sort_byandsort_dirare normalised internally; the consumer doesn't need to convert. - Unknown direction: any value outside the known set renders as
:asc(conservative — surfaces a stable icon instead of silently rendering as descending on bad input). - Bad option shape: non-tuple rows skipped; atom-or-string labels coerced. One bad row doesn't blow up the whole select.
- Nil / wrong type
options: returns empty list, doesn't crash.
Example
<.sort_selector
sort_by={@sort_by}
sort_dir={@sort_dir}
options={@sort_options}
/>
Summary
Functions
Attributes
sort_by(:any) (required) - Current sort field (atom or string).sort_dir(:any) (required) - Current direction (:asc/:desc or 'asc'/'desc').options(:list) (required) - List of {field, label} tuples — field may be atom or string.event(:string) - Defaults to"sort_form".target(:any) - Defaults tonil.class(:string) - Defaults tonil.manual_field(:any) - Atom or string field key that represents "manual" ordering (e.g.:sort_order). Whensort_bymatches, the direction toggle is replaced by a static drag-handle hint icon — direction has no meaning for a user-specified order. Defaults tonil.