Skip to content
How-to guides and worked examplesSee the guides →

Dynamic filters API ​

Per-option reference for dynamic filters. For task-oriented documentation and worked examples, see the Dynamic filters guide.

Filter options are passed either as a hash to a field's filterable option or as keyword arguments to the dynamic_filter method — the two are equivalent:

ruby
# As a field option
field :first_name, as: :text, filterable: {label: "Name", suggestions: ["Avo", "Cado"]}

# As a standalone filter, inside the resource's `filters` method
def filters
  dynamic_filter :first_name, label: "Name", suggestions: ["Avo", "Cado"]
end

Global behavior is configured under config.dynamic_filters in config/initializers/avo.rb:

ruby
Avo.configure do |config|
  config.dynamic_filters.always_expanded = false
end

The filter dropdown ​

Since avo-dynamic_filters >= v4.3.3

With simple_filters on, every filter's dropdown puts its conditions in a menu next to the filter's name, and below it a value control that fits the condition picked. Without it, filters keep the older dropdown, with the condition and value as two fields.

The Status pill's dropdown open on the Tickets index: an is condition menu next to the name, and a checklist of every status with a coloured dot. Open and In progress are ticked, the pill reads Open, In progress, and the table already shows only those tickets, with no Apply button.

Opt-in

The new dropdown is off by default and becomes the default in a future release. inline, presets and nullable need it.

Each type opens on its most common condition, and the menu holds the rest:

TypeOpens onValue control
SelectIsA checklist of every option. Ticked options match any of them; under Is not, none of them. On a badge field, each option shows its badge colour.
TextContainsA text box.
Boolean(no menu)Yes / No as one row of buttons, with nothing picked until the filter is applied. Picking one applies it straight away, and the x clears it. Rename them with options:, and add Empty with nullable: true, see below.
Number=One number. Is within takes a min and a max (fill either or both).
DateIs withinToday, 7 days, 30 days, This month and Last month as one row of buttons. Custom after them shows a from and to date. Change the buttons with presets. Other conditions take one date.
Date timeIs withinThe same as Date, and the from and to boxes pick a time as well as a day.
TimeIs withinA from and to time. No presets.
TagsOverlapA checklist of every suggestion. Contain matches records with all the ticked tags. Without static suggestions, from fetch_values_from or typed freely, a tag box instead.

Conditions that need no value, like Is blank, show no value control. The pill reads like a sentence, with the name, condition and value in separate segments: Status | is not | Open, Done.

The boolean's choices and its pill read "Yes" and "No", from the avo.dynamic_filters.simple.yes and .no locale keys. Give a filter its own words with options:, and use a lambda to translate them:

ruby
field :urgent, as: :boolean, filterable: {
  options: -> { {true: I18n.t("tickets.urgent"), false: I18n.t("tickets.not_urgent")} }
}

On a column that can be empty, nullable: true adds an Empty choice that finds the records with no value. It reads from avo.dynamic_filters.simple.empty, and options: {nil: "Not set"} renames it.

ruby
field :billable, as: :boolean, filterable: {nullable: true}

The dropdown has no Apply button: each option you tick, and each condition you pick, filters the list straight away while the dropdown stays open, and the scroll position is kept. Text, number and date boxes apply on Enter or when the dropdown closes. Set render_apply_button: true to bring the button back.

Custom conditions: fill the menu. Every other option still applies: options, suggestions, query and the rest.

Filter options ​

label

The filter's label in the filters bar.

ruby
dynamic_filter :first_name, label: "Name"
  • Type: String
  • Default: the field's name when bound to a field, otherwise the filter's ID humanized

icon

The filter's icon. Accepts any icon from Avo or heroicons — see the icons documentation.

ruby
dynamic_filter :user, icon: "heroicons/solid/users"
  • Type: String
  • Default: per filter type:
Filter typeDefault icon
Booleanheroicons/outline/check-circle
Dateheroicons/outline/calendar-days
Numberheroicons/outline/hashtag
Selectheroicons/outline/arrow-down-circle
Tagsheroicons/outline/tag
Textavo/font

type

The filter's type, which determines the input and the default conditions.

ruby
dynamic_filter :version, type: :select

WARNING

