# `Cinder.Filter`
[🔗](https://github.com/sevenseacat/cinder/blob/v0.12.1/lib/cinder/filter.ex#L1)

Base behavior for Cinder filter implementations.

Defines the common interface that all filter types must implement,
along with shared types and utility functions.

## Quick Start

The most convenient way to create a custom filter is to use this module:

    defmodule MyApp.Filters.Slider do
      use Cinder.Filter

      @impl true
      def render(column, current_value, theme, assigns) do
        filter_options = Map.get(column, :filter_options, [])
        min_value = get_option(filter_options, :min, 0)
        max_value = get_option(filter_options, :max, 100)
        current = current_value || min_value

        assigns = %{
          column: column,
          current_value: current,
          min_value: min_value,
          max_value: max_value,
          theme: theme
        }

        ~H"""
        <div class="flex flex-col space-y-2">
          <input
            type="range"
            name={field_name(@column.field)}
            value={@current_value}
            min={@min_value}
            max={@max_value}
            phx-debounce="100"
            class={Map.get(@theme, :filter_slider_input_class, "w-full")}
          />
          <output>{@current_value}</output>
        </div>
        """
      end

      @impl true
      def process(raw_value, column) when is_binary(raw_value) do
        case Integer.parse(raw_value) do
          {value, ""} ->
            %{
              type: :slider,
              value: value,
              operator: :less_than_or_equal
            }
          _ -> nil
        end
      end

      def process(_raw_value, _column), do: nil

      @impl true
      def validate(%{type: :slider, value: value, operator: operator})
          when is_integer(value) and is_atom(operator), do: true
      def validate(_), do: false

      @impl true
      def default_options, do: [min: 0, max: 100, step: 1]

      @impl true
      def empty?(value) do
        case value do
          nil -> true
          %{value: nil} -> true
          _ -> false
        end
      end

      @impl true
      def build_query(query, field, filter_value) do
        %{value: value} = filter_value
        field_atom = String.to_atom(field)
        Ash.Query.filter(query, ^ref(field_atom) <= ^value)
      end
    end

Then register it in your configuration:

    config :cinder, :filters, %{
      slider: MyApp.Filters.Slider
    }

And call `Cinder.setup()` in your application start function to register
all configured filters.

This automatically:
- Adds the `@behaviour Cinder.Filter` declaration
- Imports Phoenix.Component for HEEx templates
- Imports helper functions from this module

## Required Callbacks

All custom filters must implement these callbacks:

### render/4

Renders the filter UI component.

    @callback render(column :: map(), current_value :: any(), theme :: map(), assigns :: map()) :: Phoenix.LiveView.Rendered.t()

- `column`: Column configuration with `field`, `label`, `filter_options`
- `current_value`: Current filter value (nil if no filter applied)
- `theme`: Theme configuration with CSS classes
- `assigns`: Additional assigns from the parent component

### process/2

Processes raw form/URL input into structured filter data.

    @callback process(raw_value :: any(), column :: map()) :: map() | nil

Must return a map with `:type`, `:value`, `:operator` keys, or `nil`.

### validate/1

Validates a processed filter value.

    @callback validate(value :: any()) :: boolean()

### default_options/0

Returns default configuration options.

    @callback default_options() :: keyword()

### empty?/1

Determines if a filter value is "empty" (no filtering applied).

    @callback empty?(value :: any()) :: boolean()

### build_query/3

**Critical for functionality!** Builds the Ash query for this filter.

    @callback build_query(query :: Ash.Query.t(), field :: String.t(), filter_value :: map()) :: Ash.Query.t()

## Helper Functions

### field_name/1

Generates proper form field names for filter inputs:

    ~H"""
    <input name={field_name(@column.field)} ... />
    """

### get_option/3

Safely extracts options from filter configuration with defaults:

    filter_options = Map.get(column, :filter_options, [])
    placeholder = get_option(filter_options, :placeholder, "Enter text...")

## Query Building Patterns

### Recommended Approach: Use the Centralized Helper

    def build_query(query, field, filter_value) do
      %{value: value, operator: operator} = filter_value

      # Use the centralized helper which supports direct, relationship, and embedded fields
      Cinder.Filter.Helpers.build_ash_filter(query, field, value, operator)
    end

### Basic Field Filtering

    def build_query(query, field, filter_value) do
      %{value: value} = filter_value
      field_atom = String.to_atom(field)
      Ash.Query.filter(query, ^ref(field_atom) == ^value)
    end

### Relationship Filtering

Handle dot notation fields like "user.name":

    def build_query(query, field, filter_value) do
      %{value: value} = filter_value

      if String.contains?(field, ".") do
        path_atoms = field |> String.split(".") |> Enum.map(&String.to_atom/1)
        {rel_path, [field_atom]} = Enum.split(path_atoms, -1)

        Ash.Query.filter(query, exists(^rel_path, ^ref(field_atom) == ^value))
      else
        field_atom = String.to_atom(field)
        Ash.Query.filter(query, ^ref(field_atom) == ^value)
      end
    end

### Embedded Field Filtering

Handle bracket notation fields like "profile[:first_name]":

    def build_query(query, field, filter_value) do
      %{value: value} = filter_value

      # The centralized helper automatically detects and handles embedded fields
      Cinder.Filter.Helpers.build_ash_filter(query, field, value, :equals)
    end

Supported embedded field notations:
- Basic embedded: `profile[:first_name]`
- Nested embedded: `settings[:address][:street]`
- Mixed relationship + embedded: `user.profile[:first_name]`
- Complex mixed: `company.settings[:address][:city]`

## Best Practices

1. **Always implement build_query/3** - This is what actually filters data
2. **Handle edge cases in process/2** - Return nil for invalid input
3. **Validate filter values** - Check structure and data types
4. **Document your filters** - Include usage examples and options

# `column`

```elixir
@type column() :: map()
```

Column configuration map

# `filter_options`

```elixir
@type filter_options() :: keyword()
```

Filter options keyword list

# `processed_filter_value`

```elixir
@type processed_filter_value() :: map() | nil
```

Processed filter value returned by process/2 and passed to build_query/3

# `raw_filter_value`

```elixir
@type raw_filter_value() :: any()
```

Raw filter value from form/URL input

# `theme`

```elixir
@type theme() :: map()
```

Theme configuration map

# `build_query`

```elixir
@callback build_query(Ash.Query.t(), String.t(), map()) :: Ash.Query.t()
```

Builds query filters for this filter type.

## Parameters
- `query` - The Ash query to modify
- `field` - The field name being filtered
- `filter_value` - The processed filter value (map with :value, :type, etc.)

## Returns
Modified Ash query with the filter applied

# `default_options`

```elixir
@callback default_options() :: filter_options()
```

Returns default options for this filter type.

## Returns
Keyword list of default filter options

# `empty?`

```elixir
@callback empty?(processed_filter_value()) :: boolean()
```

Checks if a filter value is considered empty/inactive.

## Parameters
- `value` - Filter value to check

## Returns
Boolean indicating if the filter should be considered inactive

# `process`

```elixir
@callback process(raw_filter_value(), column()) :: processed_filter_value()
```

Processes raw form input into structured filter value.

## Parameters
- `raw_value` - Raw value from form submission
- `column` - Column definition with filter configuration

## Returns
Structured filter value map or nil if invalid

# `render`

```elixir
@callback render(column(), processed_filter_value(), theme(), map()) ::
  Phoenix.LiveView.Rendered.t()
```

Renders the filter input component for this filter type.

## Parameters
- `column` - Column definition with filter configuration
- `current_value` - Current filter value (processed or nil)
- `theme` - Theme configuration for styling
- `assigns` - Additional assigns (target, filter_values, etc.)

## Returns
HEEx template for the filter input

# `validate`

```elixir
@callback validate(processed_filter_value()) :: boolean()
```

Validates a filter value for this filter type.

## Parameters
- `value` - Filter value to validate

## Returns
Boolean indicating if value is valid

# `field_name`

Generates a form field name for the given column key.

# `filter_id`

Generates a unique HTML ID for a filter input.

When a table_id is provided, it's used as a prefix to ensure uniqueness
when multiple tables exist on the same page.

## Examples

    iex> Cinder.Filter.filter_id("my-table", "name")
    "my-table-filter-name"

    iex> Cinder.Filter.filter_id("users", "user.email")
    "users-filter-user_email"

    iex> Cinder.Filter.filter_id("products", "price", "min")
    "products-filter-price-min"

# `filter_id`

# `get_option`

Gets a nested value from filter options with a default.

# `has_filter_value?`

Checks if a filter has a meaningful value across all filter types.

# `humanize_atom`

Converts an atom to human readable string.

# `humanize_key`

Converts a key to human readable string.

# `merge_options`

Merges default options with provided options.

# `sanitized_field_name`

Sanitizes a field name for use in HTML attributes and CSS selectors.

Replaces all characters that are invalid in CSS selectors with underscores,
keeping only letters, numbers, hyphens, and underscores.

## Examples

    iex> Cinder.Filter.sanitized_field_name("main_unit?")
    "main_unit_"

    iex> Cinder.Filter.sanitized_field_name("user.name")
    "user_name"

    iex> Cinder.Filter.sanitized_field_name("profile[:first_name]")
    "profile__first_name_"

---

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