Has Many
The has_many field displays a model's associated records in a separate panel, with controls to browse, attach, create, and remove records according to the user's permissions.
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :projects, as: :has_many
end
endBy default, the panel appears below the regular fields on the Show view, loads lazily, and uses the resource inferred from the Rails association. If your app changes config.resource_default_view to :edit, the panel follows that setting. The attach modal uses a select input unless you enable another picker.
See the association overview for model setup and shared field options for visibility and other common settings.
Choose which records to display
Use scope to narrow the association table, for example to show only approved comments. Build on query, which already includes the target resource's policy scope.
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :comments, as: :has_many, scope: -> { query.where(approved: true) }
end
endThe block can also use the parent record to select the rows. It does not change which records appear in the attach picker.

To display the same association more than once, use different field IDs and point for_attribute at the original association:
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :comments, as: :has_many
field :approved_comments, as: :has_many,
for_attribute: :comments,
scope: -> { query.where(approved: true) }
end
endHelp users attach records
Use attach_scope to limit the attach picker, independently of the panel's rows. For example, offer only members not already assigned to this team:
# app/avo/resources/team.rb
class Avo::Resources::Team < Avo::BaseResource
def fields
field :members, as: :has_many,
attach_scope: -> { query.where.not(team_id: parent.id) }
end
endBuild on query to preserve the policy scope. A fresh relation can widen that scope, and the attach save accepts exactly the records returned by the block.
For large collections, enable searchable and follow Searchable associations to configure the target resource's search. For selecting several records without server-side search, set attach_using to :checkbox_list. Its inline search only filters records already loaded, subject to the global association lookup limit. Customize rows through the target resource's self.search[:item], using its title, description, image or avatar URL, image format, and image alt values. The picker uses the CheckboxList presentation.
Run code after attaching or detaching
Sinceavo >= v4.3.11Use before_attach or before_detach to reject a change by raising a regular error. Use after_attach and after_detach for notifications or audit entries after the transaction commits.
# app/avo/resources/team.rb
class Avo::Resources::Team < Avo::BaseResource
def fields
field :members, as: :has_many,
after_attach: -> { Rails.logger.info "#{current_user&.id} added #{record.id} to #{parent.id}" }
end
endThese hooks run only through the association's Attach and Detach controls, not through Create, nested forms, deletes, actions, API calls, or model code. See the hook contracts for transaction and error behavior.
Attach through a join model
Avo recognizes has_many :through from the Rails model's reflection; a through: option on the Avo field is not needed. Attach and detach respect a scope on the through association:
# app/models/team.rb
class Team < ApplicationRecord
has_many :admin_memberships, -> { where(level: :admin) }, class_name: "TeamMembership"
has_many :admins, through: :admin_memberships, source: :user
endAttaching a user stamps level: "admin" on the join record. Detaching removes that row without touching a level: "member" row linking the same records.
If the join model has extra attributes, declare them with attach_fields:
# app/avo/resources/store.rb
class Avo::Resources::Store < Avo::BaseResource
def fields
field :patrons, as: :has_many,
attach_fields: -> {
field :review, as: :text
}
end
endavo >= v4.3.5The fields describe the join record, so an association the join model declares belongs there too. With the join model declaring belongs_to :referrer, class_name: "User", optional: true, a belongs_to among the fields renders its usual picker in the attach modal and writes the join record's own foreign key:
field :patrons,
as: :has_many,
through: :patronships,
attach_fields: -> {
field :review, as: :text
field :referrer, as: :belongs_to
}The picker offers what the target resource's policy scope holds, narrowed by that field's own attach_scope. A value matching no record there fails the whole attach, naming the field (Referrer is invalid), and writes nothing: no join record is created.
WARNING
If the through model uses polymorphism, include the required type as a hidden field inside attach_fields, for example field :patronship_type, as: :hidden, default: "TheType". Extra attach fields persist only for a through association.
Customize the panel
Change name for a custom panel title, and add description for explanatory text below it.