A dynamic_filter whose ID doesn't match a field can't infer its type and raises with a message asking you to set it explicitly (dynamic_filter :my_filter, type: :text).

conditions

Replaces the filter type's default conditions with your own.

ruby
dynamic_filter :first_name,
  conditions: {
    case_sensitive: "Case sensitive",
    not_case_sensitive: "Not case sensitive"
  }.invert
  • Type: Hash of label => condition_key (build it as condition_key: "Label" and call .invert, as Avo does internally)
  • Default: the filter type's conditions

Custom condition keys that aren't Ransack predicates need a custom query to handle them.

A request applies only the conditions the filter offers (these, or the type's defaults); any other condition in the URL is ignored.

An empty hash ({}) hides the conditions dropdown entirely and applies the filter type's first default condition — "Is" for a select filter, "Contains" for a text filter, and so on. Useful when only one condition makes sense:

ruby
dynamic_filter :last_name,
  type: :select,
  conditions: {},
  options: User.pluck(:last_name).compact

query

Replaces the auto-generated Ransack query for this filter.

ruby
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
  }
  • Type: Proc; must return the (modified) query
  • Default: nil — Avo applies the condition to the filter's query_attributes through Ransack

Inside the block you have access to:

  • query — the Active Record relation to scope
  • filter_param — an object exposing the filter's id, the applied condition, and the value
  • filterable_field — the field the filter is bound to, if any
  • parent_record — when the filter is applied on an association view
  • all attributes of Avo::ExecutionContext

When several values are applied for the same filter, the block runs once per applied condition/value pair.

query_attributes

The database column(s) the filter queries.

ruby
dynamic_filter :name,
  type: :text,
  query_attributes: [:first_name, :last_name]
  • Type: Symbol or Array of Symbols
  • Default: the filter's ID

With multiple columns the condition matches any of them (Ransack _or_ chaining). All attributes must be authorized in ransackable_attributes.

To query through a belongs_to association, prefix the attribute with the association name (a Ransack feature — the association must be ransackable):

ruby
dynamic_filter label: "User (email & first_name)",
  icon: "heroicons/solid/users",
  query_attributes: [:user_email, :user_first_name]

options

The choices shown in a select filter's dropdown. Available only for select type filters.

ruby
dynamic_filter :version,
  type: :select,
  options: ["Label 1", "Label 2"]
  • Type: Array, Hash, or Proc
  • Default: fetched from the field when bound to one, otherwise []

Accepted shapes:

  • Array — each element is both the option's value and its label
  • Hash — label => value pairs (build it as value: "Label" and call .invert, or write labels as keys directly)
  • Proc — evaluated through Avo::ExecutionContext with filter and parent_record available; must return one of the above
ruby
dynamic_filter :version,
  type: :select,
  options: {
    value_1: "Label 1",
    value_2: "Label 2"
  }.invert

suggestions

Values suggested in the filter's input as the user types. Applies to filters with text input; on tags filters, suggestions default to the ones configured on the tags field.

ruby
dynamic_filter :first_name, suggestions: ["Avo", "Cado"]
  • Type: Array of Strings, Proc, or (tags filters only) Array of Hashes
  • Default: nil — falls back to the bound field's suggestions, then []

Accepted shapes:

  • Array of strings
ruby
dynamic_filter :first_name, suggestions: ["Avo", "Cado"]
  • Proc returning an array of strings — evaluated through Avo::ExecutionContext; parent_record is available when the filter is applied on an association view
ruby
dynamic_filter :first_name,
  suggestions: -> { ["Avo", "Cado", params[:extra_suggestion]] }
  • Array of hashes with the keys value, label, and optionally avatar — tags filters only (directly or from a proc):
ruby
dynamic_filter :tags,
  suggestions: -> {
    [
      {
        value: 1,
        label: "one",
        avatar: "https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?w=256&h=256&fit=crop"
      }
    ]
  }

fetch_values_from

A URL the filter queries to suggest values dynamically — the same mechanism as the tags field's fetch_values_from. Compatible only with tags filters.

ruby
dynamic_filter :city_ids,
  type: :tags,
  fetch_values_from: -> { "/avo-filters/resources/cities/tags" }
  • Type: String or Proc that evaluates to a String
  • Default: nil — falls back to the bound field's fetch_values_from
