Dynamic filters
The Dynamic filters make it so easy to add multiple, composable, and dynamic filters to the Index
view.
The first thing you need to do is add the filterable: true
attribute to the fields you need to filter through. We use ransack
behind the scenes so it's essential to configure the ransackable_attributes
list to ensure that every filterable field is incorporated within it.
Filter Combination Logic
When multiple filters are applied:
- Filters on the same attribute are combined using OR conditions
- Filters on different attributes are combined using AND conditions
For example, if you have two filters on the name
field (one for "John" and one for "Jane"), the query will find records where the name is either "John" OR "Jane". However, if you have one filter on name
for "John" and another on status
for "active", the query will find records where the name is "John" AND the status is "active".
class Avo::Resources::Project < Avo::BaseResource
def fields
field :name, as: :text
field :status, as: :status, filterable: true
field :stage, as: :badge, filterable: true
field :country, as: :country, filterable: true
end
end
Authorize ransackable_attributes
class Project < ApplicationRecord
def self.ransackable_attributes(auth_object = nil)
["status", "stage", "country"] # the array items should be strings not symbols
end
end
# Or authorize ALL attributes at once
class Project < ApplicationRecord
def self.ransackable_attributes(auth_object = nil)
authorizable_ransackable_attributes
end
end
WARNING
Ensure the array items are strings, not symbols.
This will make Avo add this new "Filters" button to the Index
view of your resource.
When the user clicks the button, a new filters bar will appear below enabling them to add filters based on the attributes you marked as filterable. The user can add multiple filters for the same attribute if they desire so.
Filter types
The filter type determines the kind of input provided by the filter.
For instance, a text type filter will render a text input field, while a select type filter will render a dropdown menu with predefined options fetched from the field.
Conditions
Each filter type also offers a different set of conditions. Conditions specify how the input value should be applied to filter the data. For example, text filters have conditions such as Contains
or Starts with
, while number filters include =
(equals) or >
(greater than).
Query
Avo uses the input value and the specified condition to build a Ransack query. The filter conditions and input values are translated into Ransack predicates, which are then used to fetch the filtered data.
For instance, in the text filter example above, the Contains
condition and the input value John
are translated into a Ransack query resulting into the SQL LIKE
operator to find all records where the name contains John
.
-> Boolean
Conditions
- Is true
- Is false
- Is null
- Is not null
{
is_true: "Is true",
is_false: "Is false",
is_null: "Is null",
is_not_null: "Is not null",
}.invert


Test it on avodemo, check the source code
-> Date
Conditions
- Is
- Is not
- Is on or before
- Is on or after
- Is within
- Is null
- Is not null
{
is: "Is",
is_not: "Is not",
lte: "Is on or before",
gte: "Is on or after",
is_within: "Is within",
is_null: "Is null",
is_not_null: "Is not null",
}.invert


Test it on avodemo, check the source code
-> Number
Conditions
=
(equals)!=
(is different)>
(greater than)>=
(greater than or equal to)<
(lower than)<=
(lower than or equal to)- Is within Since v3.10.11
- Is null
- Is not null
{
is: "=",
is_not: "!=",
gt: ">",
gte: ">=",
lt: "<",
lte: "<=",
is_within: "Is within",
is_null: "Is null",
is_not_null: "Is not null",
}.invert


Test it on avodemo, check the source code
-> Select
Conditions
- Is
- Is not
- Is null
- Is not null
{
is: "Is",
is_not: "Is not",
is_null: "Is null",
is_not_null: "Is not null",
}.invert


Test it on avodemo, check the source code
-> Text
Conditions
- Contains
- Does not contain
- Is
- Is not
- Starts with
- Ends with
- Is null
- Is not null
- Is present
- Is blank
{
contains: "Contains",
does_not_contain: "Does not contain",
is: "Is",
is_not: "Is not",
starts_with: "Starts with",
ends_with: "Ends with",
is_null: "Is null",
is_not_null: "Is not null",
is_present: "Is present",
is_blank: "Is blank",
}.invert


Test it on avodemo, check the source code
-> Tags
Conditions
- Are
- Contain
- Overlap
- Contained in (
active_record_extended
gem required)
{
array_is: "Are",
array_contains: "Contain",
array_overlap: "Overlap",
array_contained_in: "Contained in" # (active_record_extended gem required)
}.invert
WARNING
Contained in will not work when using the acts-as-taggable-on
gem.


