Resources API
Per-option reference for resource configuration. For task-oriented documentation and worked examples, see the Resources guide.
Configuration is declared inside the resource class, using class attributes or DSL methods such as index_slot:
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.title = :name
self.includes = [:user]
def fields
field :id, as: :id
end
endIdentity
-> self.title
The attribute or block Avo uses as the record's display name — in link labels, breadcrumbs, association pickers, and the Show view header. When not set, Avo tries the name, title, and label attributes in order and falls back to id.
class Avo::Resources::Post < Avo::BaseResource
self.title = :slug # it will now reference @post.slug to show the title
endA block receives access to resource and record, so the title can be computed without touching the model:
class Avo::Resources::Comment < Avo::BaseResource
self.title = -> {
ActionView::Base.full_sanitizer.sanitize(record.body).truncate 30
}
endThe Symbol may also point to a plain getter method defined on the model:
# app/models/comment.rb
class Comment < ApplicationRecord
def tiny_name
ActionView::Base.full_sanitizer.sanitize(body).truncate 30
end
end- Type: Symbol or Proc
- Default: the first of
name,title,labelpresent on the model, otherwiseid
-> self.description
A piece of text displayed to users on the Index, Show, Edit, and New views, under the resource name.
class Avo::Resources::User < Avo::BaseResource
self.description = "These are the users of the app."
end
A String is displayed on all views. A block can vary the message per context — it has access to record, resource, view, current_user, and params:
class Avo::Resources::User < Avo::BaseResource
self.description = -> do
if view == :index
"These are the users of the app"
else
if current_user.is_admin?
"You can update all properties for this user: #{record.id}"
else
"You can update some properties for this user: #{record.id}"
end
end
end
end- Type: String or Proc
- Default:
nil
WARNING
self.description is rendered as HTML (<%==). Do not use direct user input or any value that users can edit — that can allow stored XSS attacks.
INFO
self.description is not displayed when the resource is rendered as an association (for example, in a has_many table on another resource's page). To show a description there, use the description option on the association field.
INFO
You can skip this attribute entirely and supply the text from a locale file instead, under avo.resource_translations.<resource>.description. When that key resolves it wins over self.description, so an app can keep an English default here and translate over it. See Localizing resources.
-> self.avatar
The photo displayed next to the record on the Show and Edit views and in the breadcrumbs.
class Avo::Resources::User < Avo::BaseResource
self.avatar = {
source: :profile_photo
}
end- Type: Hash with keys
source(Symbol or Proc) andvisible_on - Default:
nil
More information on the cover and avatar page.
-> self.cover
The large banner image displayed at the top of the record.
class Avo::Resources::Post < Avo::BaseResource
self.cover = {
source: :cover_photo
}
end- Type: Hash with keys
source(Symbol or Proc),visible_on, andsize - Default:
nil
More information on the cover and avatar page.
-> self.icon
The icon displayed next to the resource on the sidebar.
class Avo::Resources::User < Avo::BaseResource
self.icon = "tabler/outline/user"
end- Type: String — an icon path
- Default: Avo's default resource icon
-> self.color
Tints the resource's sidebar icon with a color from Avo's palette. Only the icon stroke is colored — the label and the hover and active backgrounds stay neutral. Use it to make a resource recognizable at a glance, or give related resources the same color to visually group them.
The color follows the resource beyond the sidebar: the initials chip in the breadcrumbs is tinted too, and with Advanced Search installed, so are the resource group headers in global search results (which also show the resource's self.icon).
class Avo::Resources::User < Avo::BaseResource
self.icon = "tabler/outline/user"
self.color = :purple
end- Type: Symbol or String — one of
red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose - Default:
nil— the neutral icon color
Colors adapt automatically to the light and dark themes. An unknown color name renders the default neutral icon. A color: set on the menu entry overrides this value.
-> self.model_class
The model this resource references. Set it when the model is namespaced, when the class can't be inferred from the resource name, or when the resource is a secondary resource for a model.
class Avo::Resources::DelayedJob < Avo::BaseResource
self.model_class = "Delayed::Job"
end- Type: String or Class
- Default:
nil— the model class is inferred from the resource name (namespace included)
Index view
-> self.default_view_type
The view type the Index view renders when the user hasn't picked one — the classic :table, or :grid/:map for more visual data.
class Avo::Resources::Post < Avo::BaseResource
self.default_view_type = :grid
end
A Proc can pick the view type per request. Within the block you have access to all attributes of Avo::ExecutionContext along with resource and view:
class Avo::Resources::Post < Avo::BaseResource
self.default_view_type = -> {
mobile_user = request.user_agent =~ /Mobile/
mobile_user ? :table : :grid
}
end- Type: Symbol or Proc
- Default:
:table - Values:
:table,:grid,:map, or a custom view type
-> self.view_types
The view types available on the Index view switcher. Icons on the switcher follow the order in which the values are declared. When only one view type is available, the switcher is not rendered.
class Avo::Resources::City < Avo::BaseResource
self.view_types = [:table, :grid]
endA Proc can decide per request. Within the block you have access to all attributes of Avo::ExecutionContext along with resource and record:
class Avo::Resources::City < Avo::BaseResource
self.view_types = -> { current_user.is_admin? ? [:table, :grid] : :table }
end- Type: Symbol, Array of Symbols, or Proc
- Default:
nil—:table, plus:grid/:mapwhen the resource hasgrid_view/map_viewconfigured - Values:
:table,:grid,:map, or a custom view type - Validation: requesting the
Indexview with aview_typeoutside this list raises an error
-> self.default_sort_column
The column records are sorted by on the Index view.
class Avo::Resources::User < Avo::BaseResource
self.default_sort_column = :last_name
end- Type: Symbol — any sortable column on the model
- Default:
:created_at - Validation: if the column doesn't exist on the model, Avo falls back to
created_at
INFO
When changing the default sort column, it's recommended to add an index to that column in your database for better query performance.
class AddIndexOnUsersLastName < ActiveRecord::Migration[7.1]
def change
add_index :users, :last_name
end
end-> self.default_sort_direction
The direction records are sorted in on the Index view, applied to the default sort column.
class Avo::Resources::Task < Avo::BaseResource
self.default_sort_column = :position
self.default_sort_direction = :asc
end- Type: Symbol
- Default:
:desc - Values:
:ascor:desc
-> self.pagination
Pagination settings for the Index view. On large tables the record count can be inefficient — the :countless type skips it entirely.
# As block:
self.pagination = -> do
{
type: :default,
slots: 9,
}
end
# Or as hash:
self.pagination = {
type: :default,
slots: 9,
}- Type: Hash, or Proc returning a Hash
- Default:
{ type: :default, slots: 9 }
| Key | Values | Default | Behavior |
|---|---|---|---|
type | :default, :countless | :default | :countless skips the record count |
slots | see Pagy docs — control the page links | 9 | Number of page links rendered |
Examples
Default
self.pagination = -> do
{
type: :default,
slots: 9,
}
end
Countless
self.pagination = -> do
{
type: :countless
}
end
Countless and "pageless"
self.pagination = -> do
{
type: :countless,
slots: 0
}
end
-> self.index_query
The base query for the Index view. Useful for dropping a model's default_scope when rendering the index:
class Project < ApplicationRecord
default_scope { order(name: :asc) }
endclass Avo::Resources::Project < Avo::BaseResource
self.index_query = -> { query.unscoped }
end- Type: Proc — receives
queryand returns the modified query
-> self.find_record_method
How Avo fetches one record for the Show and Edit views and any other context where a record is loaded from the database. Used with custom to_param methods, slugs, and similar non-standard identifiers.
class Avo::Resources::Post < Avo::BaseResource
self.find_record_method = -> {
id.to_i == 0 ? query.find_by!(slug: id) : query.find(id)
}
endThe block is evaluated with query, id, params, and model_class available. In batch contexts (bulk actions, for example) id is an Array — return a collection with query.where(...) in that case. Avo asks the block once per id for any id that collection leaves out, so a block that takes one id at a time still works. When Avo finds several records at once (bulk actions, find_records), it keeps only the records query also holds, so a block that ignores query can't reach past the user's policy scope there. A single lookup (Show, Edit, a belongs_to) uses the block's answer as-is, so keep reading through query.
To find several records in your own code, call the resource's find_records. It goes through the same block and returns one entry per id, in order, with nil for an id that matches no record. query: defaults to the current user's policy scope.
Avo::Resources::Post.find_records(["hello-world", "missing"], query: Post.published)
# => [#<Post slug: "hello-world">, nil]- Type: Proc — receives
query,id,params, andmodel_class; returns the record (or collection whenidis an Array) - Default:
query.find(id). Models using FriendlyId (without a scope) are detected automatically and found by slug.
-> self.record_selector
Whether the selection checkbox is rendered on each row of the Index view. Disable it for resources that will never be selected to reclaim the horizontal space.
class Avo::Resources::Comment < Avo::BaseResource
self.record_selector = false
end
- Type: Boolean
- Default:
true
-> self.keep_filters_panel_open
Whether the filters panel stays open after the user changes a filter value.
class Avo::Resources::Course < Avo::BaseResource
self.keep_filters_panel_open = true
def filters
filter Avo::Filters::CourseCountryFilter
filter Avo::Filters::CourseCityFilter
end
end- Type: Boolean
- Default:
false
-> self.link_to_child_resource
For Single Table Inheritance (STI) models. When declared on the parent resource, clicking a record on the Index view redirects to the child resource's record instead of the parent's.
For example, with Sibling and Spouse models inheriting from Person, a user clicking a person on the Person index is redirected to the Sibling or Spouse record.
class Avo::Resources::Person < Avo::BaseResource
self.link_to_child_resource = true
end- Type: Boolean
- Default:
false
-> self.index_view_loading
Controls when the Index view queries and renders its records. With :lazy, Avo renders the header and controls immediately, then defers the records query to a Turbo Frame request, showing the loading component while the table, grid, or other view type loads.
class Avo::Resources::Post < Avo::BaseResource
self.index_view_loading = :lazy
end| Value | Behavior |
|---|---|
:eager | The records are queried and rendered with the initial page load. |
:lazy | The shell renders first; the records are fetched in a deferred Turbo Frame. Search, filters, sorting, pagination, and view switches then update inside that frame while the browser URL stays in sync. |
- Type: Symbol
- Default:
:eager - Values:
:eager,:lazy - Scope: applies only to a resource's own
Indexview rendered by the defaultAvo::Views::ResourceIndexComponent. Association indexes, and resources with a custom index component, keep eager loading regardless of this setting.
Forms and saving
-> self.confirm_on_save
Whether Avo asks for confirmation before saving a record. Adds friction to the saving process, helping avoid human error.
class Avo::Resources::Post < Avo::BaseResource
self.confirm_on_save = true
end
- Type: Boolean
- Default:
false
-> self.after_create_path
Where the user is redirected after creating a record.
class Avo::Resources::Comment < Avo::BaseResource
self.after_create_path = :index
end- Type: Symbol
- Default:
:show - Values:
:show,:edit, or:index
For more granular control over the redirect or response, use the after_create_path controller method.
-> self.after_update_path
Where the user is redirected after updating a record.
class Avo::Resources::Comment < Avo::BaseResource
self.after_update_path = :edit
end- Type: Symbol
- Default:
:show - Values:
:show,:edit, or:index
For more granular control over the redirect or response, use the after_update_path and after_destroy_path controller methods.
-> self.devise_password_optional
If you use devise and update your user models (usually User) without passing a password, you will get a validation error. devise_password_optional stops that error by stripping out the password key from params.
class Avo::Resources::User < Avo::BaseResource
self.devise_password_optional = true
end- Type: Boolean
- Default:
false
-> save_record
avo >= v4.3.0The method that persists a record on create and update. Avo calls it from the controller's save_record_action, so one definition covers the admin panel, the REST API and any controller of your own.
class Avo::Resources::Course < Avo::BaseResource
def save_record(record)
record.save!
SyncCourseJob.perform_later(record)
true
end
end- Type: Instance method taking the record
- Default:
record.save! - Returns: something truthy on success. Raise to fail: the exception is caught, logged and added to the record's errors, which triggers the controller's fail action and message.
A record that is not an Active Record model has no save!, so a resource not backed by one defines this method to be writable at all: see array resources and HTTP resources.
-> destroy_record
avo >= v4.3.0The method that removes a record on destroy. Avo calls it from the controller's destroy_record_action, so one definition covers every surface that deletes the record.
class Avo::Resources::Course < Avo::BaseResource
def destroy_record(record)
record.archive!
end
end- Type: Instance method taking the record
- Default:
record.destroy! - Returns: something truthy on success. Raise to fail, the same way
save_recorddoes.
-> new_record
avo >= v4.3.0The blank record a New form is filled into. Override it on a resource whose records need more than model_class.new, such as a constructor argument or a record that is not an Active Record model.
class Avo::Resources::Invoice < Avo::BaseResource
def new_record
model_class.new(currency: Current.account.currency)
end
end- Type: Instance method taking no arguments
- Default:
model_class.new
INFO
Array resources already override this to load their record class before the form is filled, so they need nothing here.
-> config.buttons_on_form_footers
Whether the Back and Save buttons are also rendered in the footer of New and Edit forms — useful when forms grow tall. Unlike the other options on this page, this one is set in the initializer and applies to all resources.
# config/initializers/avo.rb
Avo.configure do |config|
config.buttons_on_form_footers = true
end
- Type: Boolean
- Default:
false
-> config.warn_on_unsaved_changes
Whether Avo asks for confirmation before the user leaves a New or Edit form with unsaved changes. Like config.buttons_on_form_footers, this one is set in the initializer and applies to all resources.
# config/initializers/avo.rb
Avo.configure do |config|
config.warn_on_unsaved_changes = false
end- Type: Boolean
- Default:
true - i18n key:
avo.unsaved_changes_warning("You have unsaved changes. Are you sure you want to leave this page?")
What counts as a change
The form's current values are compared with the values it had before the user's first interaction with it.
- Typing something and deleting it again is not a change. Neither is only clicking into a field.
- Rich editors (Trix, Tiptap, markdown, code, tags, key value) are covered, because they keep a form input in sync.
- Custom fields are covered as long as they write their value to a form input. A field that keeps its value only in JavaScript state until submit is invisible to the check.
- Saving never asks. When the save fails validation, the form that re-renders with the errors counts as changed from the start.
How the user is asked
| Leaving through | The user sees |
|---|---|
| A link or button inside Avo | Avo's confirmation dialog |
| Browser back and forward | Avo's confirmation dialog |
| Reloading or closing the tab | The browser's native "leave site?" prompt |
The back and forward guard uses the Navigation API. Browsers without it skip that guard and keep the other two. Browsers let a page cancel back or forward only once per user interaction, so pressing back twice in a row without touching the page always leaves.
Not covered
- Forms shown in a modal, such as the
belongs_to"create new" modal. - The search and scope inputs of associations shown on the
Editpage. They are not values of the record, so typing in them is not a change. - A value that changes before the user clicks or types in the form, such as one filled in by a browser extension.
System specs
A system spec that changes a form and then navigates away without saving meets the confirmation dialog. Accept it with accept_custom_alert from the testing helpers, or turn the option off in the test environment only.
# config/initializers/avo.rb
Avo.configure do |config|
# Off in the test environment, on everywhere else.
config.warn_on_unsaved_changes = !Rails.env.test?
endNavigation
-> self.visible_on_sidebar
Whether the resource appears in the auto-generated sidebar menu.
class Avo::Resources::TeamMembership < Avo::BaseResource
self.visible_on_sidebar = false
end- Type: Boolean
- Default:
true
WARNING
This option is used in the auto-generated menu, not in the menu editor.
You'll have to use your own logic in the visible block for that.
-> self.hotkey
A keyboard shortcut that jumps to the resource's Index view from anywhere in the admin panel. The binding is used automatically when the resource appears in the sidebar via the auto-generated menu or the menu editor.
class Avo::Resources::Post < Avo::BaseResource
self.hotkey = "g p"
end- Type: String — @github/hotkey syntax; space-separate keys for sequences (
"g p"= press g then p) - Default:
nil
-> self.external_link
A link to the record's public page outside the Avo interface. When configured, Avo displays an external link button on the record — clicking it takes the user to the returned URL.
The block should return a String URL. It has access to all attributes of Avo::ExecutionContext along with record, so you can use your application's path helpers (e.g. main_app.post_path) or any external URL generator.
class Avo::Resources::Post < Avo::BaseResource
self.external_link = -> {
main_app.post_path(record)
}
end
- Type: Proc returning a String
- Default:
nil
Performance
-> self.includes
Associations to eager load on the Index view — the cure for those nasty n+1 performance issues.
class Avo::Resources::Post < Avo::BaseResource
self.includes = [:user, :tags]
# or a very nested scenario
self.includes = [files_attachments: :blob, users: [:comments, :teams, post: [comments: :user]]]
endWe know, the array notation looks weird, but it works.
- Type: Array — anything ActiveRecord's
includesaccepts - Default:
[]
-> self.single_includes
Works the same as self.includes, but eager loads the associations on the Show and Edit views only.
- Type: Array
- Default:
[]
-> self.attachments
Attachments to eager load on the Index view, similar to how self.includes works for associations.
class Post < ApplicationRecord
has_one_attached :cover_photo
has_one_attached :audio
has_many_attached :attachments
endclass Avo::Resources::Post < Avo::BaseResource
self.attachments = [:cover_photo, :audio, :attachments]
end- Type: Array of Symbols — attachment names
- Default:
[]
-> self.single_attachments
Works the same as self.attachments, but eager loads the attachments on the Show and Edit views only.
- Type: Array of Symbols
- Default:
[]
-> cache_hash
The part of a row's cache key that comes from the record. The default implementation looks like this:
def cache_hash(parent_record)
result = [record, file_hash]
if parent_record.present?
result << parent_record
end
result
endfile_hash is an md5 of the resource file, the policy file and Avo.cache_version (a digest of the installed Avo version and every registered plugin's version), so the cache gets busted when the rules change and when you upgrade Avo or a plugin. The parent_record is added when the resource is displayed as an association, so there's a separate cache record for each association.
Override the method in your resource file when a row displays data the record itself doesn't carry, such as an association:
class Avo::Resources::User < Avo::BaseResource
def cache_hash(parent_record)
result = [record, file_hash, record.post]
if parent_record.present?
result << parent_record
end
result
end
endAnything that varies by who is looking, rather than by the record, belongs in cache_context instead. The two are composed into the actual key, so overriding cache_hash can't drop the viewer out of it.
INFO
Only the view caches per row today. Table rows are rendered on every request; caching them under the same key is a follow-up.
-> cache_context
The part of a row's cache key that comes from the request. By default it resolves config.index_cache_context, which is:
# config/initializers/avo.rb
config.index_cache_context = -> { [current_user, I18n.locale, Avo::Current.tenant_id] }That puts the current user record, the locale and the tenant in every row's key, so a visible: lambda, a computed field or a grid card that reads current_user is cached per user and can never serve one user's row to another. The user record is in the key, not its id: a role edit touches updated_at, which busts that user's rows on the spot.
Both the global option and the per-resource method run through Avo::ExecutionContext, so current_user, params, request and context are available. It is resolved once per request and reused for every row, so it must not read record.
Narrow it when many users see exactly the same rows and you'd rather they share the cache. Only do this when nothing a row renders reads the user:
# config/initializers/avo.rb
config.index_cache_context = -> { [current_user.role, I18n.locale] }Or widen it on one resource whose lambdas read some other per-request dimension:
class Avo::Resources::Order < Avo::BaseResource
def cache_context
[*super, Avo::Current.context[:currency]]
end
endThe final key is index_cache_key(parent_record), which is [*cache_hash(parent_record), *cache_context].
WARNING
A key can only vary by what it contains. A visible: or computed field that reads params (say, a query-string flag) is served from whichever request cached the row first, whatever the key. Put the value in cache_context if it is a small, bounded set, or turn cache_resources_on_index_view off for that app.
Customization
-> index_slot
Adds a block at a named position on the resource's index, including association index panels. Slots are additive unless replace: true is supplied for a named region. They do not change authorization or render per row. For other views, see show_slot, edit_slot, new_slot, and form_slot. For examples and caveats, see the Slots guide.
class Avo::Resources::Project < Avo::BaseResource
index_slot :after_header do
render partial: "avo/projects/summary", locals: {resource: resource}
end
end| Position | Placement |
|---|---|
:before_cards | Before the cards area in the page shell. |
:after_cards | After the cards area in the page shell. |
:before_header | Before the header containing the title, description, and controls. |
:after_header | After the entire header. |
:before_title | Inline immediately before the title, inside the header. |
:after_title | Inline immediately after the title, inside the header. |
:before_controls | Before the header controls, inside the header. |
:after_controls | After the header controls, inside the header. |
:before_toolbar | Before the toolbar location, even when the toolbar is absent. |
:after_toolbar | After the toolbar location, even when the toolbar is absent. |
:before_collection | Outside and before the complete table or grid and its paginator, or the empty state. |
:after_collection | Outside and after the complete table or grid and its paginator, or the empty state. |
:before_records | Immediately before the records, inside the card for tables; also before the empty state. |
:after_records | Immediately after the records, inside the card for tables and before pagination; also after the empty state. |
:before_pagination | Immediately before the table or grid paginator; absent for empty results. |
:after_pagination | Immediately after the table or grid paginator; absent for empty results. |
- Type: DSL method taking a position
Symbol, optionalreplace: true, and a block. - Default: No registered blocks; the built-in index is unchanged.
- Values: The sixteen additive positions above, or the replacement regions below when
replace: true. - Validation: An unknown position, missing block, or duplicate replacement on the same resource raises
ArgumentErrorat declaration time. Region names requirereplace: true; before/after positions reject it. - Order: Additive declarations accumulate in registration order, including inherited registrations. Replacements have one owner per region, with explicit child overrides. Neither form mutates parent or sibling resources.
- Inheritance: Shared declarations belong in the application's base resource, before child declarations. Addition and replacement registrations use separate per-view copy-on-write sets. Late parent registrations may not reach children that have customized the corresponding set for that view. There is no public removal or priority API. Repeating an additive declaration appends again.
- Output: A rendered partial or component, tag-helper HTML, escaped plain text, or
nil(no output). No wrapper or spacing is added. Partial locals are not supplied automatically. - Execution: The block's return value is used, not an implicit concatenation of every expression. Exceptions propagate normally; blocks are application code, not sandboxed code. There is no once-per-request guarantee across multiple rendered panels.
- Context: Blocks run in
Avo::ExecutionContext, with standard helpers,render,current_user,params,request, andcontext, plus the following index values:
| Value | Meaning |
|---|---|
resource | The current index resource instance, not an individual row. |
resources | The current page of hydrated Avo resources, not raw model records. Each entry exposes its model through .record. |
query | The authorized index query, retaining the current index constraints; not necessarily an ActiveRecord relation. |
pagy | The index pagination object. |
parent_record | The parent record when rendered in an association index panel; otherwise nil. |
parent_resource | The parent resource when rendered in an association index panel; otherwise nil. |
view_type | The current index view type. |
record | resource.record; Index has no individual current row. |
view | The normalized Avo::ViewInquirer for Index. |
form | nil on Index. |
default_content | Replacement blocks only: a callable returning the built-in region's rendered HTML. |
Replacement regions:
| Region | Default content |
|---|---|
:cards | Resource dashboard cards, not individual grid cards. |
:header | The header with title, description, and controls, including nested title hooks. |
:title | The title text inside its existing text-value wrapper; surrounding badges and controls are not replaced. |
:controls | The header controls from self.index_controls or Avo's defaults, such as Create and Actions. |
:toolbar | Scopes, search, filters, view toggles, and dynamic filters at the toolbar location. |
:collection | The complete table card or grid and its paginator, or the empty-state branch; surrounding Turbo targets remain. |
:records | The table records area or grid records, excluding pagination; the empty state for empty results. |
:pagination | The paginator inside its existing footer; absent for empty results. |
index_slot :title, replace: true do
tag.span(default_content.call, class: "custom-title")
enddefault_content.call renders lazily and memoizes only for the current region render. The block's return value controls output: return the default to preserve it, include it in custom markup to wrap it, return different content to replace it, or return nil to hide the region. Calling the default does not insert it automatically. Do not insert the same default more than once, because this duplicates HTML IDs and controls.
The callable is available only in replacement context, is not passed as a positional block argument, and is not automatically a partial local. Its memoization is not a fragment cache or a cache across requests. Returning nil also suppresses rendering of the default when the callable was never invoked.
Only one replacement may be declared on a resource per region. Children may override inherited replacements without mutating parents or siblings. Their default is always Avo's built-in content, not the parent's replacement. Surrounding before/after hooks remain, but nested hooks render only when the replacement calls its default. Replacing a whole header or collection without its default therefore removes its nested hooks and controls. Avo keeps outer Turbo targets and loading boundaries; replacements own the behavior of controls whose markup they replace. Authorization and data loading still run. See Wrap or replace default content.
Lazy loading
Cards and header slots, including title slots, belong to the page shell. On an initial lazy render, resources, query, and pagy are not loaded there. Toolbar, collection, records, and pagination slots render in the deferred content and support data-dependent work. See self.index_view_loading.
Refresh boundaries: Live search replaces only ResourceListingComponent, not the toolbar. Toolbar slots defer with the lazy body but do not refresh on live search. In the built-in table and grid, collection, records, and pagination slots refresh with live search; the listing's empty branch renders collection and record slots too. Summaries that must track the search and page belong in collection, records, or pagination slots, not toolbar slots. Cards and header slots remain in the page shell.
Collection, records, and pagination slots are supported by the built-in table and grid and the updated Notifications add-on. The updated Calendar add-on supports collection and records slots, not pagination. These positions stay inside each view type's list Turbo Frame; date navigation and live-search visits re-render the slots in that frame. Calendar's resources, query, and pagy retain the index context, not its separate visible-date-range query.
Other view types must opt in by calling render_index_slot :before_records (and the other supported positions) from a component inheriting from Avo::ViewTypes::BaseViewTypeComponent. Existing custom index components must adopt slots themselves; resource declarations alone do not update their templates. Shell hooks must not be duplicated inside the view type. See Keep slots when customizing the index.
Other surfaces: Map only exposes listing hooks through its optional embedded table; these do not wrap the map. Kanban boards are not resource index views. Bell updates and single-notification broadcasts do not render notification index slots. With an empty index page, core renders collection and records hooks around the shared empty state without instantiating the view-type component; pagination hooks are absent. The guide's support matrix summarizes coverage.
Performance: Registrations are stored in memory. Missing positions return without rendering or partial lookup. Dispatch does not spawn processes or discover slots through filesystem scans. Registered blocks execute on each relevant render, using normal Rails template lookup and caching for partials. Output is not automatically cached, and queries or other IO inside a block remain the application's responsibility. A replacement avoids the default's rendering work only when it does not invoke default_content; it does not avoid controller authorization, querying, pagination, or hydration.
-> show_slot
Adds or replaces content on Show. Equivalent to view_slot :show, position, replace: false with the same block.
show_slot :after_title do
tag.span(record.status)
end- Type: DSL method taking a position
Symbol, optionalreplace: Boolean, and a block. - Default:
replace: false; no registrations; built-in Show is unchanged. - Values:
before_REGION/after_REGIONadditions for each region below; bare region names requirereplace: true. - Validation: Unknown positions, missing blocks, bare regions without replacement, before/after positions with replacement, and duplicate same-view, same-region replacements declared by the same resource class raise
ArgumentError. - Inheritance: Per-view copy-on-write. Additions accumulate; children can override inherited replacements without mutating parents or siblings.
- Context:
Avo::ExecutionContextwith helpers and request context as for Index.resourceis the current resource,recordisresource.record,viewis a normalizedAvo::ViewInquirerfor Show, andformisnil. Index collection values are not a single-record query API. - Output: Rendered HTML, escaped plain text, or
nil; the block's return value is used. No wrapper, spacing, or implicit partial locals are added.
| Region | Placement / default content |
|---|---|
:cards | Resource dashboard cards, outside the body. |
:body | All resource items, including the primary header on full pages, fields, panels, tabs, tools, and associations. |
:header | The primary header, including title and controls. |
:title | Title content; before/after additions are inline. |
:controls | Built-in record controls. |
Each region has symmetric before/after additions. Surrounding additions remain when a region is replaced. Replacing the body without its default removes nested header, title, and controls hooks; replacing the header without its default removes its nested title and controls hooks.
Replacement blocks receive default_content, a lazy callable memoized once per region render. It always renders Avo's built-in content, not an inherited replacement. It does not insert itself automatically or cache across requests. Returning nil hides the region. No replacement chaining occurs. See Wrap or replace default content.
-> edit_slot
Adds or replaces content on Edit, including rerenders after update validation failures. Equivalent to view_slot :edit, position, replace: false with the same block.
edit_slot :after_controls do
tag.span("Review your changes before saving.")
end- Type: DSL method taking a position
Symbol, optionalreplace: Boolean, and a block. - Default:
replace: false; no registrations; built-in Edit is unchanged. - Values: All Show regions (
cards,header,title,controls,body), plusform. Every region supports symmetric before/after additions or replacement of the bare region withreplace: true. - Validation / output / inheritance: Same contract as
show_slot, scoped to Edit. - Context:
recordisresource.record;viewis normalized to Edit, including update validation rerenders. Inside the form region and nested body, header, title, and controls,formis the current Rails form builder. It isniloutside the form. - Order: Shared
form_slotadditions precede Edit additions at every position; declaration order is preserved within each group. - Replacement precedence: An Edit replacement wins over a shared form replacement, even if the Edit replacement is inherited and the shared form replacement belongs to a child. Its
default_contentis always Avo's built-in content, never the shared or inherited replacement.
The form region contains the original form's contents after protected hidden referrer parameters. Replacing it retains the outer form tag, CSRF protection, HTTP method handling, and Stimulus wiring. before_form and after_form are outside the form, with no builder; cards are also outside the form. In belongs-to creation modals they render inside the dialog, around the form region, and so inside the form with the current builder. The full-page body includes the primary header and all resource items. Replacing that body without its default suppresses its nested header, title, and controls hooks.
Controls hooks also render at the optional footer when config.buttons_on_form_footers is enabled. Fixed IDs in those hooks can therefore be duplicated. See Share additions across Edit and New.
-> new_slot
Adds or replaces content on New, including rerenders after create validation failures and belongs-to creation modals. Equivalent to view_slot :new, position, replace: false with the same block.
new_slot :before_body do
tag.p("Choose an owner before saving.")
end- Type: DSL method taking a position
Symbol, optionalreplace: Boolean, and a block. - Default:
replace: false; no registrations; built-in New is unchanged. - Values:
cards,header,title,controls,body,form, each supporting symmetric before/after additions or bare-region replacement withreplace: true. - Validation / output / inheritance: Same contract as
show_slot, scoped to New. - Context: Same builder and form boundaries as
edit_slot.recordisresource.record, including the unsaved New record;viewis normalized to New, including create validation rerenders. - Order: Shared form additions precede New additions, preserving registration order within each group.
- Replacement precedence: A New replacement wins over the shared form fallback, including an inherited New replacement versus a child form replacement. Defaults never chain through other replacements.
In belongs-to creation modals, title, body, controls, and form hooks are available. Main header hooks are deliberately unavailable to preserve native modal chrome, close behavior, and ARIA structure. Title hooks are inline in the modal heading; controls hooks are in the footer. The modal body default excludes the header; title and controls are outside that body. Builder availability follows the actual form boundary, not the slot's name. See Creation modals and repeated controls.
-> form_slot
Registers shared content for both Edit and New, including create/update validation rerenders and belongs-to creation modals. Equivalent to view_slot :form, position, replace: false with the same block. This method targets both form views; it is distinct from the :form region inside those views.
form_slot :form, replace: true do
tag.div(default_content.call, class: "project-form-content")
end- Type: DSL method taking a position
Symbol, optionalreplace: Boolean, and a block. - Default:
replace: false; no registrations; built-in forms are unchanged. - Values:
cards,header,title,controls,body,form, each supporting symmetric before/after additions or bare-region replacement withreplace: true. - Validation / output / inheritance: Same contract as
show_slot, scoped to the shared:formregistrations. An Edit or New replacement and a shared form replacement are not duplicates. - Context: The active
edit_slotornew_slotcontext.viewidentifies Edit or New, not Form.formis the current Rails builder only inside the form; it isnilfor cards and any other outside-form rendering, includingbefore_formandafter_formoutside belongs-to creation modals. - Order: Shared additions always precede view-specific additions at each position, preserving registration order within each group.
- Replacement precedence: Shared replacements are fallbacks. View-specific replacements win regardless of inheritance depth.
default_content.callalways means the built-in region, rendered lazily at most once per region render, not another replacement.
The form region begins after the protected hidden referrer parameters; the original form, CSRF/method handling, and Stimulus wiring remain. Modal header hooks are unavailable; modal title, body, controls, and form hooks follow the boundaries documented under new_slot. Controls may render again in optional full-page footers. See the shared form guide.
-> view_slot
Generic registration method for the named slot DSLs.
view_slot :show, :title, replace: true do
tag.span(default_content.call, class: "custom-title")
end- Type: DSL method
view_slot(view, position, replace: false, &block). - Default:
replace: false; no registered blocks. - Values: View
Symbol::index,:show,:edit,:new, or:form. Positions are those supported by the corresponding named method. - Validation: Unsupported views raise
ArgumentErrorat declaration time; position, block, and duplicate-replacement validation matches the named method. - Equivalence:
view_slot :index, positionisindex_slot position; likewise forshow_slot,edit_slot,new_slot, andform_slot. Generic and named methods share registrations, ordering, inheritance, context, and replacement semantics and can be mixed. - Rendering: No additional IO or partial discovery is performed by slot dispatch. Registered blocks may explicitly render partials using Rails' normal lookup; unregistered hooks do not look up partials. Output is not automatically cached.
See Choose the view and region for the complete support matrix. Custom or ejected components must retain the appropriate rendering hooks; a resource registration does not insert hooks into an overridden template.
-> self.components
For additions and region-level replacements on Index, Show, Edit, and New, slots preserve the rest of the default component without an override. Shared form slots apply to Edit and New. Component replacement remains available when those boundaries are insufficient.
The ViewComponent classes rendered for each view. By default, each view renders:
| View | Component |
|---|---|
| Index | Avo::Views::ResourceIndexComponent |
| Show | Avo::Views::ResourceShowComponent |
| New, Edit | Avo::Views::ResourceEditComponent |
Swap any of them for your own classes. Keys must be strings that match the original component class name; values may be classes or strings.
self.components = {
"Avo::Views::ResourceIndexComponent": Avo::Views::Users::ResourceIndexComponent,
"Avo::Views::ResourceShowComponent": "Avo::Views::Users::ResourceShowComponent",
"Avo::Views::ResourceEditComponent": "Avo::Views::Users::ResourceEditComponent",
"Avo::Index::GridItemComponent": "Avo::Custom::GridItemComponent",
"Avo::ViewTypes::MapComponent": "Avo::Custom::MapComponent",
"Avo::ViewTypes::TableComponent": "Avo::Custom::TableComponent",
"Avo::ViewTypes::GridComponent": "Avo::Custom::GridComponent",
"Avo::Index::TableRowComponent": "Avo::Custom::TableRowComponent"
}- Type: Hash — original component class name (String key) to replacement component (Class or String)
- Default:
{}
WARNING
The custom view components must ensure that their initializers are configured to receive all the arguments passed during the rendering of a component. You can verify this in our codebase through the following files:
Index -> app/views/avo/base/index.html.erb
Show -> app/views/avo/base/show.html.erb
New -> app/views/avo/base/new.html.erb
Edit -> app/views/avo/base/edit.html.erb
The easiest way to create a compatible component is to eject an existing one with the --scope parameter. For a full walkthrough, see the safely override resource components guide.
-> self.discreet_information
Shows information about records without adding another field.
More information on the discreet information page.
Options documented on their own pages
Some resource options have enough surface to warrant a dedicated page:
| Option | Documented on |
|---|---|
self.search | Search |
self.translation_key | I18n |
self.ordering | Record reordering |
self.grid_view | Grid view |
self.map_view | Map view |
self.avatar | Cover and avatar |
self.cover | Cover and avatar |
self.row_controls_config | Table view API |
self.table_view | Table view API |
def chip | Record chips |