# `LiveFilter`
[🔗](https://github.com/agoodway/livefilter/blob/v0.2.0/lib/live_filter.ex#L1)

Composable filter UI library for Phoenix LiveView.

Provides builder functions for defining filterable fields that produce
`LiveFilter.FilterConfig` structs.

## Example

    filters = [
      LiveFilter.text(:search, label: "Search", always_on: true, custom_param: "search"),
      LiveFilter.select(:status, label: "Status", options: ~w(pending active shipped)),
      LiveFilter.multi_select(:tags, label: "Tags", options: ~w(urgent bug feature)),
      LiveFilter.date_range(:inserted_at, label: "Created"),
      LiveFilter.boolean(:urgent, label: "Urgent")
    ]

# `async_select`

```elixir
@spec async_select(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates an async select filter config for search-as-you-type selection.

Default operators: `[:eq]`, default: `:eq`.

Requires `:search_fn` and `:load_label_fn` callbacks.

## Callbacks

- `:search_fn` - `fn(query, context) -> [{value, label}]` — called when user types in the dropdown
- `:load_label_fn` - `fn(value, context) -> {:ok, label} | :error` — called to resolve display label from a stored value (e.g., on URL reload)

## Options

- `:min_chars` - Minimum characters before searching (default: 1)
- `:debounce` - Debounce delay in ms (default: 200)
- `:placeholder` - Search input placeholder (default: "Search...")
- `:empty_message` - Message when no results found (default: "No results found")

# `bar`

Convenience function component that renders `LiveFilter.Bar`.

## Options

- `:mode` - Filter display mode: `:basic` (default) or `:command`
- `:theme` - Theme preset: `:default`, `:minimal`, or `:bordered`
- `:variant` - Chip border style: `:outline` (default), `:ghost`, `:soft`, or custom class string

## Example

    <LiveFilter.bar filter={@livefilter} />
    <LiveFilter.bar filter={@livefilter} mode={:command} />
    <LiveFilter.bar filter={@livefilter} mode={:command} theme={:bordered} />
    <LiveFilter.bar filter={@livefilter} variant={:ghost} />

## Attributes

* `filter` (`:map`) (required)
* `mode` (`:atom`) - Defaults to `:basic`.
* `theme` (`:atom`) - Defaults to `:default`.
* `variant` (`:atom`) - Chip border style: :outline (default), :ghost, or :soft. Defaults to `:outline`.

# `boolean`

```elixir
@spec boolean(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a boolean filter config.

Default operators: `[:is]`, default: `:is`.

## Options

- `:true_label` - Label for true state (default: "Yes")
- `:false_label` - Label for false state (default: "No")
- `:any_label` - Label for null/any state (default: "Any")
- `:nullable` - Enable 3-state (true/false/any) mode (default: false)

# `date`

```elixir
@spec date(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a date filter config.

Default operators: `[:eq, :gt, :gte, :lt, :lte]`, default: `:eq`.

# `date_range`

```elixir
@spec date_range(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a date range filter config.

Default operators: `[:gte_lte]`, default: `:gte_lte`.

Use this for `:date` schema fields. For `:utc_datetime` or `:utc_datetime_usec`
fields, use `datetime_range/2` instead.

# `datetime`

```elixir
@spec datetime(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a datetime filter config.

Default operators: `[:eq, :gt, :gte, :lt, :lte]`, default: `:eq`.

## Options

- `:time_format` - Time display format: `:twelve_hour` (default) or `:twenty_four_hour`
- `:minute_step` - Minute increment step: 1 (default), 5, 15, or 30

# `datetime_range`

```elixir
@spec datetime_range(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a datetime range filter config.

Default operators: `[:gte_lte]`, default: `:gte_lte`.

Similar to `date_range/2` but generates full ISO8601 datetime values with
start-of-day (00:00:00Z) and end-of-day (23:59:59Z) times. Use this for
`:utc_datetime` or `:utc_datetime_usec` schema fields.

# `from_params`

```elixir
@spec from_params(map(), [LiveFilter.FilterConfig.t()]) ::
  {[LiveFilter.Filter.t()], map()}
```

Parses URL params into filter state using the provided config.

Returns `{filters, remaining_params}`.

# `init`

```elixir
@spec init(
  Phoenix.LiveView.Socket.t(),
  [LiveFilter.FilterConfig.t()],
  [LiveFilter.Filter.t()],
  keyword()
) :: Phoenix.LiveView.Socket.t()
```

Initializes LiveFilter state on a socket.

Assigns `:livefilter` with config, filters, and a unique ID.
Accepts an optional `context:` keyword that is stored and passed to async_select callbacks.

# `multi_select`

```elixir
@spec multi_select(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a multi-select filter config.

Default operators: `[:cs, :ov]`, default: `:cs`.
Requires `:options` or `:options_fn`.

Note: Uses array containment operators for Postgres array columns:
- `:cs` - contains (`@>`) - array contains all selected values
- `:ov` - overlaps (`&&`) - array contains any of the selected values

# `number`

```elixir
@spec number(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a number filter config.

Default operators: `[:eq, :neq, :gt, :gte, :lt, :lte]`, default: `:eq`.

# `pagination_from_params`

```elixir
@spec pagination_from_params(
  map(),
  keyword()
) :: {LiveFilter.Pagination.t(), map()}
```

Parses pagination from URL params using PostgREST `limit`/`offset` parameters.

Returns `{pagination, remaining_params}`.

## Options

  * `:default_limit` - Default items per page (default: 25)
  * `:max_limit` - Maximum allowed limit value (default: 100)
  * `:limit_options` - Available per-page options for UI dropdown (default: [10, 25, 50, 100])

## Example

    {pagination, remaining} = LiveFilter.pagination_from_params(params, default_limit: 25)
    # pagination.limit => 25
    # pagination.offset => 0

    # With URL ?limit=50&offset=100
    {pagination, remaining} = LiveFilter.pagination_from_params(%{"limit" => "50", "offset" => "100"})
    # pagination.limit => 50
    # pagination.offset => 100

# `paginator`

Convenience function component that renders `LiveFilter.Paginator`.

## Example

    <LiveFilter.paginator pagination={@pagination} />
    <LiveFilter.paginator pagination={@pagination} class="mt-4" />

## Attributes

* `pagination` (`LiveFilter.Pagination`) (required)
* `class` (`:string`) - Defaults to `""`.

# `radio_group`

```elixir
@spec radio_group(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a radio group filter config.

Default operators: `[:eq]`, default: `:eq`.
Requires `:options` or `:options_fn`.

## Options

- `:style` - Display style: `:pills` (default) or `:radios`
- `:inline_threshold` - Max options to show inline as pills (default: 4)
- `:options` - List of options (required)

# `select`

```elixir
@spec select(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a single-select filter config.

Default operators: `[:eq, :neq]`, default: `:eq`.
Requires `:options` or `:options_fn`.

# `sort_field`

```elixir
@spec sort_field(
  atom(),
  keyword()
) :: LiveFilter.SortField.t()
```

Builds a `LiveFilter.SortField` declaration for the sort UI + parse allow-list.

## Options
  * `:label` - display label (default: capitalized field name)
  * `:query_field` - the DB column to order by (default: the field name)
  * `:default_direction` - first-click direction, `:asc` (default) or `:desc`
  * `:nulls` - default nulls placement: `nil` (default), `:first`, or `:last`

# `sort_from_params`

```elixir
@spec sort_from_params(map(), [LiveFilter.SortField.t()], keyword()) ::
  {LiveFilter.Sort.t(), map()}
```

Parses the PostgREST `order=` param into a `LiveFilter.Sort`, validated against
the given `sortable_fields` (a list of `LiveFilter.SortField`).

Returns `{sort, remaining_params}`. Unknown fields and malformed tokens are
dropped (allow-list = injection + dynamic-column safety). When `order` is
absent or every entry is invalid, `opts[:default]` (a `%LiveFilter.Sort{}`) is
used, defaulting to an empty sort.

## Example

    {sort, remaining} =
      LiveFilter.sort_from_params(params, sortable_fields, default: default_sort())

# `sort_header`

Convenience wrapper for `LiveFilter.SortComponents.sort_header/1`.

## Attributes

* `field` (`:atom`) (required)
* `label` (`:string`) (required)
* `sort` (`LiveFilter.Sort`) (required)
* `sortable_fields` (`:list`) - Defaults to `[]`.
* `event` (`:string`) - Defaults to `"lf_sort"`.
* `target` (`:any`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block`

# `sort_menu`

Convenience wrapper for `LiveFilter.SortComponents.sort_menu/1`.

## Attributes

* `sortable_fields` (`:list`) (required)
* `sort` (`LiveFilter.Sort`) (required)
* `id` (`:string`) - Defaults to `"lf-sort-menu"`.
* `event` (`:string`) - Defaults to `"lf_sort_to"`.
* `target` (`:any`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.

# `text`

```elixir
@spec text(
  atom(),
  keyword()
) :: LiveFilter.FilterConfig.t()
```

Creates a text filter config.

Default operators: `[:ilike, :eq, :neq, :like]`, default: `:ilike`.

# `to_path`

```elixir
@spec to_path(String.t(), map()) :: String.t()
```

Builds a path with raw (non-encoded) query string from a params map.

Use this instead of `~p"/path?#{params}"` to avoid percent-encoding
special characters like `{`, `}`, and `,`.

## Example

    # In handle_info:
    path = LiveFilter.to_path("/tasks", all_params)
    {:noreply, push_patch(socket, to: path)}

    # Produces: /tasks?tags=ov.{testing,bug}
    # Instead of: /tasks?tags=ov.%7Btesting%2Cbug%7D

---

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