Test it on avodemo, check the source code
INFO
The source code uses custom dynamic filters DSL available Since v3.10.0
Check how to do a more advanced configuration on the custom dynamic filters section.
Options
You can have a few customization options available that you can add in your avo.rb
initializer file.
Avo.configure do |config|
# Other Avo configurations
end
if defined?(Avo::DynamicFilters)
Avo::DynamicFilters.configure do |config|
config.button_label = "Advanced filters"
config.always_expanded = true
end
end
-> button_label
This will change the label on the expand label.
-> always_expanded
You may opt-in to have them always expanded and have the button hidden.
Field to filter matching
On versions lower than 3.10.0 the filters are not configurable so each field will have a dedicated filter type. Check how to do a more advanced configuration on the custom dynamic filters section.
Field-to-filter matching in versions lower than 3.10.0:
def field_to_filter(type)
case type.to_sym
when :boolean
:boolean
when :date, :date_time, :time
:date
when :id, :number, :progress_bar
:number
when :select, :badge, :country, :status
:select
when :text, :textarea, :code, :markdown, :password, :trix
:text
else
:text
end
end
Caveats
At some point we'll integrate the Basic filters into the dynamic filters bar. Until then, if you have both basic and dynamic filters on your resource you'll have two Filters
buttons on your Index
view.
To mitigate that you can toggle the always_expanded
option to true.
Custom Dynamic Filters
Beta Since v3.10.0Dynamic filters are great but strict, as each field creates a specific filter type, each with its own icon and query. The query remains static, targeting only that particular field. Since version 3.10.0, dynamic filters have become customizable and, even better, can be declared without being bound to a field.
There are two ways to define custom dynamic filters: the field's filterable
option and the dynamic_filter
method.
Defining custom dynamic filters
To start customizing a dynamic filter from the filterable
option, change its value to a hash:
field :first_name,
as: :text,
filterable: true
filterable: { }
From this hash, you can configure several options specified below.
Alternatively, you can define a custom dynamic filter using the dynamic_filter
method, which should be called inside the filters
method:
def filters
# ...
dynamic_filter :first_name
# ...
end
Each option specified below can be used as a key in the hash definition or as a keyword argument in the method definition.
Filters order
The filter order is computed. Dynamic filters defined by the dynamic_filter
method will respect the definition order and will be rendered first in the filter list. Filters declared using the field's filterable
option will be sorted by label.
Custom Dynamic Filter IDs
When using a custom dynamic filter, the generated filter ID may not directly correspond to a database column. In such cases, you should use the query_attributes
option to specify which database columns the filter should apply to.
For example, consider a City
model with a population
column in the database:
# The filter ID is custom_population
# However, the filter should apply the query to the population attribute.
dynamic_filter :custom_population, query_attributes: :population
-> label
-> icon
Customize filter's icon. Check icons documentation
Default value
Boolean filter - heroicons/outline/check-circle
Calendar filter - heroicons/outline/calendar-days
Number filter - heroicons/outline/hashtag
Select filter - heroicons/outline/arrow-down-circle
Tags filter - heroicons/outline/tag
Text filter - avo/font
Possible values
-> type
Customize filter's type
Default value
Computed from field using field_to_filter
method.
Possible values
-> query
INFO
Since v3.11.8 the default filtering system is no longer applied when aquery
is specified on a dynamic filter.Customize filter's query
Default value
Applies the condition to the field's attribute. For example, if the field is first_name
, the condition is contains
, and the value is Bill
, the query will restrict to all records where the first name contains Bill
.
Possible values
Any lambda function.
Within the function, you have access to query
and filter_param
as well as all attributes of Avo::ExecutionContext
.
filter_param
is an Avo object that stores the filter's id
, the applied condition
and the value
.
Usage example:
# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
conditions: {
case_sensitive: "Is (case sensitive)",
not_case_sensitive: "Is (case insensitive)"
}.invert,
query: -> {
case filter_param.condition.to_sym
when :case_sensitive
query.where("name = ?", filter_param.value)
when :not_case_sensitive
query.where("LOWER(name) = ?", filter_param.value.downcase)
end
}
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
conditions: {
case_sensitive: "Is (case sensitive)",
not_case_sensitive: "Is (case insensitive)"
}.invert,
query: -> {
case filter_param.condition.to_sym
when :case_sensitive
query.where("name = ?", filter_param.value)
when :not_case_sensitive
query.where("LOWER(name) = ?", filter_param.value.downcase)
end
}
-> conditions
Customize filter's conditions
Default value
Check default conditions for each filter type above on this page.
Possible values
- A hash with the desired key-values to customize available conditions
- An empty hash
{}
to hide conditions dropdown and use the first default condition
Usage examples
Custom conditions
# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
conditions: {
case_sensitive: "Case sensitive",
not_case_sensitive: "Not case sensitive"
}.invert
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
conditions: {
case_sensitive: "Case sensitive",
not_case_sensitive: "Not case sensitive"
}.invert
Hide conditions dropdown
When set to an empty hash ({}
), this option hides the conditions dropdown and automatically applies the first default condition for each filter type. This is particularly useful when you want to simplify the filter interface by removing the conditions selection, especially for filters where only one condition makes sense.
dynamic_filter :last_name,
type: :select,
conditions: {},
options: User.pluck(:last_name).compact
field :department,
as: :select,
filterable: {
conditions: {},
type: :select,
options: ["Engineering", "Marketing", "Sales", "Support"]
}
INFO
When conditions: {}
is used, the filter will automatically use the first condition from the default conditions list for that filter type. For example, a select filter will use "Is" condition, and a text filter will use "Contains" condition.
-> query_attributes
Customize filter's query attributes
Default value
Field's / filter's id
Possible values
Any model DB column(s). Use an array of symbols for multiple columns or a single symbol for a single column. If your model has DB columns like first_name
and last_name
, you can combine both on a single filter:
# Using field's filterable option
field :name,
as: :text,
filterable: {
# ...
query_attributes: [:first_name, :last_name]
# ...
}
# Using dynamic_filter method
dynamic_filter :name,
type: :text,
query_attributes: [:first_name, :last_name]
You can also add query attributes for a belongs_to
association. For example, with a model that belongs to User
:
# Using field's filterable option
field :user,
as: :belongs_to,
filterable: {
label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
}
# Using dynamic_filter method
dynamic_filter label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
This is possible due to a Ransack feature. To use it, you need to add the association name before the attribute.
-> suggestions
Suggestions work on filters that provide text input, enhancing the user experience by offering relevant options. This functionality is especially useful in scenarios where users might need guidance or where the filter values are numerous or complex.
Default value
nil
Possible values
- Array of strings
# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
suggestions: ["Avo", "Cado"]
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
suggestions: ["Avo", "Cado"]
- Proc that returns an array of strings
parent_record
becomes accessible within the suggestions
block.# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
suggestions: -> { ["Avo", "Cado", params[:extra_suggestion]] }
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
suggestions: -> { ["Avo", "Cado", params[:extra_suggestion]] }
- Array of hashes with the keys
value
,label
and optionally anavatar
Applicable only to filters with type tags.
# Using field's filterable option
field :tags,
as: :tags,
filterable: {
# ...
suggestions: [
{
value: 1,
label: 'one',
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
# ...
}
# Using dynamic_filter method
dynamic_filter :tags,
suggestions: [
{
value: 1,
label: 'one',
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
# Using field's filterable option
field :tags,
as: :tags,
filterable: {
# ...
suggestions: -> {
[
{
value: 1,
label: 'one', # or params[:something]
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
}
# ...
}
# Using dynamic_filter method
dynamic_filter :tags,
suggestions: -> {
[
{
value: 1,
label: 'one', # or params[:something]
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
}
-> fetch_values_from
WARNING
This option is compatible only with tags
filters.
In some cases, you may need to retrieve values dynamically from an API. The fetch_values_from
option allows you to provide a URL from which the filter will suggest values, functioning similarly to the fetch_values_from
option in the tags field.
When a user searches for a record, the filter's input will send a request to the server to fetch records that match the query.
Default value
nil
INFO
If you're using a filterable
field the fetch_values_from
are fetched from the field.
field :tags, as: :tags,
fetch_values_from: -> { "/avo-filters/resources/cities/tags" }
filterable: true
Possible values
- String
fetch_values_from: "/avo-filters/resources/cities/tags"
- Proc that evaluates to a string.
fetch_values_from: -> { "/avo-filters/resources/cities/tags" }
The endpoint should handle two different scenarios:
- Search functionality: When a user types in the filter input, the endpoint receives the user input as
q
in the params (params["q"]
) - Initial load: When the filter already has selected values (like on page load), the endpoint receives an array of values in
params[:value]
to fetch the corresponding labels
The endpoint should return an array of objects with the keys value
, label
and optionally avatar
.
class Avo::CitiesController < Avo::ResourcesController
def tags
if params[:value].present?
# Handle initial load: return labels for selected values
# params[:value] contains an array of selected values
selected_cities = City.where(id: params[:value])
render json: selected_cities.map do |city|
{
value: city.id,
label: city.name,
avatar: city.avatar_url
}
end
elsif params["q"].present?
# Handle search: return cities matching the query
cities = City.where("name ILIKE ?", "%#{params["q"]}%").limit(10)
render json: cities.map do |city|
{
value: city.id,
label: city.name,
avatar: city.avatar_url
}
end
else
# Handle empty state: return some default suggestions
render json: [
{
value: 1,
label: "New York",
avatar: "https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop"
}
]
end
end
end
Rails.application.routes.draw do
# your routes...
end
if defined? ::Avo
Avo::Engine.routes.draw do
scope :resources do
get "cities/tags", to: "cities#tags"
end
end
end
-> options
Customize the options for select type filters. This is available only for select type filters and determines the options visible in the select dropdown.
Default value
Fetched from field if bond to a field or []
Possible values
An array or hash where the key-value pairs represent the options.
- If a hash is provided, the key is the option label and the value is the option value.
- If an array is provided, the array elements are used as both the option value and the option label.
Usage examples
Array
dynamic_filter :version,
type: :select,
options: ["Label 1", "Label 2"]
Hash (with invert)
dynamic_filter :version,
type: :select,
options: {
value_1: "Label 1",
value_2: "Label 2"
}.invert
Hash (without invert)
dynamic_filter :version,
type: :select,
options: {
"Label 1" => :value_1,
"Label 2" => :value_2
}
-> render_apply_button
Controls whether the "Apply" button should be rendered in the filter interface.
Default value
true
Possible values
Boolean value (true
or false
).
When set to false
, the apply button will be hidden from the filter interface. This is particularly useful when combined with apply_on_select: true
to create an immediate filtering experience.
Usage examples
dynamic_filter :status,
type: :select,
render_apply_button: false
field :status,
as: :select,
filterable: {
render_apply_button: false,
apply_on_select: true,
options: ["active", "inactive", "pending"]
}
-> apply_on_select
Controls whether the filter should be applied immediately when the selected value changes, without requiring the user to click the "Apply" button.
Default value
false
Possible values
Boolean value (true
or false
).
When set to true
, the filter will automatically apply as soon as the user selects or changes a value. This creates a more responsive user experience, especially when combined with render_apply_button: false
.
Usage examples
dynamic_filter :category,
type: :select,
apply_on_select: true,
render_apply_button: false
field :priority,
as: :select,
filterable: {
apply_on_select: true,
render_apply_button: false,
options: ["high", "medium", "low"]
}
Guides & Tutorials
-> How to filter associations
Learn how to effectively filter records based on their associations in Avo. This video tutorial demonstrates how to set up and use dynamic filters to query records through the attributes of their associations, enabling powerful and flexible data filtering capabilities.
belongs_to
example
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# Using field's filterable option
def fields
field :user,
as: :belongs_to,
filterable: {
label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
}
end
# OR using dynamic_filter method
def filters
dynamic_filter label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
end
end
has_many
example
class Avo::Resources::Author < Avo::BaseResource
self.record_selector = false
def fields
field :preview, as: :preview
field :book_list, only_on: :preview do
tag.div do
tag.ul do
safe_join(
record.books.map do |book|
tag.li("#{book.title} (#{book.genre})")
end
)
end
end
end
field :name, filterable: true
# Filter the books by title and genre
field :books, as: :has_many, filterable: {
query_attributes: [:books_title, :books_genre]
}
end
end