ruby
# When using a filterable field, the URL is picked up from the field
field :tags, as: :tags,
  fetch_values_from: -> { "/avo-filters/resources/cities/tags" },
  filterable: true

The endpoint must handle two scenarios and return an array of objects with the keys value, label, and optionally avatar:

  1. Search: the user's input arrives as params["q"]
  2. Initial load: when the filter already holds values (e.g. on page load), they arrive as an array in params[:value] — return the matching labels
ruby
class Avo::CitiesController < Avo::ResourcesController
  def tags
    if params[:value].present?
      # Handle initial load: return labels for selected values
      selected_cities = City.where(id: params[:value])
      render json: selected_cities.map { |city|
        {
          value: city.id,
          label: city.name,
          avatar: city.avatar_url
        }
      }
    elsif params["q"].present?
      # Handle search: return cities matching the query
      cities = City.where("name ILIKE ?", "%#{params["q"]}%").limit(10)
      render json: cities.map { |city|
        {
          value: city.id,
          label: city.name,
          avatar: city.avatar_url
        }
      }
    else
      # Handle empty state: return some default suggestions
      render json: [
        {
          value: 1,
          label: "New York",
          avatar: "https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?w=256&h=256&fit=crop"
        }
      ]
    end
  end
end
ruby
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

render_apply_button

Whether the filter renders its "Apply" button.

ruby
dynamic_filter :status,
  type: :select,
  render_apply_button: false

Usually paired with apply_on_select: true for an immediate filtering experience.

apply_on_select

Whether the filter applies immediately when the selected value changes, without the user clicking "Apply". Intended for select type filters.

ruby
dynamic_filter :category,
  type: :select,
  apply_on_select: true,
  render_apply_button: false
  • Type: Boolean
  • Default: false

pinned

Since avo-dynamic_filters >= v4.3.3

Keeps the filter's pill in the filter bar next to "Add filter", so users can filter by it without picking it from the dropdown first. The pill does nothing until someone picks a value and applies it. Removing an applied pinned filter puts the empty pill back.

ruby
# On a field
field :owner, as: :belongs_to, filterable: {
  label: "Owner",
  pinned: true
}

# On a custom filter
dynamic_filter :status,
  type: :select,
  pinned: true
  • Type: Boolean
  • Default: false

A pinned filter is not listed in the "Add filter" dropdown. When a resource has a pinned filter, the filter bar shows even with always_expanded set to false.

Avo Tickets index with three pinned filters, Due on, Status and Urgent, as pills next to Add filter. Status is applied and reads Status, is, Open in separate segments; the table lists open tickets.

inline

Since avo-dynamic_filters >= v4.3.3

Lays a pinned filter's control out in the filter bar, so using it takes no click to open the pill.

ruby
field :urgent, as: :boolean, filterable: {
  pinned: true,
  inline: true
}
  • Type: Boolean
  • Default: false

To lay out every pinned filter this way, set inline_filters instead. It needs simple_filters on, and works on a pinned filter of these types: boolean, text, number, date, date time, time, and a select of up to 6 options, whose checklist becomes a row of chips. Other filters ignore it and keep their pill.

An inline filter shows the same condition menu as the pill's dropdown, right after its name, and changing it filters in place. On phones every inline filter shows as its pill, since a row of controls doesn't fit.

Ticks and buttons filter straight away. Boxes apply on Enter or when they lose focus.

Inline filters in the Tickets filter bar: Due on with its condition menu and date preset buttons; Status as a row of coloured chips with Open picked; Urgent as Urgent and Not urgent buttons with Urgent picked. The table lists urgent open tickets.

presets

Since avo-dynamic_filters >= v4.3.3

The date range buttons a date or date time filter shows.

ruby
# Pick from the built-ins
field :due_on, as: :date, filterable: {presets: [:today, :this_month]}

# Add your own; a built-in name with no range keeps its own
field :due_on, as: :date, filterable: {
  presets: {
    today: nil,
    "Next 30 days" => -> { Date.current..(Date.current + 29) }
  }
}
  • Type: Array of built-in names, or a Hash of label to range
  • Default: [:today, :last_7_days, :last_30_days, :this_month, :last_month]

