# `PhoenixKitWeb.Components.Core.SortSelector`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.165/lib/phoenix_kit_web/components/core/sort_selector.ex#L1)

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>` fires `phx-change` with `params == %{"sort_by" => "..."}`
- Direction button fires `phx-click` with `params == %{"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
      ...
    end

## Loading 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.
  `field` may be atom or string; `label` is coerced via `to_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` — Optional `phx-target` for LiveComponents.
- `class` — Extra classes on the inner `<form>` element.

## Edge cases handled

- **Atom-or-string inputs**: `sort_by` and `sort_dir` are 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}
    />

# `sort_selector`

## 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 to `nil`.
* `class` (`:string`) - Defaults to `nil`.
* `manual_field` (`:any`) - Atom or string field key that represents "manual" ordering (e.g. `:sort_order`). When `sort_by` matches, the direction toggle is hidden — direction has no meaning for a user-specified order, and the drag handles on each row are themselves the affordance for reordering. Defaults to `nil`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