For a heavy collection, set loading to :manual. Users see a Load button instead of fetching the table immediately. An opened frame is remembered for 15 minutes by default; change auto_load_for to adjust this sliding window, or set it to 0 or nil to show the placeholder again on every visit. Failed requests display an inline Retry button. Global defaults live in config.associations.
WARNING
A callable description runs even on the manual placeholder. Avoid queries there, or branch on loading_type:
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :orders, as: :has_many, loading: :manual,
description: -> { loading_type == :manual ? "Orders" : "#{query.count} orders" }
end
endAdd reloadable for a reload icon, or linkable for an icon that opens the association table on its own page in a new tab.


Set per_page for a different page size, discreet_pagination to hide pagination details on a single page, hide_search_input to hide table search, or hide_filter_button to hide filters. These controls affect the table, not the attach picker.
If deleting a child changes a parent counter or total, enable refresh_on_destroy to reload the whole page instead of only the association table. The URL and success message are preserved.
Choose the resource for associated records
Set use_resource when the association should use a different Avo resource. For STI rows, use link_to_child_resource to route links to the matching child resource. See STI association setup, including configuring self.model_class on child resources.
Show the panel while editing
Add show_on: :edit to display the association panel on Edit as well as the default view. This still renders the show component, not an inline association editor. See field visibility.
Edit children with their parent
Install avo-nested to enable nested forms:
# Gemfile
gem "avo-nested", source: "https://packager.dev/avo-hq/"Run bundle install; see Gem server authentication if needed. Configure Rails nested attributes on the parent model and enable nested on the field:
# app/models/book.rb
class Book < ApplicationRecord
has_many :authors
accepts_nested_attributes_for :authors, allow_destroy: true
end# app/avo/resources/book.rb
class Avo::Resources::Book < Avo::BaseResource
def fields
field :authors, as: :has_many, nested: { on: :forms, limit: 2 }
end
endThe child resource's edit fields appear within the parent form. nested: true enables both New and Edit; use on: :new or on: :edit to restrict it. The limit hides Add when reached, rather than validating a database constraint.
Delete children when saving the form
Sinceavo-nested >= v4.3.3Unsaved rows can always be removed. Existing rows get Delete only when the model enables allow_destroy: true. Confirmation marks the row for deletion, dims it, and offers Undo; the record is destroyed only when the parent form saves. Translate the confirmation and pending notice through avo.nested.mark_for_deletion_confirmation and avo.nested.pending_deletion in your locale files.
Authorize nested changes
Existing children must be inside the child resource's policy scope and pass update?, both before and after submitted values are assigned. Rows outside the scope or initially denied updates are skipped; a refusal after assigning submitted values prevents the save. A deletion must pass destroy?; refusal prevents the save. New children need both the child policy's create? and the parent policy's create_authors?; refusal prevents the save and the parent check also hides Add.
With explicit_authorization enabled, define the parent's association creation method explicitly:
# app/policies/book_policy.rb
class BookPolicy < ApplicationPolicy
def create_authors?
user.admin?
end
endControl association actions
Use association policy methods to control attach, detach, create, destroy, and action controls. Hiding a search or filter control is not authorization.

Options reference
See the Has Many API for every association-specific option and callback contract, shared field options for common field settings, and Searchable associations for picker configuration.
| Option | Purpose |
|---|---|
scope | Filter listed rows. |
attach_scope | Filter attach choices. |
searchable | Search-as-you-type picker. |
attach_using | Checkbox-list attach picker. |
attach_fields | Extra join-model fields. |
name | Panel title. |
description | Panel explanatory text. |
loading | Lazy or manual frame loading. |
use_resource | Choose an Avo resource. |
link_to_child_resource | STI row links. |
linkable | Open the table in another tab. |
reloadable | Reload the frame. |
for_attribute | Reuse a model association. |
per_page | Association page size. |
discreet_pagination | Hide single-page pagination details. |
hide_search_input | Hide table search. |
hide_filter_button | Hide table filters. |
refresh_on_destroy | Reload the page after deletion. |
nested | Edit children within the parent form. |