Each range is a proc returning a Range of dates, run on every render so "today" stays current. A symbol label is translated under avo.dynamic_filters.simple, a string is shown as written. A symbol with nil for its range has to be a built-in name; anything else raises KeyError. presets: [] hides the buttons.

humanized_value

Customizes how the applied filter's value is displayed in the filter pill.

ruby
field :is_capital,
  as: :boolean,
  filterable: {
    humanized_value: -> {
      case filter.condition
      when "is_true"
        "yes"
      when "is_false"
        "no"
      end
    }
  }
  • Type: Proc returning a String
  • Default: nil — the raw value is displayed (select and tags filters substitute the matching option/suggestion labels automatically)

Inside the block you have access to value, the filter object (exposing the applied condition), parent_record (on association views), and all attributes of Avo::ExecutionContext. Returning nil falls back to the raw value.

ruby
dynamic_filter label: "Tags with fetch_values_from",
  type: :tags,
  fetch_values_from: -> { "/avo-filters/resources/cities/tags" },
  humanized_value: -> {
    City.controller_suggestions.select do |suggestion|
      suggestion[:value].to_s.in?(value.split(","))
    end.map { _1[:label] }.join(", ")
  }

humanized_condition

Customizes how the applied filter's condition is displayed in the filter pill.

ruby
dynamic_filter :author,
  type: :tags,
  icon: "heroicons/outline/users",
  conditions: {},
  humanized_condition: -> {
    (filter.value.split(",").count > 1) ? "are" : "is"
  },
  query: -> {
    query.where(author_id: filter_param.value.split(","))
  }
  • Type: Proc returning a String
  • Default: nil — the condition's auto-generated label is displayed

Inside the block you have access to condition, the filter object, parent_record (on association views), and all attributes of Avo::ExecutionContext. Returning nil falls back to the default label.

Global configuration ​

Set these under config.dynamic_filters inside Avo.configure in config/initializers/avo.rb. The older Avo::DynamicFilters.configure block is deprecated; see the upgrade guide.

button_label

The label on the button that expands the filters bar.

ruby
config.dynamic_filters.button_label = -> { I18n.t("my.filters") }
  • Type: Proc or String
  • Default: -> { I18n.t("avo.filters") }
  • i18n key: avo.filters ("Filters")

The button only renders when always_expanded is false.

A plain string is fixed for the life of the process

The initializer runs once at boot and the configuration object lives for the whole process, so a string is pinned for every request afterwards. config.dynamic_filters.button_label = I18n.t("my.filters") has the same problem — it looks translated, but it is evaluated at boot like any other expression.

Pass a proc and it is re-evaluated on every render, so it follows the request locale. Left unset, the default is already a proc, so a localized app usually needs nothing here.

always_expanded

Whether the dynamic filters bar is always shown expanded.

ruby
config.dynamic_filters.always_expanded = false
  • Type: Boolean
  • Default: true — the bar is always visible and the toggle button is hidden

When false, the bar starts collapsed behind a toggle button and expands automatically when filters are present in the URL.

param_key

The URL query param dynamic filters read from and write to.

ruby
config.dynamic_filters.param_key = :df
  • Type: Symbol
  • Default: :filters

With the default key, a filtered URL looks like /avo/resources/users?filters[first_name][contains][]=Avo. Change it if filters collides with another param in your app.

simple_filters

Since avo-dynamic_filters >= v4.3.3

Turns on the new filter dropdown for every filter.

ruby
config.dynamic_filters.simple_filters = true
  • Type: Boolean
  • Default: false

inline_filters

Since avo-dynamic_filters >= v4.3.3

Lays every pinned filter out in the bar, as if each had inline: true. Filters of a type inline doesn't cover keep their pill, and a filter's own inline: false keeps its pill too. It needs simple_filters on.

ruby
config.dynamic_filters.simple_filters = true
config.dynamic_filters.inline_filters = true
  • Type: Boolean
  • Default: false

Filter types and their conditions ​

Each filter type ships a set of conditions the user picks from. The tables below list each condition's key (used in URL params and as a reference for the conditions option), its English label, and the i18n key that label resolves from.

