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
Create a custom filter by using 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 = if current_value in [nil, ""], do: min_value, else: current_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
Cinder.Filter.Helpers.build_ash_filter(query, field, value, :less_than_or_equal)
end
endThen register it in your configuration:
config :cinder, :filters, slider: MyApp.Filters.SliderThis automatically:
- Adds the
@behaviour Cinder.Filterdeclaration - 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 withfield,label,filter_optionscurrent_value: The:valuefromprocess/2, or an empty value (like"") when the filter isn't in usetheme: Theme configuration with CSS classesassigns: 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() | nilMust 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
Adds the filter to the Ash query. This is what filters the records.
@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)
endField Filtering
Cinder.Filter.Helpers.build_ash_filter/4 builds the filter for any field notation:
direct fields, relationships ("user.name") and embedded fields (below), with
:equals, :contains, :starts_with, :ends_with, :in, and the comparisons
(:greater_than, :less_than_or_equal and so on):
def build_query(query, field, filter_value) do
%{value: value} = filter_value
Cinder.Filter.Helpers.build_ash_filter(query, field, value, :equals)
endPrefer it to building ref/1 expressions from field yourself: it handles every
notation, and never turns a name into a new atom (String.to_atom/1 on names isn't
safe in general, since atoms are never garbage collected).
Embedded Field Filtering
Embedded fields use double-underscore notation, e.g. "profile__first_name". This is the
same notation you write in column definitions, so build_query/3 receives field exactly
as the column declared it:
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)
endSupported 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
- Always implement build_query/3 - This is what actually filters data
- Handle edge cases in process/2 - Return nil for invalid input
- Validate filter values - Check structure and data types
- Document your filters - Include usage examples and options
Summary
Types
Column configuration map
Filter options keyword list
Processed filter value returned by process/2 and passed to build_query/3
Raw filter value from form/URL input
Theme configuration map
Callbacks
Builds query filters for this filter type.
Returns default options for this filter type.
Checks if a filter value is considered empty/inactive.
Processes raw form input into structured filter value.
Renders the filter input component for this filter type.
Validates a filter value for this filter type.
Functions
Generates a form field name for the given column key.
Generates a unique HTML ID for a filter input.
Gets a nested value from filter options with a default.
Checks if a filter has a meaningful value across all filter types.
Converts an atom to human readable string.
Converts a key to human readable string.
Merges default options with provided options.
Sanitizes a field name for use in HTML attributes and CSS selectors.
Types
@type column() :: map()
Column configuration map
@type filter_options() :: keyword()
Filter options keyword list
@type processed_filter_value() :: map() | nil
Processed filter value returned by process/2 and passed to build_query/3
@type raw_filter_value() :: any()
Raw filter value from form/URL input
@type theme() :: map()
Theme configuration map
Callbacks
@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 modifyfield- The field name being filteredfilter_value- The processed filter value (map with :value, :type, etc.)
Returns
Modified Ash query with the filter applied
@callback default_options() :: filter_options()
Returns default options for this filter type.
Returns
Keyword list of default filter options
@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
@callback process(raw_filter_value(), column()) :: processed_filter_value()
Processes raw form input into structured filter value.
Parameters
raw_value- Raw value from form submissioncolumn- Column definition with filter configuration
Returns
Structured filter value map, or nil if the value can't be used
@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 configurationcurrent_value- Current filter value (processed or nil)theme- Theme configuration for stylingassigns- Additional assigns (target, filter_values, etc.)
Returns
HEEx template for the filter input
@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
Functions
Generates a form field name for the given column key.
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"
Gets a nested value from filter options with a default.
Checks if a filter has a meaningful value across all filter types.
Converts an atom to human readable string.
Converts a key to human readable string.
Merges default options with provided options.
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"