Filters
Filters allow you to better scope the index queries for records you are looking for.
Each filter is configured in a class with a few dedicated methods and options. To use a filter on a resource you must register it and it will be displayed on the Index
view.
Filter options
-> self.name
self.name
is what is going to be displayed to the user in the filters panel.
-> self.visible
You may want to show/hide the filter in some scenarios. You can do that using the self.visible
attribute.
Inside the visible block you can acces the following variables and you should return a boolean (true
/false
).
self.visible = -> do
# You have access to:
# block
# context
# current_user
# params
# parent_model
# parent_resource
# resource
# view
# view_context
true
end
-> self.empty_message
More on this in the Empty message guide.
-> options
In the options
method you have access to the request
, params
, context
, view_context
, and current_user
objects.
-> apply
Index
view. It recieves the request
form which you can get all the params
if you need them, it gets the query
which is the query Avo made to fetch the records. It's a regular Active Record which you can manipulate.
It also receives the values
variable which holds the actual choices the user made on the front-end for the options you set.
-> default
In the default
method you have access to the request
, params
, context
, view_context
, and current_user
objects.
-> react
More on this in the React to filters guide
Register filters
In order to use a filter you must register it on a Resource
using the filter
method inside the filters
method.
class Avo::Resources::Post < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
end
def filters
filter Avo::Filters::Published
end
end
Filter types
Avo has several types of filters available Boolean filter, Select filter, Multiple select filter, Text filter and since version 3.11.8 Date time filter.
Filter values
Because the filters get serialized back and forth, the final value
/values
in the apply
method will be stringified or have the stringified keys if they are hashes. You can declare them as regular hashes in the options
method, but they will get stringified.
-> Boolean Filter
The boolean filter is a filter where the user can filter the records using one or more checkboxes.
To generate one run:
bin/rails generate avo:filter featured
or
bin/rails generate avo:filter featured --type boolean
Here's a sample filter
class Avo::Filters::Featured < Avo::Filters::BooleanFilter
self.name = 'Featured filter'
# `values` comes as a hash with stringified keys
# Eg:
# {
# 'is_featured': true
# }
def apply(request, query, values)
return query if values['is_featured'] && values['is_unfeatured']
if values['is_featured']
query = query.where(is_featured: true)
elsif values['is_unfeatured']
query = query.where(is_featured: false)
end
query
end
def options
{
is_featured: "Featured",
is_unfeatured: "Unfeatured"
}
end
# Optional method to set the default state.
# def default
# {
# is_featured: true
# }
# end
end
Each filter file comes with a name
, apply
, and options
methods.
The name
method lets you set the name of the filter.
The apply
method is responsible for filtering out the records by giving you access to modify the query
object. The apply
method also gives you access to the current request
object and the passed values
. The values
object is a Hash
containing all the configured options
with the option name as the key and true
/false
as the value.
# Example values payload
{
'is_featured': true,
'is_unfeatured': false,
}
The options
method defines the available values of your filter. They should return a Hash
with the option id as a key and option label as value.
Default value
You can set a default value to the filter, so it has a predetermined state on load. To do that, return the state you desire from the default
method.
class Avo::Filters::Featured < Avo::Filters::BooleanFilter
self.name = 'Featured status'
def apply(request, query, values)
return query if values['is_featured'] && values['is_unfeatured']
if values['is_featured']
query = query.where(is_featured: true)
elsif values['is_unfeatured']
query = query.where(is_featured: false)
end
query
end
def options
{
is_featured: "Featured",
is_unfeatured: "Unfeatured"
}
end
def default
{
is_featured: true
}
end
end
-> Select Filter
Select filters are similar to Boolean ones but they give the user a dropdown with which to filter the values.
rails generate avo:filter published --type select
The most significant difference from the Boolean filter is in the apply
method. You only get back one value
attribute, which represents which entry from the options
method is selected.
A finished, select filter might look like this.
class Avo::Filters::Published < Avo::Filters::SelectFilter
self.name = 'Published status'
# `value` comes as a string
# Eg: 'published'
def apply(request, query, value)
case value
when 'published'
query.where.not(published_at: nil)
when 'unpublished'
query.where(published_at: nil)
else
query
end
end
def options
{
published: "Published",
unpublished: "Unpublished"
}
end
# Optional method to set the default state.
# def default
# :published
# end
end
Default value
The select filter supports setting a default too. That should be a string or symbol with the select item. It will be stringified by Avo automatically.
class Avo::Filters::Published < Avo::Filters::SelectFilter
self.name = 'Published status'
def apply(request, query, value)
case value
when 'published'
query.where.not(published_at: nil)
when 'unpublished'
query.where(published_at: nil)
else
query
end
end
def options
{
'published': 'Published',
'unpublished': 'Unpublished',
}
end
def default
:published
end
end
-> Multiple select filter
You may also use a multiple select filter.
rails generate avo:filter post_status --type multiple_select
class Avo::Filters::PostStatus < Avo::Filters::MultipleSelectFilter
self.name = "Status"
# `value` comes as an array of strings
# Ex: ['admins', 'non_admins']
def apply(request, query, value)
if value.include? 'admins'
query = query.admins
end
if value.include? 'non_admins'
query = query.non_admins
end
query
end
def options
{
admins: "Admins",
non_admins: "Non admins",
}
end
# Optional method to set the default state.
# def default
# ['admins', 'non_admins']
# end
end
Dynamic options
The select filter can also take dynamic options:
class Avo::Filters::Author < Avo::Filters::SelectFilter
self.name = 'Author'
def apply(request, query, value)
query = query.where(author_id: value) if value.present?
query
end
# Example `applied_filters`
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def options
# Here you have access to the `applied_filters` object too
Author.select(:id, :name).each_with_object({}) { |author, options| options[author.id] = author.name }
end
end
-> Text Filter
You can add complex text filters to Avo using the Text filter
rails generate avo:filter name --type text
class Avo::Filters::Name < Avo::Filters::TextFilter
self.name = "Name filter"
self.button_label = "Filter by name"
# `value` comes as text
# Eg: 'avo'
def apply(request, query, value)
query.where('LOWER(name) LIKE ?', "%#{value}%")
end
# def default
# 'avo'
# end
end
-> Date time Filter
The ideal filter for date selection. This filter allows you to generate a date input, with options to include time selection and even a range selection mode. Customizable to suit your specific needs.
Timezone Handling
This filter sends the selected value exactly as selected, without any timezone adjustments. If you need to apply timezone conversion or adjustments, please ensure to handle it during the apply
method.
Generate one by using:
rails generate avo:filter created_at --type date_time
The generated file should be following a similar format:
# frozen_string_literal: true
class Avo::Filters::CreatedAt < Avo::Filters::DateTimeFilter
self.name = "Created at"
# self.type = :date_time
# self.mode = :range
# self.visible = -> do
# true
# end
def apply(request, query, value)
query
end
# def format
# case type
# when :date_time
# 'yyyy-LL-dd TT'
# when :date
# 'yyyy-LL-dd'
# end
# end
# def picker_format
# case type
# when :date_time
# 'Y-m-d H:i:S'
# when :time
# 'Y-m-d'
# end
# end
end
Type
Determines the format of the input field.
Default value
:date_time
By default, the input allows users to select both a date and a time.
Possible values
:date
- This option restricts the input to date selection only, ideal for scenarios where time input is unnecessary.
:time
- This option limits the input to time selection only, suitable to apply where only the time is relevant.
:date_time
- This combined option enables both date and time selection, providing a comprehensive input for more detailed needs.
Mode
Defines whether the input allows selection of a single date or a range of dates.
Default value
:range
By default, the input permits users to select a range of dates, ideal for scenarios such as booking periods or event durations.
Possible values
:range
- Allows users to choose a start and end date, making it suitable for applications that require a time span, such as reservations or scheduling.
INFO
In
:range
mode thevalue
will be formatted as"2024-08-13 to 2024-08-16"
.To separate the start and end dates, use
date_1, date_2 = value.split(" to ")
, which will split the value into["2024-08-13", "2024-08-16"]
:single
- Limits the selection to a single date, perfect for use cases where only one specific day needs to be selected, such as an appointment or event date.
picker_options
This filter uses flatpickr as the date and time picker. If you wish to customize the picker’s options, you can do so by overriding the picker_options(value)
method. You can merge your custom options with those provided by flatpickr, which are detailed here.
# frozen_string_literal: true
class Avo::Filters::StartingAt < Avo::Filters::DateTimeFilter
self.name = "The starting at filter"
self.button_label = "Filter by start time"
self.empty_message = "Search by start time"
self.type = :time
self.mode = :single
def picker_options(value)
super.merge({
minuteIncrement: 3
})
end
def apply(request, query, value)
query.where("to_char(starting_at, 'HH24:MI:SS') = ?", value)
end
end
Dynamic filter options
Watch the demo videoYou might want to compose more advanced filters, like when you have two filters, one for the country and another for cities, and you'd like to have the cities one populated with cities from the selected country.
Let's take the Avo::Resources::Course
as an example.
# app/models/course.rb
class Course < ApplicationRecord
def self.countries
["USA", "Japan", "Spain", "Thailand"]
end
def self.cities
{
USA: ["New York", "Los Angeles", "San Francisco", "Boston", "Philadelphia"],
Japan: ["Tokyo", "Osaka", "Kyoto", "Hiroshima", "Yokohama", "Nagoya", "Kobe"],
Spain: ["Madrid", "Valencia", "Barcelona"],
Thailand: ["Chiang Mai", "Bangkok", "Phuket"]
}
end
end
We will create two filters—one for choosing countries and another for cities.
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
def filters
filter Avo::Filters::CourseCountryFilter
filter Avo::Filters::CourseCityFilter
end
end
The country filter is pretty straightforward. Set the query so the country
field to be one of the selected countries and the options
are the available countries as Hash
.
# app/avo/filters/course_country.rb
class Avo::Filters::CourseCountry < Avo::Filters::BooleanFilter
self.name = "Course country filter"
def apply(request, query, values)
query.where(country: values.select { |country, selected| selected }.keys)
end
def options
Course.countries.map { |country| [country, country] }.to_h
end
end
The cities filter has a few more methods to manage the data better, but the gist is the same. The query
makes sure the records have the city value in one of the cities that have been selected.
The options
method gets the selected countries from the countries filter (Avo::Filters::CourseCountryFilter
) and formats them to a Hash
.
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
cities_for_countries countries
end
private
# Get a hash of cities for certain countries
# Example payload:
# countries = ["USA", "Japan"]
def cities_for_countries(countries_array = [])
countries_array
.map do |country|
# Get the cities for this country
Course.cities.stringify_keys[country]
end
.flatten
# Prepare to transform to a Hash
.map { |city| [city, city] }
# Turn to a Hash
.to_h
end
# Get the value of the selected countries
# Example payload:
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["Avo::Filters::CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_filters["Avo::Filters::CourseCountryFilter"]
# Keep only the ones selected
.select { |country, selected| selected }
# Pluck the name of the coutnry
.keys
else
# Return empty array
[]
end
end
end
The countries
method above will check if the Avo::Filters::CourseCountryFilter
has anything selected. If so, get the names of the chosen ones. This way, you show only the cities from the selected countries and not all of them.
React to filters
Watch the demo videoGoing further with the example above, a filter can react to other filters. For example, let's say that when a user selects USA
from the list of countries, you want to display a list of cities from the USA (that's already happening in options
), and you'd like to select the first one on the list. You can do that with the react
method.
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
cities_for_countries countries
end
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def react
# Check if the user selected a country
if applied_filters["Avo::Filters::CourseCountryFilter"].present? && applied_filters["Avo::Filters::CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_filters["Avo::Filters::CourseCountryFilter"].select do |name, selected|
selected
end
# Get the first city
cities = cities_for_countries(selected_countries.keys)
first_city = cities.first.first
# Return the first city as selected
[[first_city, true]].to_h
end
end
private
# Get a hash of cities for certain countries
# Example payload:
# countries = ["USA", "Japan"]
def cities_for_countries(countries_array = [])
countries_array
.map do |country|
# Get the cities for this country
Course.cities.stringify_keys[country]
end
.flatten
# Prepare to transform to a Hash
.map { |city| [city, city] }
# Turn to a Hash
.to_h
end
# Get the value of the selected countries
# Example `applied_filters` payload:
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["Avo::Filters::CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_filters["Avo::Filters::CourseCountryFilter"]
# Keep only the ones selected
.select { |country, selected| selected }
# Pluck the name of the coutnry
.keys
else
# Return empty array
[]
end
end
end
After all, filters are applied, the react
method is called, so you have access to the applied_filters
object. Using the applied filter payload, you can return the value of the current filter.
def react
# Check if the user selected a country
if applied_filters["Avo::Filters::CourseCountryFilter"].present? && applied_filters["Avo::Filters::CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_filters["Avo::Filters::CourseCountryFilter"]
.select do |name, selected|
selected
end
# Get the first city
cities = cities_for_countries(selected_countries.keys)
first_city = cities.first.first
# Return the first city selected as a Hash
[[first_city, true]].to_h
end
end
Besides checking if the countries filter is populated (applied_filters["Avo::Filters::CourseCountryFilter"].present?
), we also want to allow the user to customize the cities filter further, so we need to check if the user has added a value to that filter (applied_filters["Avo::Filters::CourseCountryFilter"].blank?
). If these conditions are true, the country filter has a value, and the user hasn't selected any values from the cities filter, we can react to it and set a value as the default one.
Of course, you can modify the logic and return all kinds of values based on your needs.
Empty message text
Watch the demo videoThere might be times when you will want to show a message to the user when you're not returning any options. You may customize that message using the empty_message
option.
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
self.empty_message = "Please select a country to view options."
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
if countries.present?
[]
else
["Los Angeles", "New York"]
end
end
private
def countries
# logic to fetch the countries
end
end
Keep filters panel open
Watch the demo videoThere are scenarios where you wouldn't want to close the filters panel when you change the values. For that, you can use the keep_filters_panel_open
resource option.
More on this on the keep_filters_panel_open
resource option.
Filter arguments
Filters can have different behaviors according to their host resource. In order to achieve that, arguments must be passed like on the example below:
class Avo::Resources::Fish < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text, hide_on: :forms
end
def filters
filter Avo::Filters::NameFilter, arguments: {
case_insensitive: true
}
end
end
Now, the arguments can be accessed inside Avo::Filters::NameFilter
apply
method, options
method and on the visible
block!
class Avo::Filters::Name < Avo::Filters::TextFilter
self.name = "Name filter"
self.button_label = "Filter by name"
self.visible = -> do
arguments[:case_insensitive]
end
def apply(request, query, value)
if arguments[:case_insensitive]
query.where("LOWER(name) LIKE ?", "%#{value.downcase}%")
else
query.where("name LIKE ?", "%#{value}%")
end
end
end
Manually create encoded URLs
You may want to redirect users to filtered states of the Index
view from other places in your app. In order to create those filtered states you may use these helpers functions or Rails helpers.
Rails helpers
-> decode_filter_params
Decodes the filters
param. This Rails helper can be used anywhere in a view or off the view_context
.
Usage
# in a view
decode_filter_params params[:filters] # {"NameFilter"=>"Apple"}
# Or somewhere in an Avo configuration file
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
filters = view_context.decode_filter_params(params[:filters])
do_something_important_with_the_filters filters
end
end
-> encode_filter_params
Encodes a filters
object into a serialized state that Avo understands. This Rails helper can be used anywhere in a view or off the view_context
.
Usage
# in a view
filters = {"NameFilter"=>"Apple"}
encode_filter_params filters # eyJOYW1lRmlsdGVyIjoiQXBwbGUifQ==
# Or somewhere in an Avo configuration file
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
do_something_important
redirect_to avo.resources_users_path(filters: view_context.decode_filter_params({"NameFilter"=>"Apple"}))
end
end
Standalone helpers
-> Avo::Filters::BaseFilter.decode_filters
Decodes the filters
param. This standalone method can be used anywhere.
Usage
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
filters = Avo::Filters::BaseFilter.decode_filters(params[:filters])
do_something_important_with_the_filters filters
end
end
-> Avo::Filters::BaseFilter.encode_filters
Encodes a filters
object into a serialized state that Avo understands. This standalone method can be used anywhere.
Usage
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
do_something_important
redirect_to avo.resources_users_path(encoded_filters: Avo::Filters::BaseFilter.encode_filters({"Avo::Filters::NameFilter"=>"Apple"}))
end
end