i18n keys are relative to avo.dynamic_filters.conditions — defaults.is_null in the tables below is avo.dynamic_filters.conditions.defaults.is_null in a locale file. They resolve per filter type before falling back to defaults, so number.is overrides defaults.is for number filters only. See Localization for the full tree and the matching pill_conditions phrases.

Null conditions appear only on nullable columns

The Is null / Is not null conditions (and Is present / Is blank on text filters) are offered only when the underlying database column is nullable — on columns with a NOT NULL constraint they're omitted, since they could never match anything.

Boolean ​

KeyLabeli18n key, under avo.dynamic_filters.conditions
is_trueIs truedefaults.is_true
is_falseIs falsedefaults.is_false
is_nullIs nulldefaults.is_null
is_not_nullIs not nulldefaults.is_not_null

A boolean filter never renders a value input: the condition is the whole query. That holds for a custom conditions hash too, and a query lambda reads the user's pick from filter_param.condition. Since avo-dynamic_filters >= v4.2.1

Avo Users index: the Is active dynamic filter pill and open card showing the Is true condition and Apply button, zoomed in over a short three-row table with pagination.

Test it on avodemo, check the source code

Date ​

Covers the :date, :date_time, and :time types — same conditions, different picker: :date_time adds time selection to the calendar and :time shows a time-only picker (no calendar).

KeyLabeli18n key, under avo.dynamic_filters.conditions
isIsdefaults.is
is_notIs notdefaults.is_not
lteIs on or beforedate.lte
gteIs on or afterdate.gte
is_withinIs withindefaults.is_within
is_nullIs nulldefaults.is_null
is_not_nullIs not nulldefaults.is_not_null
Avo Teams index with a short three-row table: the Created at dynamic filter with flatpickr calendar and time picker open over the table.

Test it on avodemo, check the source code

Number ​

KeyLabeli18n key, under avo.dynamic_filters.conditions
is= (equals)number.is
is_not!= (is different)number.is_not
gt> (greater than)defaults.gt
gte>= (greater than or equal to)defaults.gte
lt< (lower than)defaults.lt
lte<= (lower than or equal to)defaults.lte
is_withinIs withindefaults.is_within
is_nullIs nulldefaults.is_null
is_not_nullIs not nulldefaults.is_not_null
Avo Teams index with a short three-row table: the ID dynamic filter pill and open card over the table.

Test it on avodemo, check the source code

Select ​

KeyLabeli18n key, under avo.dynamic_filters.conditions
isIsdefaults.is
is_notIs notdefaults.is_not
is_nullIs nulldefaults.is_null
is_not_nullIs not nulldefaults.is_not_null
Avo Courses index with a short three-row table: the Country dynamic filter pill and open card over the table.

Test it on avodemo, check the source code

Text ​

KeyLabeli18n key, under avo.dynamic_filters.conditions
containsContainsdefaults.contains
does_not_containDoes not containdefaults.does_not_contain
isIsdefaults.is
is_notIs notdefaults.is_not
starts_withStarts withdefaults.starts_with
ends_withEnds withdefaults.ends_with
is_nullIs nulldefaults.is_null
is_not_nullIs not nulldefaults.is_not_null
is_presentIs presentdefaults.is_present
is_blankIs blankdefaults.is_blank
Avo Users index with a short three-row table: the First name dynamic filter pill and open card over the table.

Test it on avodemo, check the source code

Tags ​

KeyLabeli18n key, under avo.dynamic_filters.conditions
array_isAredefaults.array_is
array_containsContaindefaults.array_contains
array_overlapOverlapdefaults.array_overlap
array_contained_inContained indefaults.array_contained_in

WARNING

array_contained_in requires the active_record_extended gem and is not offered on fields using the acts-as-taggable-on gem.

Avo Courses index with a short three-row table: the Skills dynamic filter pill and open card over the table.

Test it on avodemo, check the source code

Field-to-filter mapping ​

When a filter's type isn't set explicitly, Avo derives it from the field type:

Field typeFilter type
boolean:boolean
date, date_time, time:date
id, number, progress_bar:number
select, badge, country, status:select
tags:tags
text, textarea, code, markdown, password, trix:text
anything else:text